fix: preserve tool_search Responses API wire shape - #4711
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds ChangesTool search response round-trip
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@core/schemas/responses.go`:
- Around line 939-942: Add ToolSearchOutputTools to both ResponsesMessage
deep-copy paths: update deepCopyResponsesMessage in
framework/streaming/responses.go and DeepCopyResponsesMessage in
core/schemas/utils.go so the new json.RawMessage field is copied alongside
Author and Recipient. Use the same nil-check and raw-byte append pattern already
used for other RawMessage fields, ensuring the ToolSearchOutputTools payload
survives streaming accumulation and plugin processing without being dropped.
- Line 1222: The ResponsesToolMessage deep-copy helper is dropping the Namespace
and Execution fields, so update the field-by-field copy logic in the
ResponsesToolMessage clone routine in core/schemas/utils.go to include both
*string values. Mirror the existing deref-and-copy handling used for CallID,
Name, Arguments, and Error, and make sure the new branches copy Namespace and
Execution alongside the other optional string fields so tool execution metadata
is preserved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3427bfb6-d2d8-475d-810c-baeb32f8fb3a
📒 Files selected for processing (2)
core/schemas/responses.gocore/schemas/responses_test.go
Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
framework/streaming/responses.go (1)
238-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace reflection-based field copies with direct assignment.
copyRawMessageFieldByName/copyOptionalStringFieldByNameusereflect+ string field names to do what is plain field copying. The companion implementation incore/schemas/utils.go(DeepCopyResponsesMessage) copies the very sameToolSearchOutputTools,Namespace, andExecutionfields with direct assignment — so we now have two divergent approaches for identical logic. Concerns with the reflection variant here:
- Silent failure on rename: both helpers
returnquietly whenFieldByNameis invalid. IfToolSearchOutputToolsorExecutionis ever renamed, the copy is silently dropped and the tool-search round-trip bug this PR fixes regresses with no compile error.- Hot path cost: this runs per
ResponsesMessageduring stream accumulation;reflect.ValueOf/FieldByName/Setadd avoidable allocations and lookups versus a direct copy.- Local inconsistency:
Namespaceis copied inline (Lines 281–284) whileExecutiongoes through reflection (Line 286).Direct assignment matches the rest of this function and
core/schemas/utils.go, and is compile-time safe.♻️ Proposed direct-copy refactor
- copyRawMessageFieldByName(©, original, "ToolSearchOutputTools") + if original.ToolSearchOutputTools != nil { + copy.ToolSearchOutputTools = append(json.RawMessage(nil), original.ToolSearchOutputTools...) + }- copyOptionalStringFieldByName(copy.ResponsesToolMessage, original.ResponsesToolMessage, "Execution") + if original.ResponsesToolMessage.Execution != nil { + copyExecution := *original.ResponsesToolMessage.Execution + copy.ResponsesToolMessage.Execution = ©Execution + }Then drop the now-unused
copyRawMessageFieldByName/copyOptionalStringFieldByNamehelpers (Lines 460–485) and thereflectimport.Also applies to: 460-485
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@framework/streaming/responses.go` at line 238, Replace the reflection-based field copying in ResponsesMessage aggregation with direct assignment: in the copy logic around ToolSearchOutputTools, Execution, and Namespace, assign the fields directly instead of calling copyRawMessageFieldByName/copyOptionalStringFieldByName. Then remove the now-unused helper functions from responses.go and drop the reflect import, keeping the implementation aligned with DeepCopyResponsesMessage in core/schemas/utils.go and compile-time safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@framework/streaming/responses.go`:
- Line 238: Replace the reflection-based field copying in ResponsesMessage
aggregation with direct assignment: in the copy logic around
ToolSearchOutputTools, Execution, and Namespace, assign the fields directly
instead of calling copyRawMessageFieldByName/copyOptionalStringFieldByName. Then
remove the now-unused helper functions from responses.go and drop the reflect
import, keeping the implementation aligned with DeepCopyResponsesMessage in
core/schemas/utils.go and compile-time safe.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3660f047-eb55-43b0-8d83-e8e95946d046
📒 Files selected for processing (4)
core/schemas/responses.gocore/schemas/responses_test.gocore/schemas/utils.goframework/streaming/responses.go
🚧 Files skipped from review as they are similar to previous changes (2)
- core/schemas/responses_test.go
- core/schemas/responses.go
Co-authored-by: Cursor <cursoragent@cursor.com>
The merge-base changed after approval.
|
|
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
44564de to
493bff0
Compare
Signed-off-by: Akshay Deo <akshay@akshaydeo.com>
6c58c43
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* fix domains * docs update * [fix]: preserve tool_search_call argument wire shape * [fix]: preserve tool_search execution flag and tool_search_output tools Two further Responses-API wire-shape losses broke the codex tool_search round-trip through bifrost: - `execution: "client"` was dropped from tool_search_call items, so codex never recognized the call as client-dispatchable and silently ended the turn. Add Execution to ResponsesToolMessage. - `tool_search_output` discovered tools are ResponsesTool-shaped (namespace/function, discriminated by `type`), not mcp_list_tools-shaped. The embedded ResponsesMCPListTools decode dropped `type` and nesting, causing OpenAI to 400 with "Missing required parameter input[].tools[].type". Round-trip the tools array verbatim as raw JSON. Adds regression tests for both. Verified end-to-end with codex-acp against a local bifrost build: the full tool_search -> discover -> call -> result loop now completes instead of silently ending the turn. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: trim verbose comments on tool_search wire-shape handling Co-authored-by: Cursor <cursoragent@cursor.com> * fix: preserve tool_search metadata in response copies Keep tool_search_output raw tools, namespace, and execution metadata intact across the response deep-copy paths used by schema helpers and streaming accumulation. Also preserve object-shaped arguments on tool_search_output serialization and document the raw tools invariant for programmatic callers. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: explain response copy compatibility shim Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Akshay Deo <akshay@akshaydeo.com> Co-authored-by: akshaydeo <akshay@akshaydeo.com> Co-authored-by: Cursor <cursoragent@cursor.com>
What this fixes
When a client enables the
tool_searchtool (Codex does this for deferred tool discovery), Bifrost mangles three parts of the Responses API wire shape as it re-serializes streamed items. Each one independently breaks the round-trip, and the visible result is that the model's turn just ends: no tool call, no output, a cleanend_turn. From the outside it looks like the agent "randomly stopped."It only happens through Bifrost — talking to OpenAI directly works — because the corruption is introduced in Bifrost's serialization layer.
Root cause
There are three separate issues, all in how
core/schemas/responses.godecodes and re-encodes Responses items:tool_search_call.argumentsgets stringified.OpenAI sends these arguments as a JSON object (
{}while in progress,{"query":"..."}when done), but Bifrost re-emitted them as a JSON string. Codex sees the wrong type and aborts the turn (see tool_search aborts with 'unsupported payload' when using non-native API endpoints (Ollama/Bifrost) openai/codex#20574).execution: "client"is dropped fromtool_search_call.This flag tells the client the call is its to dispatch.
ResponsesToolMessagehad no field for it, so it was silently discarded and Codex never dispatched the search.typeis dropped from the tools insidetool_search_output.The discovered tools are
ResponsesTool-shaped (anamespacewrappingfunctionentries, discriminated bytype). They were being decoded throughResponsesMCPListTools, which doesn't modeltypeor the nesting, so the follow-up request to OpenAI failed withMissing required parameter: 'input[].tools[].type'.The fix
MarshalJSONkeepstool_search_call.argumentsas a JSON object while leavingfunction_call.argumentsas a string.Executionfield toResponsesToolMessagesoexecutionsurvives the round-trip.tool_search_outputas its own item type and pass itstoolsarray through verbatim as raw JSON. Themcp_list_toolspath is untouched.How to reproduce / verify
The schema tests cover each issue directly:
TestResponsesMessageMarshalsToolSearchArgumentsAsObject— arguments stay an object (issue 1)TestResponsesMessagePreservesToolSearchExecution—executionsurvives (issue 2)TestResponsesMessageRoundTripsToolSearchOutputTools— tooltypesurvives (issue 3)End-to-end, I ran the real Codex ACP client against two local Bifrost builds (before/after this PR), pointed at a stub MCP server with enough tools to trigger
tool_search:tool_searchdispatchedend_turn, empty outputBefore: the model emits the tool_search step, gets the broken shape back, and ends the turn with nothing. After: it discovers the tool, calls it, gets the result, and answers.
Question for maintainers
All three bugs come from the same root: Bifrost fully decodes provider responses into its typed schema and then re-marshals them, so any field the schema doesn't model is dropped. These three are the ones I hit, but the pattern means there are likely others lurking for any newer/less-common Responses item.
Is there appetite for a less invasive approach on the response path — stream the provider bytes through to the client untouched, and parse a copy on the side for logging/governance/cost rather than reconstructing the payload from a lossy typed model? That would make Bifrost behave like a true proxy and eliminate this whole class of wire-shape bugs, instead of us patching each dropped field as we find it. Happy to take a stab if you think it's worth it, or if there's a reason the round-trip re-marshal is required I'd like to understand it.
Type of change
Affected areas
Breaking changes
Related issues
openai/codex#20574
Checklist