From 9572b60b80c7fbda8d4434a1745efb5a25aeb6f4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:09:03 +0000 Subject: [PATCH] refactor(responses): move streaming header restore behind a typed model method Keeps the logged copy's hidden params limited to headers so cost fields on the bridge path are untouched, and avoids private-attr writes from outside the model. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/responses/streaming_iterator.py | 14 +++++++++----- litellm/types/llms/openai.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 127a5301db0..04410827438 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -207,10 +207,11 @@ class BaseResponsesAPIStreamingIterator: "api_base": _api_base, "custom_llm_provider": custom_llm_provider, } - self._hidden_params["additional_headers"] = process_response_headers( - self.response.headers or {} - ) # GUARANTEE OPENAI HEADERS IN RESPONSE - self._hidden_params["headers"] = dict(self.response.headers or {}) # mutable-ok: raw headers for logging + _raw_headers: Final = dict(self.response.headers or {}) # mutable-ok: process_response_headers takes a dict + _processed_headers: Final = process_response_headers(_raw_headers) + self._hidden_params["additional_headers"] = _processed_headers # GUARANTEE OPENAI HEADERS IN RESPONSE + self._raw_headers: Final[Mapping[str, str]] = MappingProxyType(_raw_headers) + self._processed_headers: Final[Mapping[str, object]] = MappingProxyType(_processed_headers) def _check_max_streaming_duration(self) -> None: """Raise litellm.Timeout if the stream has exceeded LITELLM_MAX_STREAMING_DURATION_SECONDS.""" @@ -425,7 +426,10 @@ class BaseResponsesAPIStreamingIterator: copied: Final = self._copy_for_logging(completed) inner_response: Final = getattr(copied, "response", None) if isinstance(inner_response, ResponsesAPIResponse): - inner_response._hidden_params = dict(self._hidden_params) # mutable-ok: model_dump drops private attrs + inner_response.store_provider_response_headers( + processed_headers=self._processed_headers, + raw_headers=self._raw_headers, + ) return copied @staticmethod diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index e7a3f825455..e33e994ad9d 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1337,6 +1337,16 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): # Define private attributes using PrivateAttr _hidden_params: dict = PrivateAttr(default_factory=dict) + def store_provider_response_headers( + self, *, processed_headers: Mapping[str, object], raw_headers: Mapping[str, str] + ) -> None: + """Keep provider headers on a response rebuilt from `model_dump()`, which drops private attrs.""" + self._hidden_params = { # mutable-ok: the private attr is typed as dict + **self._hidden_params, + "additional_headers": dict(processed_headers), # mutable-ok: logging callbacks expect a plain dict + "headers": dict(raw_headers), # mutable-ok: logging callbacks expect a plain dict + } + @field_validator("reasoning", mode="before") @classmethod def validate_reasoning_to_dict(cls, value: Any) -> dict[str, Any] | None: