Skip to content

fix(responses): stop the chat completions bridge from leaking unknown request fields - #36268

Open
anchan77 wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
anchan77:litellm_fix_responses_bridge_param_leak
Open

fix(responses): stop the chat completions bridge from leaking unknown request fields#36268
anchan77 wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
anchan77:litellm_fix_responses_bridge_param_leak

Conversation

@anchan77

@anchan77 anchan77 commented Aug 8, 2026

Copy link
Copy Markdown

TLDR

Problem this solves:

  • Codex CLI against a chat-completions-only backend 400s on every turn
  • The /v1/responses bridge forwards unknown client fields (client_metadata, custom_metadata) verbatim
  • Spec-valid params like prompt_cache_key and text.verbosity were silently lost instead

How it solves it:

  • The bridge now only forwards litellm params and valid chat completion params
  • Unknown client-only fields are dropped and logged, matching the native responses path
  • prompt_cache_key, prompt_cache_retention, safety_identifier, service_tier, metadata and text.verbosity are now mapped through
  • store, prompt_cache_key and prompt_cache_retention now reach the wire on plain /v1/chat/completions too

User Flow

Before: a Codex CLI user pointing at a proxy whose model is a chat-completions-only backend gets a 400 on every turn

  1. They set base_url = "http://localhost:4000/v1" and wire_api = "responses" for their provider in ~/.codex/config.toml and run codex exec "Create hello.txt containing 'bridge works'"
  2. Codex sends POST http://localhost:4000/v1/responses with its usual payload, which includes a client_metadata field
  3. The turn fails with 400: Invalid JSON payload received. Unknown name "client_metadata": Cannot find field. and Codex prints an ERROR instead of doing the task
  4. Retrying never helps because every Codex request carries that field

After: the same Codex run completes because client-only fields never reach the backend

  1. They set base_url = "http://localhost:4000/v1" and wire_api = "responses" for their provider in ~/.codex/config.toml and run codex exec "Create hello.txt containing 'bridge works'"
  2. Codex sends POST http://localhost:4000/v1/responses with its usual payload, which includes a client_metadata field
  3. The backend accepts the translated request; Codex streams the reply, runs its shell tool twice, and answers "The file hello.txt contains the line 'bridge works'"
  4. The proxy debug log shows bridge dropped unsupported params: ('client_metadata',) on each turn, and prompt_cache_key now reaches backends that support prompt caching

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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

All runs hit a real Google backend over its OpenAI-compatible /chat/completions surface and cost real money. The proxy was run with python litellm/proxy/proxy_cli.py --config /tmp/litellm_codex_bridge_config.yaml --detailed_debug --use_v2_migration_resolver. Note: current litellm_internal_staging HEAD cannot boot the proxy under the locked fastapi 0.141.1 (missing get_flat_dependant); both runs had the one-file boot fix from the unmerged branch litellm_fix_management_v1_flat_dependant applied locally, which is unrelated to request handling

Config used (a chat-completions-only deployment, drop_params: True on, exactly as an affected user would have):

model_list:
  - model_name: codex-bridge
    litellm_params:
      model: openai/google/gemini-2.5-flash
      api_base: https://aiplatform.googleapis.com/v1/projects/<project>/locations/global/endpoints/openapi
      api_key: os.environ/VERTEX_OPENAI_TOKEN
      use_chat_completions_api: true
      additional_drop_params: ["prompt_cache_key", "prompt_cache_retention", "safety_identifier"]
general_settings:
  master_key: sk-1234
litellm_settings:
  drop_params: True

Before, at commit cb211b5 (base of this branch), the exact request the latest Codex (0.135.0) shape produces, against Google's strict Generative Language OpenAI-compat endpoint:

