fix(responses): tolerate dict response payloads in streaming events#34756
fix(responses): tolerate dict response payloads in streaming events#34756devin-ai-integration[bot] wants to merge 2 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Greptile SummaryThis PR hardens Responses API streaming against unvalidated dictionary payloads and null output values.
Confidence Score: 4/5The usage normalization defect should be fixed before merging because malformed nested details currently remove valid token totals from fallback and logging accounting. The helper validates the complete usage object atomically and returns no usage when a nested detail fails validation, even when all top-level token counts are valid, so downstream aggregation and spend logging undercount the request. Files Needing Attention: litellm/responses/utils.py, litellm/router.py, litellm/litellm_core_utils/litellm_logging.py
|
| Filename | Overview |
|---|---|
| litellm/llms/openai/responses/transformation.py | Reconstructs the nested response and usage models before returning an otherwise unvalidated streaming event. |
| litellm/responses/utils.py | Adds shared usage helpers, but whole-object validation discards valid top-level token totals when nested usage details are malformed. |
| litellm/router.py | Routes streaming fallback usage extraction and combination through the new helpers, inheriting their all-or-nothing normalization behavior. |
| litellm/litellm_core_utils/litellm_logging.py | Normalizes dictionary response and usage payloads while assembling completed streams, but malformed nested usage details still remove valid totals. |
| litellm/completion_extras/litellm_responses_transformation/transformation.py | Safely interprets a null completed-response output as empty when determining the bridged finish reason. |
Reviews (1): Last reviewed commit: "fix(responses): tolerate dict response p..." | Re-trigger Greptile
| if isinstance(usage, Mapping): | ||
| try: | ||
| return ResponseAPIUsage(**dict(usage)) | ||
| except ValidationError: | ||
| return None |
There was a problem hiding this comment.
Malformed details discard token totals
When a dict contains valid input_tokens, output_tokens, and total_tokens but a malformed nested details field, validating the entire object raises and this helper returns None, causing fallback aggregation and streaming logging to discard the valid top-level counts and underreport usage and spend.
Knowledge Base Used:
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
TLDR
Problem this solves:
AttributeError: 'dict' object has no attribute 'usage'mid-streamresponse.output: nullcrashes the /chat/completions bridgeHow it solves it:
event.responsetyped when validation falls back tomodel_constructoutputas an empty listRelevant issues
Fixes #34754
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Repro setup: a local OpenAI-compatible upstream that streams a non-compliant
response.completed(nocreated_at,output: null, usage as a plain dict), which is exactly the payload shape that makes litellm fall back tomodel_construct; the happy path is a real, paidgpt-5.6call through the same proxyproof_config.yamlBefore, at 2412326 (
python litellm/proxy/proxy_cli.py --config proof_config.yaml --detailed_debug)proxy log; the event was built with
model_construct, so the response stayed a raw dict and the spend entry lost its costAfter, at 4859517, same curl; the stream is unchanged and the log no longer carries a cost failure
Real provider sanity check on the same build, a paid streaming call to OpenAI
The two dict-usage paths (
Router._extract_partial_responses_usageandLogging._get_assembled_streaming_response) need a provider that both fails validation and drops mid-stream, which I could not force against a live provider; they are covered by the regression tests insteadType
🐛 Bug Fix
Changes
The root cause is in
OpenAIResponsesAPIConfig.transform_streaming_response: when a provider'sresponse.completedpayload fails pydantic validation, the code falls back toevent_pydantic_model.model_construct(**parsed_chunk), which skips validation for nested fields too. The event then carries a plain dict where aResponsesAPIResponseis declared, and every downstreamevent.response.usageaccess blows up.Cost tracking had the same null-output crash (
StandardBuiltInToolCostTracking.response_includes_output_typeiteratingresponse_object.output), which silently zeroed out the spend entry, so it takes the sameor []treatment.The consumers are hardened as well, since nothing stops another code path from handing them an unvalidated payload.
litellm/responses/utils.pygrowsnormalize_response_api_usage,get_response_api_usageandset_response_api_usage; the router's_extract_partial_responses_usage/_combine_responses_fallback_usageandLogging._get_assembled_streaming_responsego through those instead of reaching for.usagedirectly. The logging path previously only transformed usage when it was already aResponseAPIUsageinstance, so a dict-shaped usage silently produced no chat-style usage; it now normalizes first, which is what keeps the spend log entry.Last, the /responses -> /chat/completions bridge treated
response.outputas always iterable.response_data.get("output", [])returnsNonewhen the key is present and null, sofor item in output_itemsraised. It now falls back to an empty list, and the completed chunk still reports usage andfinish_reason.Final Attestation
Link to Devin session: https://app.devin.ai/sessions/b310da521394445bb32dfb071ff7fdd5