feat(llm): carry the upstream provider and error type as request-log event fields

This commit is contained in:
Ian 2026-10-07 16:43:12 -04:00
parent dde135c2f3
commit e68c75b9c2
2 changed files with 11 additions and 18 deletions

View file

@ -109,6 +109,9 @@ class LlmRequestEvent:
# Streamed calls only.
time_to_first_token_ms: int | None = None
finish_reason: str | None = None
# The provider a gateway (OpenRouter) routed the attempt to, and its error type.
upstream_provider: str | None = None
upstream_error_type: str | None = None
# Every response header the provider sent, name-normalized, values capped.
response_headers: dict[str, str] | None = None
# Free-form: request parameters, full usage object, SDK hidden params,
@ -177,15 +180,6 @@ def record_upstream_provider(provider: object, error_type: object = None) -> Non
reply.upstream_error_type = error_type
def _upstream_details(reply: HttpReply | None) -> list[tuple[str, object]]:
if reply is None:
return []
return [
("upstream_provider", reply.upstream_provider),
("upstream_error_type", reply.upstream_error_type),
]
async def record_http_reply(response: Response) -> None:
"""httpx ``response`` event hook: remember the reply for the attempt in flight.
@ -704,6 +698,7 @@ def event_from_litellm(
ttft_ms = max(0, int((first_at - started).total_seconds() * 1000))
context = current_call_context()
reply = _http_reply.get() or HttpReply()
base = LlmRequestEvent(
call_id=call_id,
route="litellm",
@ -726,6 +721,8 @@ def event_from_litellm(
request_bytes=json_size(_litellm_request_body(kwargs)),
time_to_first_token_ms=ttft_ms,
response_headers=headers_from_response(reply_headers),
upstream_provider=reply.upstream_provider,
upstream_error_type=reply.upstream_error_type,
)
if outcome == "success":
return _litellm_success(base, kwargs, response, hidden, slo)
@ -794,7 +791,6 @@ def _litellm_success(
("request", _litellm_request_details(kwargs)),
("response", _litellm_response_details(response)),
("litellm", _litellm_hidden_details(hidden, slo)),
*_upstream_details(_http_reply.get()),
),
)
@ -844,7 +840,6 @@ def _litellm_failure(
("request", _litellm_request_details(kwargs)),
("error", error or None),
("litellm", _litellm_hidden_details(hidden, slo)),
*_upstream_details(_http_reply.get()),
),
)
@ -918,7 +913,6 @@ def _observe_sdk_shared_http_client() -> None:
def _log_line_sink(event: LlmRequestEvent) -> None:
details = event.details or {}
level = logging.DEBUG if event.outcome == "success" else logging.WARNING
logger.log(
level,
@ -928,8 +922,8 @@ def _log_line_sink(event: LlmRequestEvent) -> None:
"in=%s out=%s cached=%s cost=%s agent=%s attempt=%d%s",
event.route,
event.provider or "-",
details.get("upstream_provider") or "-",
details.get("upstream_error_type") or "-",
event.upstream_provider or "-",
event.upstream_error_type or "-",
event.model,
event.api_host or "-",
event.outcome,
@ -1077,6 +1071,8 @@ class RequestLoggingModel(Model):
time_to_first_token_ms=ttft_ms,
finish_reason=finish_reason,
response_headers=headers_from_response(reply.headers),
upstream_provider=reply.upstream_provider,
upstream_error_type=reply.upstream_error_type,
)
if exc is None:
usage = _openai_usage(response) if response is not None else {}
@ -1097,7 +1093,6 @@ class RequestLoggingModel(Model):
details=merge_details(
("request", request.details),
("response", _openai_response_details(response, raw_response)),
*_upstream_details(reply),
),
)
status, request_id = _openai_error_fields(exc)
@ -1117,7 +1112,6 @@ class RequestLoggingModel(Model):
details=merge_details(
("request", request.details),
("error", _exception_details(exc)),
*_upstream_details(reply),
),
)

View file

@ -410,8 +410,7 @@ def test_log_line_names_upstream_recorded_during_attempt(
)
finally:
request_log._http_reply.reset(token)
assert event.details is not None
assert event.details["upstream_provider"] == "Together"
assert event.upstream_provider == "Together"
with caplog.at_level(logging.DEBUG, logger="strix.llm.request_log"):
request_log._log_line_sink(event)
line = caplog.records[-1].getMessage()