curl -sS http://localhost:4000/v1/responses \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -H "User-Agent: codex_cli_rs/0.135.0" \
  -d '{"model": "codex-bridge", "instructions": "You are Codex, a coding agent.",
       "input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Reply with exactly: bridge-ok"}]}],
       "tool_choice": "auto", "parallel_tool_calls": false, "store": false, "stream": false,
       "include": ["reasoning.encrypted_content"], "prompt_cache_key": "codex-qa-session-1",
       "text": {"verbosity": "low"},
       "client_metadata": {"x-codex-turn-metadata": "{\"turn\":1}"},
       "custom_metadata": {"team": "qa"}}'
{"error":{"message":"litellm.BadRequestError: OpenAIException - Error code: 400 - [{'error': {'code': 400,
'message': 'Invalid JSON payload received. Unknown name \"client_metadata\": Cannot find field.\n
Invalid JSON payload received. Unknown name \"custom_metadata\": Cannot find field.', 'status': 'INVALID_ARGUMENT', ...
HTTP_STATUS:400

Same commit against the more tolerant Vertex AI OpenAI-compat endpoint returns 200 but the debug log proves the leak still goes over the wire:

'extra_body': {'client_metadata': {'x-codex-turn-metadata': '{"turn":1}'}, 'custom_metadata': {'team': 'qa'}, ...

After, at commit 31a4057, the identical curl returns 200 with the model reply:

{"id":"resp_...","object":"response","output":[{"type":"message","id":"...","status":"completed","role":"assistant",
"content":[{"type":"output_text","text":"bridge-ok","annotations":[]}],...}],"status":"completed",...}
HTTP_STATUS:200

and the debug log shows the guard working instead of the leak:

Responses API to chat completion bridge dropped unsupported params: ('client_metadata', 'custom_metadata')

Real Codex CLI end to end, at commit 31a4057 (CODEX_HOME=/tmp/codex-qa-home LITELLM_API_KEY=sk-1234 codex exec --skip-git-repo-check --sandbox workspace-write "Create a file named hello.txt containing the single line 'bridge works', then read it back and tell me its contents."):

OpenAI Codex v0.135.0
model: codex-bridge / provider: litellm
codex
Okay, I'll create `hello.txt` and then read its contents.
exec /bin/zsh -lc "echo 'bridge works' > hello.txt" succeeded
exec /bin/zsh -lc 'cat hello.txt' succeeded: bridge works
codex
The file `hello.txt` contains the line "bridge works".
tokens used: 138

The proxy log for that session shows Codex really sent the field on every turn and the bridge dropped it every time:

$ grep -o "dropped unsupported params: ([^)]*)" litellm.log | sort | uniq -c
   3 dropped unsupported params: ('client_metadata',)

One caveat surfaced during QA: Google's Generative Language compat endpoint also rejects the spec-valid prompt_cache_key this PR now forwards. Backends that reject valid OpenAI chat params can opt out per deployment with additional_drop_params, as in the config above; tolerant spec-compliant backends need nothing

Second backend, at commit 369f059 (current tip), an OpenAI-compatible chat-completions-only deployment registered in the DB through the Admin UI (a customer's self-hosted vLLM gateway, credential stored in the DB), exercising the same Codex-shaped payload:

Routing prerequisite: the deployment needs use_chat_completions_api: true in its litellm_params, otherwise /v1/responses is forwarded verbatim to a backend that has no such route:

curl -sS -X POST http://localhost:4000/model/update \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model_name": "openai/<model>", "model_info": {"id": "<deployment-id>"},
       "litellm_params": {"model": "openai/<model>", "custom_llm_provider": "openai",
                          "litellm_credential_name": "<credential>", "use_chat_completions_api": true}}'

Without that flag the turn fails before reaching the bridge at all:

{"error":{"message":"litellm.NotFoundError: NotFoundError: OpenAIException - 404 page not found
. Received Model Group=openai/<model>\nAvailable Model Group Fallbacks=None","code":"404"}}
HTTP_STATUS:404

With the flag set, the same Codex-shaped request succeeds:

curl -sS http://localhost:4000/v1/responses \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model": "openai/<model>",
       "input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Reply with exactly: responses-bridge-ok"}]}],
       "prompt_cache_key": "bridge-qa-session-1",
       "client_metadata": {"x-codex-turn-metadata": "{\"turn\":1}"},
       "custom_metadata": {"team": "qa"}}'
{"id":"resp_...","object":"response","model":"openai/<model>","status":"completed",
"output":[{"type":"reasoning",...},{"type":"message","status":"completed","role":"assistant",
"content":[{"type":"output_text","text":"responses-bridge-ok","annotations":[]}]}],
"usage":{"input_tokens":57,"output_tokens":71,"total_tokens":128}}
HTTP_STATUS:200

The guard fires on the way through, and the outgoing chat completions body carries prompt_cache_key with an empty extra_body:

Responses API to chat completion bridge dropped unsupported params: ('client_metadata', 'custom_metadata')

POST Request Sent from LiteLLM:
-d '{'model': '<model>', 'messages': [...], 'prompt_cache_key': 'bridge-qa-session-1', 'extra_body': {}}'

Scope note on this backend: it reaches litellm through the stock openai provider and the OpenAI Python SDK, which rejects unrecognized top-level kwargs on its own, so the pre-fix extra_body leak is not observable here the way it is on the raw-HTTP Vertex path above. What this backend does confirm is that the filter is live on a real deployment, that the drop is logged rather than silent, and that prompt_cache_key survives the bridge. text.verbosity does not reach this backend either way: verbosity is gated to the GPT-5 family in gpt_5_transformation.py, so a non-GPT-5 model on the generic openai provider drops it downstream of this PR, matching OpenAI's own model gating

Type

🐛 Bug Fix

Changes

  • litellm/responses/litellm_completion_transformation/handler.py: residual kwargs are filtered against litellm params + OPENAI_CHAT_COMPLETION_PARAMS + provider credential keys (plus extra_body, drop_params, additional_drop_params and friends) before being splatted into litellm.completion; anything else is dropped with a debug log. allowed_openai_params entries always survive, and extra_body remains the verbatim escape hatch. Previously every unknown key was swept into extra_body and merged into the upstream JSON, which drop_params could not prevent
  • litellm/responses/litellm_completion_transformation/transformation.py: the bridge now maps safety_identifier, prompt_cache_key, prompt_cache_retention and text.verbosity -> verbosity, and sources metadata / service_tier from the parsed responses request (they are named aresponses() params, so the old kwargs.get(...) reads could never see them)
  • litellm/main.py: store, prompt_cache_key and prompt_cache_retention are fed into get_optional_params; they are valid chat params excluded from provider-specific passthrough, so before this they silently never reached the wire on any chat completions call
  • Tests: handler-level filter tests (unknown fields dropped, internal/chat/credential params kept, allowed_openai_params honored) and wire-level respx tests asserting the outgoing /chat/completions JSON has no client_metadata/custom_metadata and does carry prompt_cache_key, safety_identifier and verbosity; plus a wire test for store/prompt_cache_key/prompt_cache_retention on plain chat completions. All five behavior tests fail at the base commit and pass here
  • type-discipline-budget.json: LIT002 ceiling lowered by the 2 violations this refactor removed

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@CLAassistant

CLAassistant commented Aug 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The follow-up replaces the bridge filter’s broad Any value type with object, while retaining the allowlist-based filtering and request-parameter forwarding.

  • Filters unsupported Responses API bridge fields before invoking chat completions.
  • Maps supported Responses parameters into their chat-completions equivalents.
  • Adds regression coverage for bridge filtering and wire-level parameter forwarding.
  • Lowers the type-discipline budget to reflect the removed violations.

Confidence Score: 5/5

The PR appears safe to merge with respect to the previously reported typing issue.

No blocking failure remains; the bridge filter now uses the repository-preferred object type instead of Any.

Important Files Changed

Filename Overview
litellm/responses/litellm_completion_transformation/handler.py The previously reported untyped filtering boundary now uses Mapping[str, object], resolving the prior Any concern.
litellm/responses/litellm_completion_transformation/transformation.py Maps supported Responses API request fields into the chat-completions request.
litellm/main.py Adds chat-completions optional-parameter processing for store and prompt-cache fields.
tests/test_litellm/responses/litellm_completion_transformation/test_handler.py Adds bridge allowlist and unsupported-field regression coverage.
tests/test_litellm/responses/test_responses_api_request_body.py Adds wire-level coverage for filtering and mapped Responses API parameters.
tests/test_litellm/test_main.py Verifies store and prompt-cache parameters reach the chat-completions wire payload.
type-discipline-budget.json Reduces the explicit-Any budget after removing the reported violations.

Reviews (2): Last reviewed commit: "refactor(responses): type the bridge kwa..." | Re-trigger Greptile

Comment thread litellm/responses/litellm_completion_transformation/handler.py Outdated
@anchan77

anchan77 commented Aug 8, 2026

Copy link
Copy Markdown
Author

@greptileai typed the bridge filter with object instead of Any in ac82c77, please take another look

…itellm_fix_responses_bridge_param_leak

# Conflicts:
#	tests/test_litellm/test_main.py
#	type-discipline-budget.json
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.91304% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...onses/litellm_completion_transformation/handler.py 76.19% 5 Missing ⚠️
...itellm_completion_transformation/transformation.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing anchan77:litellm_fix_responses_bridge_param_leak (369f059) with litellm_internal_staging (a738c45)

Open in CodSpeed

Existing bridge tests all short-circuited before litellm.completion/
acompletion returned, so the ModelResponse and CustomStreamWrapper
handling branches (both sync and async) were never exercised.
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.

2 participants