Skip to content

Fix duplicate/empty tool_use blocks in OpenAI->Claude streaming translation#1579

Open
lightsofapollo wants to merge 1 commit intorouter-for-me:mainfrom
lightsofapollo:fix/claude-tool-use-streaming
Open

Fix duplicate/empty tool_use blocks in OpenAI->Claude streaming translation#1579
lightsofapollo wants to merge 1 commit intorouter-for-me:mainfrom
lightsofapollo:fix/claude-tool-use-streaming

Conversation

@lightsofapollo
Copy link

Summary

This fixes a response-translation bug in the OpenAI->Claude path that could break tool execution in Claude clients.

Problem

Some OpenAI-compatible providers stream tool_calls in fragmented deltas:

  • function.name appears once, then appears again as empty in later chunks
  • IDs/arguments may arrive across multiple chunks

The previous translator could emit multiple content_block_start events for the same tool call, including an empty tool name, which can cause client-side tool execution failures.

Changes

  • Emit tool_use content_block_start exactly once per tool call in streaming mode.
  • Ignore empty repeated function.name deltas after start.
  • Canonicalize returned tool names to match request-declared tool names (case-insensitive), e.g. bash -> Bash.
  • Apply the same name canonicalization to non-stream responses.
  • Finalize tool-call blocks in deterministic index order.

Testing

  • Added streaming regression test:
    • verifies only one tool_use start is emitted
    • verifies no empty-name tool_use blocks
    • verifies canonical tool name output
  • Added non-stream regression test for canonical tool name mapping.
  • Ran full test suite: go test ./...

Context / Risk

  • This was validated primarily against Fireworks in an Anthropic-compatible proxy setup.
  • There is potential for regressions with other OpenAI-compatible providers that emit tool call deltas differently.
  • Opening this PR to get maintainer feedback on whether this behavior is the right default for broader provider compatibility.

…ders

- Emit exactly one tool_use content block per tool call in streaming mode

- Canonicalize returned tool names to match declared request tool names (case-insensitive)

- Add regression tests for duplicate/empty-name tool_use starts and non-stream name canonicalization
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @lightsofapollo, 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 critical bug in the OpenAI to Claude response translation path that could lead to broken tool execution in Claude clients. The issue stemmed from OpenAI-compatible providers streaming tool_calls in fragmented deltas, causing the previous translator to emit multiple or empty content_block_start events. The changes ensure robust and consistent handling of tool call deltas, preventing client-side failures and improving overall compatibility with various OpenAI-compatible providers.

Highlights

  • Tool Call Streaming Fix: Ensured that tool_use content_block_start events are emitted exactly once per tool call during streaming translation from OpenAI to Claude, preventing duplicate or empty blocks.
  • Empty Name Handling: Implemented logic to ignore empty repeated function.name deltas that can occur in fragmented OpenAI streams, which previously caused client-side tool execution failures.
  • Tool Name Canonicalization: Added functionality to canonicalize tool names to match the case-insensitive names declared in the original request, improving consistency and compatibility.
  • Non-Stream Response Consistency: Applied the same tool name canonicalization to non-streaming responses for uniform behavior across different response types.
  • Deterministic Tool Block Finalization: Modified the process to finalize tool-call blocks in a deterministic index order, enhancing reliability.
  • Comprehensive Testing: Introduced new streaming and non-streaming regression tests to validate the fixes for single tool_use start emission, absence of empty names, and correct tool name canonicalization.

🧠 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.

Changelog
  • internal/translator/openai/claude/openai_claude_response.go
    • Imported the sort package for ordering tool call indexes.
    • Added CanonicalToolNameByLower map to ConvertOpenAIResponseToAnthropicParams to store canonical tool names.
    • Introduced a Started boolean field to ToolCallAccumulator to track if a tool call's content_block_start has been emitted.
    • Initialized the CanonicalToolNameByLower map by parsing the original request's tools.
    • Updated convertOpenAINonStreamingToAnthropic to accept and utilize the canonicalToolNameByLower map.
    • Modified convertOpenAIStreamingChunkToAnthropic to ensure tool_use content_block_start is emitted only once per tool call, canonicalize tool names, and assign an ID if missing.
    • Adjusted convertOpenAIStreamingChunkToAnthropic and convertOpenAIDoneToAnthropic to iterate over sorted tool call indexes and to emit content_block_start for any tool calls that haven't been started yet.
    • Updated ConvertOpenAIResponseToClaudeNonStream to build and apply tool name canonicalization.
    • Added helper functions: sortedToolCallIndexes for deterministic iteration, buildCanonicalToolNameByLower for creating the canonical name map, and canonicalizeToolName for applying the canonicalization.
  • internal/translator/openai/claude/openai_claude_response_test.go
    • Added a new test file to cover the new translation logic.
    • Implemented TestConvertOpenAIResponseToClaude_StreamToolStartEmittedOnceAndNameCanonicalized to verify correct streaming behavior, including single tool_use start emission and canonical name usage.
    • Added TestConvertOpenAIResponseToClaudeNonStream_CanonicalizesToolName to confirm that non-streaming responses correctly canonicalize tool names.
Activity
  • The pull request was opened to fix a response-translation bug in the OpenAI->Claude path that could break tool execution.
  • The author validated the changes primarily against Fireworks in an Anthropic-compatible proxy setup.
  • New streaming and non-streaming regression tests were added to ensure the fixes work as expected.
  • The full test suite was run (go test ./...) to confirm no regressions were introduced.
  • The author is seeking maintainer feedback on whether the implemented behavior is the right default for broader provider compatibility, indicating an initial review phase.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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
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 effectively fixes a bug in the OpenAI to Claude streaming translation for tool calls. The changes correctly handle fragmented deltas by ensuring tool_use blocks are started only once and by canonicalizing tool names. The addition of regression tests for both streaming and non-streaming modes is a great way to ensure the fix is robust. I have one suggestion to refactor some duplicated code for better maintainability.

Comment on lines +365 to +379
if !accumulator.Started {
if strings.TrimSpace(accumulator.Name) == "" {
delete(param.ToolCallBlockIndexes, index)
continue
}
if accumulator.ID == "" {
accumulator.ID = fmt.Sprintf("call_%d", index)
}
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID)
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
accumulator.Started = true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This block of logic for ensuring a tool_use content_block_start is emitted is duplicated from lines 285-299 in convertOpenAIStreamingChunkToAnthropic. To improve maintainability and reduce redundancy, consider extracting this logic into a helper function that can be called from both locations.

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.

1 participant