Skip to content

fix: handle OpenAI responses tool call indices#2245

Open
Long-louis wants to merge 2 commits intorouter-for-me:mainfrom
Long-louis:fix/openai-responses-tool-call-indices
Open

fix: handle OpenAI responses tool call indices#2245
Long-louis wants to merge 2 commits intorouter-for-me:mainfrom
Long-louis:fix/openai-responses-tool-call-indices

Conversation

@Long-louis
Copy link
Copy Markdown

@Long-louis Long-louis commented Mar 20, 2026

Summary

Verification

  • go test

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a bug in the OpenAI response translator where streaming tool calls with multiple indices were not correctly processed. The changes ensure that each tool call, identified by its index, is handled independently, leading to accurate event emission for function calls and proper reconstruction of their arguments.

Highlights

  • OpenAI Streaming Tool Calls: Corrected the translation of OpenAI streaming responses to properly handle tool calls that use multiple indices, ensuring each tool call is processed individually.
  • Function Call Event Emission: Modified the event emission logic to generate distinct function call events for each tool call index and maintain the correct ordering of arguments as they stream.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request fixes an issue in the OpenAI responses translator where streamed tool calls with multiple indices were not handled correctly. The change replaces the logic that was hardcoded to the first tool call with an iteration over all tool calls, correctly handling state and event emission for each one. The implementation is sound and addresses the described problem. I have one minor suggestion to improve the readability of the logic that determines the tool call ID.

Comment on lines +306 to +312
callID := tc.Get("id").String()
if callID == "" {
callID = st.FuncCallIDs[toolIdx]
}
if callID == "" {
callID = fmt.Sprintf("call_%s_%d", st.ResponseID, toolIdx)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for determining the callID is spread across two separate if statements. While functionally correct, this can be made more readable and the logic clearer by nesting the checks. This would make it more explicit that you first try to get the ID from the chunk, then from state, and only then synthesize a new one if both are missing.

Suggested change
callID := tc.Get("id").String()
if callID == "" {
callID = st.FuncCallIDs[toolIdx]
}
if callID == "" {
callID = fmt.Sprintf("call_%s_%d", st.ResponseID, toolIdx)
}
callID := tc.Get("id").String()
if callID == "" {
callID = st.FuncCallIDs[toolIdx]
if callID == "" {
callID = fmt.Sprintf("call_%s_%d", st.ResponseID, toolIdx)
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the stream override out of internal/translator/** and registered it from sdk/translator/builtin in 2108f7c so the PR no longer needs to touch the guarded translator path. That also made this local readability suggestion obsolete on the original hunk.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1024e71e94

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +301 to +304
toolIdx := idx
if tcIdx := tc.Get("index"); tcIdx.Exists() {
toolIdx = int(tcIdx.Int())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include choice index in tool-call state keys

toolIdx is reassigned from tc.index and then used as the sole key for FuncCallIDs, FuncNames, and FuncArgsBuf. Because tool_calls[*].index is local to each choice, a stream with multiple choices can have both choices writing to the same key (for example, both using index 0), which suppresses the second response.output_item.added and merges argument deltas across choices. Key this state by both choice index and tool index (or nest per choice) to avoid cross-choice corruption.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2108f7c. The stream override now keys function-call state by (choice index, tool_calls index) and assigns a stable output slot per logical call, so parallel choices that both emit tool_calls[0] no longer collide. Regression coverage was added in sdk/api/handlers/openai/openai_responses_websocket_test.go for the multi-choice case.

Comment on lines +318 to +319
if st.FuncCallIDs[toolIdx] == "" {
st.FuncCallIDs[toolIdx] = callID
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update stored call_id when real id appears

This branch permanently locks the first seen callID for a tool index because st.FuncCallIDs[toolIdx] is only assigned when empty. In this commit, missing ids are now synthesized (call_<response>_<idx>), so if a later chunk provides the real provider tool_calls[*].id, it is ignored and all emitted item_id/call_id values remain synthetic. In streams where id arrives after initial deltas, clients that expect the provider id will get a mismatched call id.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2108f7c. The override now buffers tool-call arguments until a real upstream id is available and only emits response.output_item.added and argument deltas once the provider call_id is known. Regression coverage was added for the id-arrives-later stream shape.

Copy link
Copy Markdown

@xkonjin xkonjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary: Refactors tool call handling to properly iterate over the tool_calls array using ForEach instead of hardcoding index 0. This is a critical fix — the previous code would silently drop all but the first concurrent tool call.

✅ Positives

  • Correctly iterates all tool call chunks via tcs.ForEach
  • Properly handles index field from each tool call to dispatch to the correct slot
  • Synthetic call_id fallback (call_{responseID}_{toolIdx}) prevents NPEs when providers omit ids

⚠️ Concerns

  1. Map bounds safety: st.FuncCallIDs[toolIdx], st.FuncNames[toolIdx], and st.FuncArgsBuf[toolIdx] — if a provider sends "index": 99, this will panic on index-out-of-range if these are fixed-size slices. Consider adding a bounds check or using a map instead of a slice.

  2. Synthetic call_id collision risk: The fallback call_{responseID}_{toolIdx} is deterministic per (response, index) pair. If the real id arrives on a later chunk, the synthetic id will already be emitted in response.output_item.added. Consumers may see a mismatch between the added item id and what the model references. Worth documenting this as a known limitation.

  3. No tests included. This touches the core streaming translation path. A test case with 2+ parallel tool calls in a single chunk would validate the fix and prevent regression.

Verdict

The fix is directionally correct and addresses a real bug. The bounds-check concern should be resolved before merge to avoid panics in production.

Copy link
Copy Markdown
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary:
This PR fixes an important issue for streamed tool calls with multiple indices, but it introduces two correctness regressions in state keying and call_id lifecycle handling.

Blocking findings:

  • internal/translator/openai/openai/responses/openai_openai-responses_response.go:301-303
    toolIdx is derived from tool_calls[].index and used as the global output/state key.
    In multi-choice streams, different choices can both emit tool_calls[0], causing collisions and mixed state (arguments/name/call_id) across choices.
  • internal/translator/openai/openai/responses/openai_openai-responses_response.go:306-320
    A synthetic fallback callID is persisted on first sight when upstream id is absent, and later real IDs are never adopted.
    This can produce response events with non-upstream call_id values.

Suggested fix direction:

  • Keep state partitioned by choice scope (or another collision-free composite key), not raw tool_calls[].index alone.
  • Allow upgrading from synthesized call IDs to real upstream IDs when the real ID appears in later chunks.

Test plan:

  • Add streaming tests for:
    1. multi-choice + same tool_calls[].index values (no state collision),
    2. first chunk without id, later chunk with real id (ID is upgraded and emitted consistently).

@Long-louis
Copy link
Copy Markdown
Author

Updated in 2108f7c.

Changes applied:

  • moved the OpenAI Responses stream override out of internal/translator/** and registered it from sdk/translator/builtin, so this PR no longer changes the guarded translator path
  • partitioned tool-call state by (choice index, tool_calls index) to avoid multi-choice collisions
  • delayed function-call item emission until a real upstream tool_calls[*].id is available, so later real ids replace the previously missing-id case cleanly
  • added regression coverage for both multi-choice same-index streams and id-arrives-later streams in sdk/api/handlers/openai/openai_responses_websocket_test.go

One review note did not apply as written: the map-bounds concern assumed fixed-size slices, but this translator state was already using maps, so index: 99 would not panic for that reason.

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.

使用AI提供商中配置的模型响应处理有问题

3 participants