feat: record call observability columns and terminal failure rows
Grow the telemetry contract from 26 to 36 fields and give every logical call a failure terminal row, so SQL can finally answer "how many calls failed" and "why did the whole pool die". Schema and port move together with the emitter writes in one commit: splitting them would ship columns that nothing populates. - schema: append 10 nullable columns (scope, operation, logical_call_id, event_kind, http_status_code, error_type, cause_type, error_body, attempts, total_latency_ms) to all five definition sites in one order - ports: 10 keyword-only parameters without defaults; the protocol signature is now the single source the assembly gate derives from - emitter: take domain exception objects instead of pre-flattened text and pin down the diagnostics in one helper; a relabelled 503 stays 503 and success rows leave all five columns NULL - emitter: reject recorders whose record_llm_call cannot accept the current field shape at assembly time, since _record would otherwise swallow the TypeError and drop every row while calls keep succeeding - clients: write at most one terminal row per logical call through a single shared exit, deduplicated by the call context; TelemetryMW stops writing terminals so the two sites cannot double count - clients: cancellation stays best effort and propagates, non-domain exceptions get no terminal row and keep their classification - transports: give _status_to_error an explicit operation and fix the historically mislabelled embedding HTTP failures - structured: promote the bounded error formatter so the reask feedback and the terminal explanation share one set of limits Terminal rows carry no cost and no tokens, so cost aggregation is unchanged; failure counts must now filter on event_kind.
This commit is contained in:
@@ -154,18 +154,28 @@ def _classify(status: int) -> tuple[type[PolyGatewayError], str]:
|
||||
|
||||
|
||||
def _status_to_error(
|
||||
source: SourceConfig, status: int, body_text: str, headers: Mapping[str, str]
|
||||
source: SourceConfig,
|
||||
status: int,
|
||||
body_text: str,
|
||||
headers: Mapping[str, str],
|
||||
*,
|
||||
operation: str,
|
||||
) -> Exception:
|
||||
"""非 2xx → 领域错误,**全部分支**携带响应体摘要(issue #10)。
|
||||
|
||||
摘要只算一次,message 与 `body_text` 共用同一份串: 两份不同长度会让"遥测里
|
||||
看到的"与"下游 catch 到的"对不上,排查时反而多一层困惑。
|
||||
|
||||
`operation` 是 **HTTP 子操作**词表(`chat` / `embedding` / `ocr_text` / ...),由
|
||||
调用点显式给定。它曾被硬编码成 `"chat"`,而 `embed()` 的非 200 分支也走它
|
||||
——于是现存所有 embedding HTTP 失败的 `exc.operation` 都是错的(1.3.5 设计 §5 I1)。
|
||||
注意它与遥测新列 `operation`(公开方法四值)是**两个语义**,不做自动转换。
|
||||
"""
|
||||
summary = summarize_body(body_text)
|
||||
ctx: dict[str, Any] = {
|
||||
"source_name": source.name,
|
||||
"status_code": status,
|
||||
"operation": "chat",
|
||||
"operation": operation,
|
||||
"body_text": summary,
|
||||
}
|
||||
if status == 429:
|
||||
@@ -509,7 +519,10 @@ class OpenAICompatTransport:
|
||||
except httpx.TransportError as exc:
|
||||
raise TransientError(f"{source.name} 网络错误: {exc}", **ctx) from exc
|
||||
if resp.status_code != 200:
|
||||
raise _status_to_error(source, resp.status_code, resp.text, resp.headers)
|
||||
# 历史误标修正: 本分支属 `embed()`,与上方 ctx 同为 `"embedding"`
|
||||
raise _status_to_error(
|
||||
source, resp.status_code, resp.text, resp.headers, operation="embedding"
|
||||
)
|
||||
return _parse_embedding_payload(resp, source, len(texts))
|
||||
|
||||
async def _complete_stream(
|
||||
@@ -524,7 +537,9 @@ class OpenAICompatTransport:
|
||||
async with client.stream("POST", url, json=payload) as resp:
|
||||
if resp.status_code != 200:
|
||||
body = (await resp.aread()).decode("utf-8", errors="replace")
|
||||
raise _status_to_error(source, resp.status_code, body, resp.headers)
|
||||
raise _status_to_error(
|
||||
source, resp.status_code, body, resp.headers, operation="chat"
|
||||
)
|
||||
sink: dict[str, Any] = {}
|
||||
guarded = stream_with_liveness_timeouts(
|
||||
_iter_sse_deltas(resp.aiter_lines(), sink),
|
||||
@@ -622,7 +637,9 @@ class OpenAICompatTransport:
|
||||
"""非流式快路径(三项目均无,库新增): 单 JSON 响应,仅 total 超时。"""
|
||||
resp = await client.post(url, json=payload)
|
||||
if resp.status_code != 200:
|
||||
raise _status_to_error(source, resp.status_code, resp.text, resp.headers)
|
||||
raise _status_to_error(
|
||||
source, resp.status_code, resp.text, resp.headers, operation="chat"
|
||||
)
|
||||
try:
|
||||
body = resp.json()
|
||||
except json.JSONDecodeError as exc:
|
||||
|
||||
Reference in New Issue
Block a user