From 13c66cbb9ec854db57fb1f679001700a1869e904 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Wed, 23 Sep 2026 02:10:56 +0000 Subject: [PATCH] fix(llm): request-log fixes from the live provider matrix - Attribute LiteLLM calls to the configured api_base before LiteLLM's hidden api_base: a non-streamed call through a gateway (OpenRouter) otherwise lands on the provider default (api.openai.com). - Size a mapped LiteLLM error by the body it kept, not the empty stand-in httpx.Response every litellm exception carries. - Accept any non-empty req_ id in an error body (drop the arbitrary 6-char minimum). - Prefer the configured litellm_params.api_base; ignore empty APIStatusError.response.content; regression tests for both. --- strix/llm/request_log.py | 182 +++++++++++++++++++++++----------- tests/test_llm_request_log.py | 166 ++++++++++++++++++++++++++++++- 2 files changed, 289 insertions(+), 59 deletions(-) diff --git a/strix/llm/request_log.py b/strix/llm/request_log.py index 84e0d113..cad9e8f8 100644 --- a/strix/llm/request_log.py +++ b/strix/llm/request_log.py @@ -39,6 +39,7 @@ from typing import TYPE_CHECKING, Any, Literal, cast from urllib.parse import urlsplit from agents.items import ModelResponse +from agents.models.fake_id import FAKE_RESPONSES_ID from agents.models.interface import Model from agents.tool import FunctionTool from agents.usage import Usage @@ -258,23 +259,27 @@ def clean_error_message(exc: BaseException) -> str: # Provider-side request identifiers, in the order they are trusted. Anthropic # and most gateways: ``request-id``; OpenAI/Azure: ``x-request-id``; Bedrock: -# ``x-amzn-requestid``; Vertex/Gemini: ``x-goog-request-id``. LiteLLM prefixes -# stored provider headers with ``llm_provider-``; both spellings are accepted. +# ``x-amzn-requestid``; Vertex/Gemini: ``x-goog-request-id``; OpenRouter: +# ``x-generation-id``. LiteLLM prefixes stored provider headers with +# ``llm_provider-``; both spellings are accepted. _REQUEST_ID_HEADERS: tuple[str, ...] = ( "request-id", "x-request-id", "x-amzn-requestid", "x-amz-request-id", "x-goog-request-id", - "cf-ray", + "x-generation-id", ) +# The Cloudflare edge id: the only handle a reply carries when the provider +# sends none of its own, in the header or the body. +_EDGE_ID_HEADERS: tuple[str, ...] = ("cf-ray",) _LITELLM_HEADER_PREFIX = "llm_provider-" # Anthropic error bodies carry the id even when a gateway strips the header. -_BODY_REQUEST_ID = re.compile(r"[\"']?request_id[\"']?\s*[:=]\s*[\"']?(req_[A-Za-z0-9_-]{6,})") +_BODY_REQUEST_ID = re.compile(r"[\"']?request_id[\"']?\s*[:=]\s*[\"']?(req_[A-Za-z0-9_-]+)") -def request_id_from_headers(headers: Mapping[str, Any] | None) -> str | None: +def _header_value(headers: Mapping[str, Any] | None, names: tuple[str, ...]) -> str | None: if not headers: return None normalized: dict[str, str] = {} @@ -283,12 +288,22 @@ def request_id_from_headers(headers: Mapping[str, Any] | None) -> str | None: continue name = str(key).lower().removeprefix(_LITELLM_HEADER_PREFIX) normalized.setdefault(name, value.strip()) - for name in _REQUEST_ID_HEADERS: + for name in names: if name in normalized: return normalized[name] return None +def request_id_from_headers(headers: Mapping[str, Any] | None) -> str | None: + """The provider's own request id from a reply's headers, if it sent one.""" + return _header_value(headers, _REQUEST_ID_HEADERS) + + +def edge_request_id(headers: Mapping[str, Any] | None) -> str | None: + """The edge network's id, for replies where nothing better exists.""" + return _header_value(headers, _EDGE_ID_HEADERS) + + def request_id_from_text(text: str | None) -> str | None: if not text: return None @@ -296,6 +311,13 @@ def request_id_from_text(text: str | None) -> str | None: return match.group(1) if match else None +def request_id_from_reply(headers: Mapping[str, Any] | None, text: str | None = None) -> str | None: + """Provider header id, then the id in the error body, then the edge id.""" + return ( + request_id_from_headers(headers) or request_id_from_text(text) or edge_request_id(headers) + ) + + # --------------------------------------------------------------------------- # # Free-form telemetry: response headers and details # # --------------------------------------------------------------------------- # @@ -537,6 +559,13 @@ def _str_or_none(value: object) -> str | None: return value if isinstance(value, str) and value else None +def _real_response_id(response: ModelResponse | None) -> str | None: + """The provider's response id; the SDK's chat-completions placeholder is not one.""" + if response is None or response.response_id == FAKE_RESPONSES_ID: + return None + return _str_or_none(response.response_id) + + def _mapping(value: object) -> Mapping[str, Any] | None: return cast("Mapping[str, Any]", value) if isinstance(value, Mapping) else None @@ -594,8 +623,10 @@ def _litellm_request_body(kwargs: Mapping[str, Any]) -> object: params, which is the same information before translation. """ additional = _mapping(kwargs.get("additional_args")) - payload = _mapping(additional.get("complete_input_dict")) if additional else None - if payload: + payload = additional.get("complete_input_dict") if additional else None + if isinstance(payload, str | bytes) and payload: + return payload + if _mapping(payload): return payload body: dict[str, Any] = {"model": kwargs.get("model")} messages = kwargs.get("messages") @@ -627,9 +658,9 @@ def _exception_details(exc: BaseException | None) -> dict[str, Any] | None: if exc is None: return None error: dict[str, Any] = {} - body = getattr(exc, "body", None) - if isinstance(body, Mapping | str): - error["body"] = body if isinstance(body, Mapping) else _mapping(_parse_json(body)) or body + body = _error_body(exc) + if body is not None: + error["body"] = body for name in ("code", "param", "type", "llm_provider", "max_retries", "num_retries"): value = getattr(exc, name, None) if isinstance(value, str | int) and not isinstance(value, bool): @@ -637,9 +668,26 @@ def _exception_details(exc: BaseException | None) -> dict[str, Any] | None: return error or None -def _parse_json(text: str) -> object: +def _error_body(exc: BaseException) -> object: + """The provider's error reply as the SDK kept it. + + The OpenAI SDK parses it onto ``body``. LiteLLM's mapped exceptions keep + only their message, which ends in the reply text (``AnthropicException - + {...}``); the JSON object in that text is the body. + """ + body = getattr(exc, "body", None) + if body is not None: + return body + text = getattr(exc, "message", None) + return _json_object_in(text if isinstance(text, str) else str(exc)) + + +def _json_object_in(text: str) -> Mapping[str, Any] | None: + start, end = text.find("{"), text.rfind("}") + if start < 0 or end <= start: + return None try: - return json.loads(text) + return _mapping(json.loads(text[start : end + 1])) except ValueError: return None @@ -670,17 +718,32 @@ def _exception_headers(exc: BaseException | None) -> Mapping[str, Any] | None: return first -def _exception_body(exc: BaseException) -> object: - """The response body an SDK exception carries, else its message text.""" - body = getattr(exc, "body", None) - if body is not None: - return body - text = getattr(exc, "message", None) - return text if isinstance(text, str) and text else str(exc) +def _exception_body_size(exc: BaseException, status_code: int | None) -> int | None: + """Size of the error reply's body: the bytes the OpenAI SDK read, else the + body LiteLLM kept. Unknown when no reply arrived or the SDK kept none of it. + """ + if status_code is None: + return None + if isinstance(exc, APIStatusError): + # LiteLLM's mapped exceptions subclass this with an empty stand-in response. + with contextlib.suppress(Exception): + content = exc.response.content + if content: + return len(content) + return json_size(_error_body(exc)) -def _exception_body_size(exc: BaseException) -> int | None: - return json_size(_exception_body(exc)) +def _litellm_reply_status(exc: BaseException, headers: Mapping[str, Any] | None) -> int | None: + """The status of the provider's reply, or None when no reply arrived. + + LiteLLM stamps a status on every exception, including 408 on timeouts and + 500 on connection failures that never reached the provider. Those are the + non-status ``APIError`` family and carry no response headers; a reply that + was received always does. + """ + if isinstance(exc, APIError) and not isinstance(exc, APIStatusError) and not headers: + return None + return _int_or_none(getattr(exc, "status_code", None)) def _headers_mapping(headers: object) -> Mapping[str, Any] | None: @@ -713,10 +776,13 @@ def event_from_litellm( or _str_or_none(litellm_params.get("custom_llm_provider")) ) model = _str_or_none(kwargs.get("model")) or _str_or_none(slo.get("model")) or "unknown" + # The endpoint the call was configured with. LiteLLM's hidden ``api_base`` + # can be the provider default (``api.openai.com``) on a non-streamed call + # that actually went to a gateway. host = api_host( - _str_or_none(hidden.get("api_base")) + _str_or_none(litellm_params.get("api_base")) or _str_or_none(slo.get("api_base")) - or _str_or_none(litellm_params.get("api_base")) + or _str_or_none(hidden.get("api_base")) ) streaming = bool(kwargs.get("stream")) or bool(slo.get("stream")) call_id = ( @@ -725,13 +791,12 @@ def event_from_litellm( or str(uuid.uuid4()) ) - response_headers = _mapping(hidden.get("additional_headers")) + # LiteLLM stores the reply's headers on the logging object or on the + # response, depending on the adapter; the first non-empty block is the reply. response_hidden = _mapping(getattr(response, "_hidden_params", None)) or {} - hidden_headers = _mapping(response_hidden.get("additional_headers")) - request_id = request_id_from_headers(response_headers) or request_id_from_headers( - hidden_headers + reply_headers = _mapping(hidden.get("additional_headers")) or _mapping( + response_hidden.get("additional_headers") ) - headers = headers_from_response(response_headers) or headers_from_response(hidden_headers) ttft_ms: int | None = None if streaming: @@ -750,7 +815,7 @@ def event_from_litellm( streaming=streaming, outcome=outcome, status_code=None, - provider_request_id=request_id, + provider_request_id=request_id_from_reply(reply_headers), response_id=None, error_type=None, error_message=None, @@ -762,11 +827,11 @@ def event_from_litellm( retry_attempt=context.retry_attempt, request_bytes=json_size(_litellm_request_body(kwargs)), time_to_first_token_ms=ttft_ms, - response_headers=headers, + response_headers=headers_from_response(reply_headers), ) if outcome == "success": return _litellm_success(base, kwargs, response, hidden, slo) - return _litellm_failure(base, kwargs, exc, error_info, slo) + return _litellm_failure(base, kwargs, exc, error_info, slo, reply_headers) def _litellm_response_details(response: object) -> dict[str, Any] | None: @@ -841,22 +906,21 @@ def _litellm_failure( exc: BaseException | None, error_info: Mapping[str, Any], slo: Mapping[str, Any], + reply_headers: Mapping[str, Any] | None, ) -> LlmRequestEvent: status_code: int | None = None error_type: str | None = None error_message: str | None = None - request_id = base.provider_request_id - headers = base.response_headers response_bytes: int | None = None + error: dict[str, Any] = {} if exc is not None: - status_code = _int_or_none(getattr(exc, "status_code", None)) + reply_headers = reply_headers or _exception_headers(exc) + status_code = _litellm_reply_status(exc, reply_headers) error_type = type(exc).__name__ error_message = clean_error_message(exc) - exc_headers = _exception_headers(exc) - request_id = request_id or request_id_from_headers(exc_headers) - headers = headers or headers_from_response(exc_headers) - response_bytes = _exception_body_size(exc) - if status_code is None: + response_bytes = _exception_body_size(exc, status_code) + error = _exception_details(exc) or {} + else: code = error_info.get("error_code") status_code = _int_or_none(code) if status_code is None and isinstance(code, str) and code.isdigit(): @@ -865,19 +929,21 @@ def _litellm_failure( if error_message is None: raw = _str_or_none(error_info.get("error_message")) or _str_or_none(slo.get("error_str")) error_message = redact(raw)[:ERROR_MESSAGE_MAX_CHARS] if raw else error_type - request_id = request_id or request_id_from_text(error_message) + provider = _str_or_none(error_info.get("llm_provider")) + if provider: + error.setdefault("llm_provider", provider) hidden = _mapping(slo.get("hidden_params")) or {} return replace( base, status_code=status_code, - provider_request_id=request_id, + provider_request_id=request_id_from_reply(reply_headers, error_message), error_type=error_type, error_message=error_message, response_bytes=response_bytes, - response_headers=headers, + response_headers=headers_from_response(reply_headers), details=merge_details( ("request", _litellm_request_details(kwargs)), - ("error", _exception_details(exc) or dict(error_info) or None), + ("error", error or None), ("litellm", _litellm_hidden_details(hidden, slo)), ), ) @@ -995,11 +1061,7 @@ def _openai_error_fields(exc: BaseException) -> tuple[int | None, str | None]: if isinstance(exc, APIStatusError): status = _int_or_none(exc.status_code) request_id = _str_or_none(exc.request_id) - if request_id is None: - request_id = request_id_from_headers(_openai_error_headers(exc)) - if request_id is None: - request_id = request_id_from_text(str(exc)) - return status, request_id + return status, request_id or request_id_from_reply(_openai_error_headers(exc), str(exc)) def _openai_error_headers(exc: BaseException) -> Mapping[str, Any] | None: @@ -1098,7 +1160,7 @@ class RequestLoggingModel(Model): streaming=streaming, outcome="success", status_code=reply.status_code or 200, - provider_request_id=request_id_from_headers(reply.headers), + provider_request_id=request_id_from_reply(reply.headers), response_id=None, error_type=None, error_message=None, @@ -1115,7 +1177,7 @@ class RequestLoggingModel(Model): ) if exc is None: usage = _openai_usage(response) if response is not None else {} - response_id = response.response_id if response is not None else None + response_id = _real_response_id(response) body_source = ( raw_response if raw_response is not None @@ -1136,16 +1198,17 @@ class RequestLoggingModel(Model): ) status, request_id = _openai_error_fields(exc) error_headers = _openai_error_headers(exc) + # A reply the hook saw but the SDK turned into an exception (a + # cancel mid-stream, an unparsable 200) keeps its wire status. + status = status if status is not None else reply.status_code return replace( base, outcome="error", - # A reply the hook saw but the SDK turned into an exception (a - # cancel mid-stream, an unparsable 200) keeps its wire status. - status_code=status if status is not None else reply.status_code, + status_code=status, provider_request_id=request_id or base.provider_request_id, error_type=type(exc).__name__, error_message=_abandonment_message(exc) or clean_error_message(exc), - response_bytes=_exception_body_size(exc), + response_bytes=_exception_body_size(exc, status), response_headers=headers_from_response(error_headers) or base.response_headers, details=merge_details( ("request", request.details), @@ -1407,9 +1470,12 @@ def _openai_response_details( ) -> dict[str, Any] | None: """The Responses API object minus its output, or the SDK usage when that is all there is.""" if isinstance(raw_response, BaseModel): - return raw_response.model_dump( + dumped = raw_response.model_dump( exclude={"output", "instructions", "tools", "text"}, exclude_none=True ) + if dumped.get("id") == FAKE_RESPONSES_ID: + del dumped["id"] + return dumped if response is None: return None return {"usage": response.usage, "output_items": len(response.output)} @@ -1436,7 +1502,7 @@ def failure_text(exc: BaseException) -> str: request_id: str | None = None if isinstance(exc, APIError): _, request_id = _openai_error_fields(exc) - request_id = request_id or request_id_from_headers(_exception_headers(exc)) + request_id = request_id or request_id_from_reply(_exception_headers(exc), text) if request_id and request_id not in text: text = f"{text} [provider request id: {request_id}]" return text @@ -1452,6 +1518,7 @@ __all__ = [ "bind_call_context", "clean_error_message", "current_call_context", + "edge_request_id", "emit", "event_from_litellm", "failure_text", @@ -1463,6 +1530,7 @@ __all__ = [ "redact", "register_sink", "request_id_from_headers", + "request_id_from_reply", "request_id_from_text", "reset_call_context", "sanitize_details", diff --git a/tests/test_llm_request_log.py b/tests/test_llm_request_log.py index 907fc4f0..5e5ed504 100644 --- a/tests/test_llm_request_log.py +++ b/tests/test_llm_request_log.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import logging from dataclasses import dataclass from datetime import UTC, datetime, timedelta @@ -12,10 +13,15 @@ import litellm import pytest from agents.items import ModelResponse from agents.models import _openai_shared +from agents.models.fake_id import FAKE_RESPONSES_ID from agents.models.interface import Model from agents.models.openai_provider import shared_http_client from agents.tool import FunctionTool from agents.usage import Usage +from litellm.exceptions import APIConnectionError as LiteLlmConnectionError +from litellm.exceptions import APIError as LiteLlmApiError +from litellm.exceptions import AuthenticationError as LiteLlmAuthenticationError +from litellm.exceptions import Timeout as LiteLlmTimeout from litellm.llms.anthropic.common_utils import AnthropicError from openai import APIStatusError, APITimeoutError, PermissionDeniedError from openai.types.responses import Response, ResponseCompletedEvent, ResponseCreatedEvent @@ -636,6 +642,33 @@ def test_litellm_request_size_prefers_the_provider_payload_litellm_built() -> No assert "sk-ant-secret" not in str(event.to_dict()) +def test_litellm_request_size_accepts_the_serialized_payload_streaming_adapters_record() -> None: + kwargs = _anthropic_kwargs(None, stream=True) + payload = '{"model":"claude-sonnet-4-5","messages":[],"max_tokens":1,"stream":true}' + kwargs["additional_args"] = {"complete_input_dict": payload} + event = request_log.event_from_litellm(kwargs, _FakeResponse(), None, None, outcome="success") + assert event.request_bytes == len(payload.encode()) + + +def test_openrouter_generation_id_beats_the_cloudflare_ray() -> None: + headers = {"cf-ray": "a3f5d8f23fde88dc-PDX", "x-generation-id": "gen-1790127690-M8vfqPTAr"} + assert request_log.request_id_from_reply(headers) == "gen-1790127690-M8vfqPTAr" + assert request_log.request_id_from_headers({"cf-ray": "a3f5-PDX"}) is None + assert request_log.request_id_from_reply({"cf-ray": "a3f5-PDX"}) == "a3f5-PDX" + + +def test_request_id_in_the_error_body_beats_the_cloudflare_ray() -> None: + """A gateway that strips ``request-id`` still forwards Anthropic's body.""" + body = '{"type":"error","error":{"type":"not_found_error"},"request_id":"req_body1"}' + exc = _anthropic_error(404, body, {"cf-ray": "a3f5-PDX", "content-type": "application/json"}) + event = request_log.event_from_litellm( + _anthropic_kwargs(exc), None, None, None, outcome="error" + ) + assert event.provider_request_id == "req_body1" + assert request_log.request_id_from_reply({"cf-ray": "a3f5-PDX"}, body) == "req_body1" + assert request_log.request_id_from_reply({"cf-ray": "a3f5-PDX"}, "no id here") == "a3f5-PDX" + + def test_litellm_streaming_time_to_first_token_from_completion_start() -> None: kwargs = _anthropic_kwargs(None, stream=True) start = datetime(2026, 9, 19, 12, 0, tzinfo=UTC) @@ -689,13 +722,106 @@ def test_litellm_failure_carries_status_body_size_headers_and_error_details() -> assert "sk-ant-secret" not in str(event.to_dict()) -def test_litellm_failure_without_headers_has_no_headers() -> None: +def test_litellm_failure_without_a_reply_has_no_status_headers_or_size() -> None: exc = APITimeoutError(httpx.Request("POST", "https://api.anthropic.com/v1/messages")) kwargs = _anthropic_kwargs(exc) kwargs["standard_logging_object"]["error_information"]["error_code"] = "" event = request_log.event_from_litellm(kwargs, None, None, None, outcome="error") + assert event.status_code is None assert event.response_headers is None - assert event.response_bytes is not None # size of the timeout message text + assert event.response_bytes is None + assert event.error_type == "APITimeoutError" + + +def test_litellm_connection_failure_drops_the_synthetic_500() -> None: + exc = LiteLlmConnectionError( + message="Connection refused", llm_provider="openrouter", model="openrouter/x" + ) + kwargs = _anthropic_kwargs(exc) + event = request_log.event_from_litellm(kwargs, None, None, None, outcome="error") + assert exc.status_code == 500 + assert event.status_code is None + assert event.response_bytes is None + assert event.provider_request_id is None + + +def test_litellm_timeout_drops_the_synthetic_408() -> None: + exc = LiteLlmTimeout(message="Request timed out.", model="x", llm_provider="openrouter") + kwargs = _anthropic_kwargs(exc) + event = request_log.event_from_litellm(kwargs, None, None, None, outcome="error") + assert exc.status_code == 408 + assert event.status_code is None + assert event.response_bytes is None + + +def test_litellm_api_error_with_headers_keeps_its_status() -> None: + body = '{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}' + exc = LiteLlmApiError( + status_code=529, + message=f"AnthropicException - b'{body}'", + llm_provider="anthropic", + model="x", + ) + # LiteLLM's exception mapping attaches the reply's headers this way. + exc.litellm_response_headers = {"request-id": "req_529"} # type: ignore[attr-defined] + kwargs = _anthropic_kwargs(exc) + event = request_log.event_from_litellm(kwargs, None, None, None, outcome="error") + assert event.status_code == 529 + assert event.provider_request_id == "req_529" + assert event.response_bytes == len(body.encode()) + assert event.details is not None + assert event.details["error"]["body"] == { + "type": "error", + "error": {"type": "overloaded_error"}, + } + + +def test_litellm_mapped_error_without_a_json_body_has_no_size_but_keeps_its_status() -> None: + exc = LiteLlmApiError( + status_code=502, + message="OpenrouterException - bad gateway", + llm_provider="openrouter", + model="x", + ) + exc.litellm_response_headers = {"cf-ray": "a3f5-PDX"} # type: ignore[attr-defined] + event = request_log.event_from_litellm( + _anthropic_kwargs(exc), None, None, None, outcome="error" + ) + assert event.status_code == 502 + assert event.response_bytes is None + assert event.provider_request_id == "a3f5-PDX" + assert event.details is not None + assert "body" not in event.details["error"] + + +def test_litellm_mapped_error_sizes_the_body_it_kept_not_its_empty_stand_in_response() -> None: + # LiteLLM's mapped exceptions subclass openai.APIStatusError and carry an + # httpx.Response with no content; the reply body lives on ``body``/``message``. + body = {"error": {"message": "User not found.", "code": 401}} + exc = LiteLlmAuthenticationError( + message="OpenrouterException - " + json.dumps(body, separators=(",", ":")), + llm_provider="openrouter", + model="x", + ) + assert isinstance(exc, APIStatusError) + assert exc.response.content == b"" + exc.litellm_response_headers = {"cf-ray": "a3f5-PDX"} # type: ignore[attr-defined] + event = request_log.event_from_litellm( + _anthropic_kwargs(exc), None, None, None, outcome="error" + ) + assert event.status_code == 401 + assert event.response_bytes == len(json.dumps(body, separators=(",", ":")).encode()) + + +def test_litellm_host_is_the_configured_endpoint_not_the_provider_default() -> None: + # On a non-streamed call through a gateway, LiteLLM's hidden api_base can be + # the provider default (api.openai.com) while the call went to the gateway. + kwargs = _anthropic_kwargs(None) + kwargs["litellm_params"] = {"api_base": "https://openrouter.ai/api/v1/"} + kwargs["standard_logging_object"]["api_base"] = "https://openrouter.ai/api/v1" + kwargs["standard_logging_object"]["hidden_params"]["api_base"] = "https://api.openai.com" + event = request_log.event_from_litellm(kwargs, _FakeResponse(), None, None, outcome="success") + assert event.api_host == "openrouter.ai" def test_to_dict_and_log_line_include_new_fields() -> None: @@ -820,6 +946,42 @@ async def test_openai_route_success_event(captured: list[LlmRequestEvent]) -> No assert model.model == "gpt-5" +@pytest.mark.asyncio +async def test_openai_route_drops_the_sdk_placeholder_response_id( + captured: list[LlmRequestEvent], +) -> None: + """Chat-completions backends get ``__fake_id__`` from the SDK, not a provider id.""" + model = request_log.RequestLoggingModel( + _Inner(response=_openai_response(FAKE_RESPONSES_ID)), + model_name="anthropic/claude-sonnet-4-5", + provider="openai", + base_url="https://openrouter.ai/api/v1", + ) + await model.get_response(*_CALL_ARGS, **_CALL_KWARGS) + + assert len(captured) == 1 + assert captured[0].response_id is None + assert FAKE_RESPONSES_ID not in str(captured[0].to_dict()) + + +@pytest.mark.asyncio +async def test_openai_route_streaming_drops_the_sdk_placeholder_response_id( + captured: list[LlmRequestEvent], +) -> None: + model = request_log.RequestLoggingModel( + _Inner(stream_events=[_completed_event(FAKE_RESPONSES_ID)]), + model_name="anthropic/claude-sonnet-4-5", + provider="openai", + base_url="https://openrouter.ai/api/v1", + ) + async for _ in model.stream_response(*_CALL_ARGS, **_CALL_KWARGS): + pass + + assert len(captured) == 1 + assert captured[0].response_id is None + assert FAKE_RESPONSES_ID not in str(captured[0].to_dict()) + + @pytest.mark.asyncio async def test_openai_route_blocked_request_keeps_header_request_id( captured: list[LlmRequestEvent],