fix(responses-bridge): improve OpenAI Codex CLI compatibility with Chat Completions providers - #31571
Conversation
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
chore(ci): promote internal staging to main
Greptile SummaryThis PR fixes the Codex CLI
Confidence Score: 5/5Safe 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)
|
| 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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
f79fefc to
cf0cc73
Compare
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 2 · PR risk: 0/10 |
197bc39 to
17f26f3
Compare
|
Reorganized the commits and added some test cases. |
7aeccf6 to
125c4fa
Compare
chore(ci): promote internal staging to main
|
@greptileai |
|
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 What changed:
Could a maintainer take a look? cc @mateo-berri — you recently touched the Responses tools bridge (#31663) |
125c4fa to
81bc704
Compare
…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.
81bc704 to
d3ce123
Compare
|
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? |
|
This has been merged. Thank you for the contribution @duanhongyi! |
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. |
|
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. |
Relevant issues
Fixes Codex CLI
apply_patchround-trip when bridging Responses API through Chat Completions providers.allowed_callersallowlist 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
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays 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
/responsesrequests to Chat Completions-only providers, tools withtype: "custom"(e.g., Codex CLI'sapply_patch) were silently dropped or treated as regular functions. The model returned afunction_calloutput item that the client could not map back tocustom_tool_call, breaking the round-trip. Additionally,allowed_callersallowlist was lost during custom→function conversion — a silent security regression.Fix:
New module:
litellm/responses/litellm_completion_transformation/custom_tools.pyfunctiontools (grammar embedded in description so the model produces correctly-formatted output).function_callresponses back tocustom_tool_calloutput items (unwrapping the{"content": ...}JSON envelope).allowed_callersso the Anthropic adapter's caller allowlist is maintained.transformation.py— Integrates custom tool conversion into the Responses→ChatCompletions bridge. Emitscustom_tool_calloutput items with unwrappedinputstring (not JSONarguments) when the model returns afunction_callfor 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.streaming_iterator.py— Streaming path uses sharedbuild_tool_call_item_kwargs()to emit eitherfunction_callorcustom_tool_callitems depending on whether the call targets a custom tool. Custom tool names extracted from request tools at construction time. Same modern annotation cleanup.router.py— FixesStopAsyncIterationhandling in the streaming wrapper: when the inner generator is exhausted, falls back to the source iterator'scompleted_responseinstead of logging a spurious "no completed_response" warning.common_request_processing.py— Defensive fix for_apply_client_disconnect_metadatawhen the target dict isNone(happens when logging_obj metadata is missing during streaming disconnect).Types: Added
CustomToolCallOutputIteminlitellm/types/responses/main.pyand registered it in theResponsesAPIResponseallowed output items union.Tests: Coverage added in
test_custom_tool_call.py(custom→function→custom_tool_call round-trip,allowed_callerspreservation, edge cases),test_litellm_completion_responses.py(transformation integration), andtest_common_request_processing.py(client disconnect metadata fix).Files changed: 10 files, +1204 / −342