Skip to content

fix(passthrough): use Responses API config for /v1/responses logging - #29666

Closed
DanielMaly wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
DanielMaly:fix_passthrough_responses_parser
Closed

fix(passthrough): use Responses API config for /v1/responses logging#29666
DanielMaly wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
DanielMaly:fix_passthrough_responses_parser

Conversation

@DanielMaly

Copy link
Copy Markdown

Problem

The is_responses branch in openai_passthrough_logging_handler.py calls OpenAIConfig().transform_response() — the chat completions parser — for Responses API passthrough requests. This parser expects a choices key in the response JSON, but Responses API responses have output/usage instead, causing an APIError and producing an empty ModelResponse.

Downstream, Langfuse reads the empty ModelResponse and logs input_tokens=0, output_tokens=0, total_tokens=0 even though LiteLLM's internal spend tracker has the correct token counts (tracked via a separate billing path).

Fix

Use OpenAIResponsesAPIConfig().transform_response_api_response() for the is_responses branch, which correctly parses Responses API JSON (output, usage with input_tokens/output_tokens) and returns a proper ResponsesAPIResponse.

Also:

  • Added ResponsesAPIResponse to the PassThroughEndpointLoggingResultValues union type
  • Fixed the endpoint_type debug log to include "responses"
  • Updated the existing test_responses_api_cost_tracking test to exercise the real code path (removed mock of get_provider_config that was papering over the bug)
  • Added test_responses_api_returns_usage_not_zero regression test documenting the root cause

Relationship to PR #29574

This PR is a separate, complementary fix to #29574:

  • fix(langfuse): log Responses API usage #29574 fixes the Langfuse callback (langfuse.py) to correctly read usage from ResponsesAPIResponse objects
  • This PR fixes the passthrough handler to actually produce ResponsesAPIResponse objects instead of empty ModelResponse objects

Both fixes are needed for complete Langfuse observability of Responses API traffic through passthrough endpoints.

Refs: #29575

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug in the OpenAI passthrough logging handler where the is_responses branch incorrectly used OpenAIConfig.transform_response() (the chat completions parser, which expects a choices key) for Responses API traffic. Because Responses API responses carry output/usage instead of choices, the old parser raised an APIError, producing an empty ModelResponse with zero token counts — causing Langfuse to log input_tokens=0, output_tokens=0.

  • Switches the is_responses branch to OpenAIResponsesAPIConfig.transform_response_api_response(), which correctly parses output/usage fields and returns a ResponsesAPIResponse.
  • Adds ResponsesAPIResponse to the PassThroughEndpointLoggingResultValues union and expands the endpoint_type debug string to include \"responses\" and \"unknown\".
  • Removes the get_provider_config mock from the existing test (which was papering over the bug) and adds a regression test asserting non-zero usage after the fix."

Confidence Score: 4/5

The change is a targeted swap of one parser call for a correctly typed one, with a pre-existing outer try/except fallback in the handler, so a mis-parse cannot break the passthrough pipeline.

The core handler change is straightforward and well-tested by both the updated existing test and the new regression test. The only observation is that the dual-path usage assertion in the tests could silently accept the model_construct fallback path instead of failing loudly on a schema mismatch, so a future regression there would be harder to catch.

The test file's dual isinstance guard on usage (lines 696-704 and 768-775) deserves a second look to ensure it enforces the expected type rather than accepting both shapes.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds ResponsesAPIResponse to the PassThroughEndpointLoggingResultValues union type — a necessary companion change to the handler fix.
litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py Replaces OpenAIConfig.transform_response() (chat completions parser) with OpenAIResponsesAPIConfig.transform_response_api_response() for the is_responses branch, fixing zero-token logging. Also expands the endpoint_type debug string to cover "responses" and "unknown".
tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_openai_passthrough_logging_handler.py Removes the get_provider_config mock that papered over the bug, now exercises the real OpenAIResponsesAPIConfig path. Adds a regression test confirming non-zero usage. Usage assertion uses a dual isinstance guard (dict vs model) that could silently mask model_construct fallback.

Reviews (1): Last reviewed commit: "fix(passthrough): use Responses API conf..." | Re-trigger Greptile

Comment on lines +696 to +704
# usage may be a ResponseAPIUsage model or a plain dict depending on
# how the response was constructed (model_construct vs strict init)
usage = result["result"].usage
if isinstance(usage, dict):
assert usage["input_tokens"] == 20
assert usage["output_tokens"] == 15
else:
assert usage.input_tokens == 20
assert usage.output_tokens == 15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Dual-path usage assertion may silently accept model_construct fallback

The test accepts both dict and Pydantic-model shapes for usage. With valid JSON the strict ResponsesAPIResponse(...) constructor should always succeed, making usage a proper ResponseAPIUsage object — not a dict. If transform_response_api_response unexpectedly falls back to model_construct (e.g. a schema change), the dict branch would hide that regression. Asserting the concrete type with assert not isinstance(usage, dict) before the attribute access would make the intent explicit and catch the fallback earlier.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — addressed in the amended push. The test now asserts not isinstance(usage, dict) so any unexpected model_construct fallback would fail immediately rather than being silently accepted. Also added total_tokens to the mock usage dicts so the strict ResponsesAPIResponse() constructor succeeds (it was previously falling back to model_construct because total_tokens was missing).

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ugh logging

The is_responses branch in openai_passthrough_logging_handler used
OpenAIConfig().transform_response() (chat completions parser) which
expects 'choices' in the response JSON. Responses API responses have
'output'/'usage' instead, causing APIError → empty ModelResponse →
Langfuse logs input=0, output=0.

Fix: use OpenAIResponsesAPIConfig().transform_response_api_response()
for the is_responses branch, which correctly parses Responses API JSON.

Also adds ResponsesAPIResponse to PassThroughEndpointLoggingResultValues
union type so the return type is accurate.

Refs: BerriAI#29575
@DanielMaly

Copy link
Copy Markdown
Author

Closing as superseded by #29728, which merged the same Responses API passthrough transformer fix and also includes the outer dispatch-gate coverage and stronger dispatch tests. Thanks for getting this in!

@DanielMaly DanielMaly closed this Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant