Skip to content

fix(responses-bridge): improve OpenAI Codex CLI compatibility with Chat Completions providers - #31571

Closed
duanhongyi wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
duanhongyi:fix/codex-compatibility
Closed

fix(responses-bridge): improve OpenAI Codex CLI compatibility with Chat Completions providers#31571
duanhongyi wants to merge 5 commits into
BerriAI:litellm_internal_stagingfrom
duanhongyi:fix/codex-compatibility

Conversation

@duanhongyi

@duanhongyi duanhongyi commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes Codex CLI apply_patch round-trip when bridging Responses API through Chat Completions providers. allowed_callers allowlist was silently dropped during custom→function conversion, allowing a tool meant to be callable only by another tool to be invoked directly by the model.

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 requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

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

Type

🐛 Bug Fix
🧹 Refactoring

Changes

Problem: When the LiteLLM bridge routes /responses requests to Chat Completions-only providers, tools with type: "custom" (e.g., Codex CLI's apply_patch) were silently dropped or treated as regular functions. The model returned a function_call output item that the client could not map back to custom_tool_call, breaking the round-trip. Additionally, allowed_callers allowlist was lost during custom→function conversion — a silent security regression.

Fix:

  1. New module: litellm/responses/litellm_completion_transformation/custom_tools.py

    • Forward-converts custom tools to function tools (grammar embedded in description so the model produces correctly-formatted output).
    • Reverse-converts function_call responses back to custom_tool_call output items (unwrapping the {"content": ...} JSON envelope).
    • Validates and preserves allowed_callers so the Anthropic adapter's caller allowlist is maintained.
    • Forward and reverse paths share a single module so future custom tool types can be added without touching the streaming iterator or transformation logic.
  2. transformation.py — Integrates custom tool conversion into the Responses→ChatCompletions bridge. Emits custom_tool_call output items with unwrapped input string (not JSON arguments) when the model returns a function_call for a tool that was originally custom. Migrated to modern type annotations (list/dict/set/X | None) throughout to keep the ruff strict budget within its ratcheted ceilings.

  3. streaming_iterator.py — Streaming path uses shared build_tool_call_item_kwargs() to emit either function_call or custom_tool_call items depending on whether the call targets a custom tool. Custom tool names extracted from request tools at construction time. Same modern annotation cleanup.

  4. router.py — Fixes StopAsyncIteration handling in the streaming wrapper: when the inner generator is exhausted, falls back to the source iterator's completed_response instead of logging a spurious "no completed_response" warning.

  5. common_request_processing.py — Defensive fix for _apply_client_disconnect_metadata when the target dict is None (happens when logging_obj metadata is missing during streaming disconnect).

  6. Types: Added CustomToolCallOutputItem in litellm/types/responses/main.py and registered it in the ResponsesAPIResponse allowed output items union.

Tests: Coverage added in test_custom_tool_call.py (custom→function→custom_tool_call round-trip, allowed_callers preservation, edge cases), test_litellm_completion_responses.py (transformation integration), and test_common_request_processing.py (client disconnect metadata fix).

Files changed: 10 files, +1204 / −342

yuneng-berri and others added 3 commits June 26, 2026 09:59
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
@CLAassistant

CLAassistant commented Jun 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the Codex CLI apply_patch round-trip when routing Responses API requests through Chat Completions providers by adding proper custom_tool_call support to the LiteLLM bridge.

  • New custom_tools.py module: converts type: "custom" tools to function tools (embedding grammar in description) for Chat Completions providers, and reverses function_call responses back to custom_tool_call output items with unwrapped input strings; allowed_callers is validated and preserved through the conversion.
  • Streaming + non-streaming paths updated: OutputItemAddedEvent and OutputItemDoneEvent now correctly emit custom_tool_call type items for tools that were originally custom; transform_chat_completion_tools_to_responses_tools now returns CustomToolCallOutputItem instead of ResponseFunctionToolCall for those tools.
  • Two supporting fixes: router.py FallbackResponsesStreamWrapper.__anext__ now falls back to source_iterator.completed_response on StopAsyncIteration; common_request_processing.py correctly creates and persists a new metadata dict instead of calling setdefault (which silently returns None when the key already maps to None).

Confidence Score: 5/5

Safe to merge; the changes are well-scoped to the Responses API bridge path and do not affect unrelated call paths.

The core custom-tool round-trip logic is well-tested (unit tests for every converter function plus integration tests for the full transformation path). The common_request_processing.py fix correctly migrates the logging_obj paths to an explicit get+assign pattern rather than setdefault, and the new test validates persistence. Two minor edge cases worth following up: the intermediate streaming delta events still use function_call_arguments event types for custom tool calls, and allowed_callers is forwarded to upstream providers as a non-standard field in the tool dict.

streaming_iterator.py (intermediate delta event types for custom tool calls) and custom_tools.py (the allowed_callers field in the upstream-provider request)

Important Files Changed

Filename Overview
litellm/responses/litellm_completion_transformation/custom_tools.py New module implementing custom→function and function_call→custom_tool_call conversions; logic is clean and well-tested
litellm/responses/litellm_completion_transformation/transformation.py Integrates custom tool conversion into the bridge; streaming delta events still use function_call_arguments semantics for custom tool calls
litellm/responses/litellm_completion_transformation/streaming_iterator.py OutputItemAdded/Done events correctly use custom_tool_call type; intermediate delta events (FunctionCallArgumentsDeltaEvent/DoneEvent) still use function_call_arguments semantics
litellm/proxy/common_request_processing.py Correctly fixes the None-metadata crash in logging_obj paths by creating and storing new dicts rather than using setdefault
litellm/router.py StopAsyncIteration handler in FallbackResponsesStreamWrapper correctly falls back to source_iterator.completed_response when wrapper has no latched terminal event
litellm/types/responses/main.py CustomToolCallOutputItem added as a Pydantic model with correct fields; registered in ResponsesAPIResponse output union
tests/test_litellm/responses/test_custom_tool_call.py New test file with thorough coverage of custom tool utilities and round-trip transformation; all tests use mocks, no real network calls
tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py test_transform_computer_use_tools assertion changed from pass-through to drop; behavioral change is documented and correct
tests/test_litellm/proxy/test_common_request_processing.py Two new tests correctly verify None-metadata fix: one for the streaming disconnect handler, one directly for the helper function

Reviews (4): Last reviewed commit: "fix(responses-bridge): custom tool round..." | Re-trigger Greptile

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.45226% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...itellm_completion_transformation/transformation.py 80.00% 19 Missing ⚠️

📢 Thoughts on this report? Let us know!

@duanhongyi
duanhongyi force-pushed the fix/codex-compatibility branch 5 times, most recently from f79fefc to cf0cc73 Compare June 29, 2026 07:59
@veria-ai

veria-ai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 2 · PR risk: 0/10

@duanhongyi
duanhongyi force-pushed the fix/codex-compatibility branch 2 times, most recently from 197bc39 to 17f26f3 Compare June 30, 2026 10:08
@duanhongyi

Copy link
Copy Markdown
Contributor Author

Reorganized the commits and added some test cases.

Comment thread litellm/responses/litellm_completion_transformation/custom_tools.py
@duanhongyi
duanhongyi force-pushed the fix/codex-compatibility branch 3 times, most recently from 7aeccf6 to 125c4fa Compare June 30, 2026 15:54
chore(ci): promote internal staging to main
@duanhongyi

Copy link
Copy Markdown
Contributor Author

@greptileai
I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5

@duanhongyi

duanhongyi commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@mateo-berri

All CI checks pass (77/77), Greptile confidence 5/5, and both Veria security findings are resolved.

What this fixes: When the LiteLLM bridge routes /responses requests to Chat Completions-only providers, tools with type: "custom" (e.g. Codex CLI's apply_patch) were silently dropped or returned as function_call output items the client couldn't map back, breaking the round-trip. allowed_callers was also lost during custom→function conversion — a silent security regression.

What changed:

  • New custom_tools.py module handles custom↔function conversion and preserves allowed_callers
  • transformation.py + streaming_iterator.py emit custom_tool_call output items with unwrapped input
  • Types: added CustomToolCallOutputItem to the ResponsesAPIResponse output union
  • 371 lines of new tests covering the full round-trip + edge cases

Could a maintainer take a look? cc @mateo-berri — you recently touched the Responses tools bridge (#31663)

@duanhongyi
duanhongyi force-pushed the fix/codex-compatibility branch from 125c4fa to 81bc704 Compare July 2, 2026 01:30
@duanhongyi
duanhongyi requested a review from a team July 2, 2026 01:30
…ion for Codex CLI

Convert Responses API custom tools to Chat Completions function tools and map
function_call responses back to custom_tool_call output items so Codex CLI gets
the apply_patch round-trip it expects. Preserve and validate allowed_callers
during the custom->function conversion so the Anthropic adapter's caller
allowlist is not silently dropped, which would let a tool meant to be callable
only by another tool be invoked directly by the model. Use modern type
annotations (list/dict/set/X | None) throughout to keep the ruff strict budget
within its ratcheted ceilings.
@duanhongyi
duanhongyi force-pushed the fix/codex-compatibility branch from 81bc704 to d3ce123 Compare July 2, 2026 01:34
@mateo-berri

Copy link
Copy Markdown
Contributor

Hey @duanhongyi. This seems like a cool add. I'll make a copy of this PR to see if it passes internal e2e tests. In a sentence or two, could you tell me what this PR solves concretely?

@mateo-berri

Copy link
Copy Markdown
Contributor

This has been merged. Thank you for the contribution @duanhongyi!

@mateo-berri mateo-berri closed this Jul 7, 2026
@duanhongyi

Copy link
Copy Markdown
Contributor Author

Hey @duanhongyi. This seems like a cool add. I'll make a copy of this PR to see if it passes internal e2e tests. In a sentence or two, could you tell me what this PR solves concretely?

Hey @mateo-berri, thanks! Concretely: Codex CLI sends type: "custom" tools (like apply_patch) through the /responses endpoint, but when LiteLLM bridges those to Chat-Completions-only providers the tools get passed through verbatim and the provider rejects them with 400 'function' is a required property. This PR converts custom tools to standard function tools on the way out and maps the function_call response back to custom_tool_call on the way back, so Codex CLI works end-to-end against any Chat Completions provider.

@duanhongyi

Copy link
Copy Markdown
Contributor Author

I've been dogfooding this PR for about two weeks — Codex CLI paired with GLM-5.2, DeepSeek V4 Pro, and Kimi 2.7 Code — and it's been running very smoothly. You just need to set use_chat_completions_api: true on the model config.

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.

5 participants