[fix]: accept object-valued tool_search_call arguments on Responses streaming path - #4644
Conversation
📝 WalkthroughWalkthroughAdds a custom ChangesResponses API tool-call arguments normalization
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
…treaming path OpenAI's Responses API normally serializes function_call `arguments` as a JSON string, but some models (e.g. gpt-5.x) and the native websocket /responses transport emit them as a JSON object. The embedded ResponsesToolMessage.Arguments field is a *string, so an object value made the stream chunk decode fail with "Mismatch type string with value object", silently dropping the tool call mid-stream and hanging streaming clients until their own idle watchdog fired. ResponsesMessage now has a custom UnmarshalJSON that shadows `arguments` as raw JSON, decodes the rest of the item normally, then stores the canonical stringified-JSON form (object preserved as its raw JSON text). Affected packages: - core/schemas/responses.go - ResponsesMessage.UnmarshalJSON + helper - core/schemas/responses_test.go - regression tests (string + object forms, streamed item) - core/changelog.md Co-authored-by: Cursor <cursoragent@cursor.com>
9056c30 to
e488546
Compare
…treaming path (#4644) OpenAI's Responses API normally serializes function_call `arguments` as a JSON string, but some models (e.g. gpt-5.x) and the native websocket /responses transport emit them as a JSON object. The embedded ResponsesToolMessage.Arguments field is a *string, so an object value made the stream chunk decode fail with "Mismatch type string with value object", silently dropping the tool call mid-stream and hanging streaming clients until their own idle watchdog fired. ResponsesMessage now has a custom UnmarshalJSON that shadows `arguments` as raw JSON, decodes the rest of the item normally, then stores the canonical stringified-JSON form (object preserved as its raw JSON text). Affected packages: - core/schemas/responses.go - ResponsesMessage.UnmarshalJSON + helper - core/schemas/responses_test.go - regression tests (string + object forms, streamed item) - core/changelog.md Co-authored-by: Cursor <cursoragent@cursor.com>
Description
When a model streams tool calls over the OpenAI Responses API,
tool_search_callitems serialize theirargumentsas a JSON object instead of the JSON string thatfunction_callitems use.tool_search_callitems are emitted whenever the request enables OpenAI'stool_searchtool (deferred tool discovery) — which Codex (codex_cli_rs) enables by default. The streamed item (response.output_item.added/.done) decodes intoResponsesMessage, whose embeddedResponsesToolMessage.Argumentsis a*string. An object value therefore fails to decode:HandleOpenAIResponsesStreaminglogs this atwarnandcontinues, so the item is silently dropped mid-stream. Strict streaming clients never receive it and hang on a half-open stream until their own idle watchdog fires — no error is surfaced.This reproduces on the latest release (
maximhq/bifrost:v1.5.16).Root cause
function_callitems serializeargumentsas a string — these decode fine.tool_search_callitems serializeargumentsas an object:{}whilein_progress, and e.g.{"query":"...","limit":10}whencompleted.tool_searchtool. Codex enables this by default for deferred tool discovery.Runnable reproduction
From a local
maximhq/bifrostcheckout, run this command. It imports the local checkout and feeds three Responses stream frames through the same parser path Bifrost uses (schemas.UnmarshalintoBifrostResponsesStreamResponse).Before this fix
Run the repro against
dev/v1.5.16and thetool_search_callframes fail with the same parse error seen in production:After this fix
Run the same repro on this PR branch and all frames parse. Object-valued
argumentsare preserved as stringified JSON:Optional live OpenAI shape check
The deterministic repro above does not require credentials. To verify the upstream shape directly, send a Responses request that includes a
tool_searchtool. OpenAI streamstool_search_callframes like this:That object-valued
argumentsfield is what the old parser rejected.Type of Change
Affected Packages
core/schemas/responses.gocore/schemas/responses_test.gocore/changelog.mdChanges Made
ResponsesMessage.UnmarshalJSONthat shadowsargumentsas raw JSON, decodes the rest of the item normally, then normalizesargumentsto the canonical stringified-JSON form (a JSON object is preserved as its raw JSON text; a JSON string is kept as-is). Every downstream consumer that reads*Argumentsas stringified JSON keeps working unchanged.responsesToolArgumentsToStringhelper.{}, an object inside a streamedresponse.output_item.doneevent, and realtool_search_callframes captured fromapi.openai.com.Testing
go test ./schemas/ -run TestResponsesMessageToolCallArguments -vChecklist
[type]: descriptioncore/changelog.md)Related Issues
Relates to the broader streaming tool-call serialization issues (e.g. #3443, #3475); this one is the OpenAI Responses path receiving
tool_search_call.argumentsas an object rather than a string.Made with Cursor