Skip to content

fix: gemini openai through signature compatibility - #4810

Merged
Pratham-Mishra04 merged 1 commit into
devfrom
06-30-fix_gemini_openai_through_signature_compatibility
Jul 2, 2026
Merged

fix: gemini openai through signature compatibility#4810
Pratham-Mishra04 merged 1 commit into
devfrom
06-30-fix_gemini_openai_through_signature_compatibility

Conversation

@TejasGhatte

@TejasGhatte TejasGhatte commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a conversation switches from Gemini to OpenAI, Gemini embeds its thoughtSignature into tool call IDs using the format <baseID>_ts_<signature>. OpenAI rejects call_id values exceeding 64 characters, causing cross-provider conversation continuations to fail. This PR strips the embedded signature before sending IDs to OpenAI, while leaving the caller's input untouched so a subsequent Gemini turn can still recover the signature.

Changes

  • Introduced ThoughtSignatureSeparator (_ts_) as a shared constant in core/providers/utils and a StripThoughtSignature helper that returns the base ID by cutting at the separator.
  • The Gemini thoughtSignatureSeparator constant now references the shared ThoughtSignatureSeparator instead of a local string literal.
  • In the OpenAI chat converter (ConvertBifrostMessagesToOpenAIMessages), assistant tool call IDs and tool result tool_call_id values are stripped of any embedded signature. Structs are cloned before modification to avoid mutating the caller's input.
  • In the OpenAI Responses converter (ToOpenAIResponsesRequest), ResponsesToolMessage.CallID is stripped the same way, also via a shallow clone.
  • When a Gemini assistant message part has no ThoughtSignature, it is now assigned skipThoughtSignatureValidator to prevent downstream validation errors during cross-provider routing.
  • Tests cover StripThoughtSignature edge cases, the chat converter stripping behavior (including immutability of the original input), and the Responses converter stripping behavior.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./core/providers/openai/... ./core/providers/gemini/... ./core/providers/utils/...

The new tests verify:

  • StripThoughtSignature correctly handles IDs with and without embedded signatures.
  • The OpenAI chat converter strips signatures from assistant tool call IDs and tool result IDs without mutating the original request.
  • The OpenAI Responses converter strips signatures from call_id fields without mutating the original request.

Breaking changes

  • No

Security considerations

No auth, secrets, or PII implications. The stripping is deterministic and only affects tool call ID formatting on the wire.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d59d294-a0ac-4d4c-96c7-80b201be0d03

📥 Commits

Reviewing files that changed from the base of the PR and between 1b6a465 and afebdf9.

📒 Files selected for processing (8)
  • core/providers/gemini/responses.go
  • core/providers/gemini/types.go
  • core/providers/openai/chat_test.go
  • core/providers/openai/responses.go
  • core/providers/openai/responses_test.go
  • core/providers/openai/utils.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go
📝 Walkthrough

Walkthrough

Adds shared stripping logic for thought-signature suffixes, applies it to OpenAI chat and responses request conversion, centralizes the Gemini separator constant, and ensures Gemini function-call parts always carry a non-nil thought signature field.

Changes

Thought Signature Stripping

Layer / File(s) Summary
Shared ThoughtSignatureSeparator and stripping utility
core/providers/utils/utils.go, core/providers/utils/utils_test.go
Adds ThoughtSignatureSeparator and StripThoughtSignature, with tests covering embedded signatures, separator-only input, and empty input.
Gemini separator centralization and ThoughtSignature nil-guard
core/providers/gemini/types.go, core/providers/gemini/responses.go
Reuses the shared separator constant in Gemini types and assigns a sentinel placeholder when a function-call part still has a nil ThoughtSignature.
OpenAI chat conversion stripping
core/providers/openai/utils.go, core/providers/openai/chat_test.go
Adds max-length-based stripping for chat tool-call IDs and assistant tool-call IDs, with tests for stripping behavior and preservation of short IDs containing the separator substring.
OpenAI responses conversion stripping
core/providers/openai/responses.go, core/providers/openai/responses_test.go
Normalizes ResponsesToolMessage.CallID by stripping embedded signatures when over the length limit, cloning the message before updating it, with a test covering the converted IDs and input immutability.

Sequence Diagram(s)

sequenceDiagram
  participant Bifrost as Bifrost request
  participant OpenAIChat as ConvertBifrostMessagesToOpenAIMessages
  participant Utils as core/providers/utils
  participant OpenAI as OpenAI message

  Bifrost->>OpenAIChat: assistant/tool call IDs
  OpenAIChat->>Utils: StripThoughtSignature(callID)
  Utils-->>OpenAIChat: base ID
  OpenAIChat->>OpenAI: emit sanitized ToolCallID / ToolCalls
Loading

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: akshaydeo, danpiths

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main fix: Gemini/OpenAI signature compatibility for tool call IDs.
Description check ✅ Passed The description covers the required sections and includes summary, changes, testing, breaking changes, security, and checklist details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-30-fix_gemini_openai_through_signature_compatibility

Comment @coderabbitai help to get the list of available commands.

@TejasGhatte
TejasGhatte marked this pull request as ready for review June 30, 2026 18:12
@CLAassistant

CLAassistant commented Jun 30, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

TejasGhatte commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai
coderabbitai Bot requested review from akshaydeo and danpiths June 30, 2026 18:13
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the stripping is deterministic, length-guarded, and clone-first, so no mutation of caller state and no false positives on short IDs.

Both the chat and Responses converters strip only when the ID exceeds OpenAI's 64-character limit and the separator is present, clone affected structs before modifying, and produce identical stripped values for matched call/output pairs. The shared constant eliminates the prior string duplication between Gemini and the new utility. The skipThoughtSignatureValidator sentinel in the Gemini Responses path follows the established pattern from the chat path. Tests cover the primary correctness properties including immutability of the original input.

No files require special attention.

Important Files Changed

Filename Overview
core/providers/utils/utils.go Adds ThoughtSignatureSeparator constant and StripThoughtSignature helper; both are pure string utilities with no side effects and are well-tested.
core/providers/openai/utils.go Adds length-guarded, clone-first stripping of embedded thought signatures from assistant tool call IDs and tool result IDs in the shared chat converter; MaxToolCallIDLength constant added.
core/providers/openai/responses.go Strips over-long embedded thought signatures from ResponsesToolMessage.CallID before forwarding to OpenAI, covering both function_call and function_call_output message types with a shallow clone to preserve caller immutability.
core/providers/gemini/responses.go Sets skipThoughtSignatureValidator sentinel on function-call parts that have no thought signature, following the same pattern already used in the chat path; prevents validator errors on cross-provider conversations.
core/providers/gemini/types.go Replaces local thoughtSignatureSeparator string literal with the new shared providerUtils.ThoughtSignatureSeparator constant; purely mechanical change.
core/providers/utils/utils_test.go Table-driven tests for StripThoughtSignature cover no-separator, embedded signature, separator-only, and empty-string cases.
core/providers/openai/chat_test.go Two new tests verify over-long embedded IDs are stripped from both assistant and tool-result messages, short IDs are preserved, and the original input is not mutated.
core/providers/openai/responses_test.go New test verifies Responses converter strips over-long embedded call IDs from both function_call and function_call_output items and leaves caller input untouched.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant Bifrost
    participant Gemini
    participant OpenAI

    Caller->>Bifrost: Send request (Turn 1, target: Gemini)
    Bifrost->>Gemini: Forward request
    Gemini-->>Bifrost: "Response with tool call ID = baseID_ts_signature"
    Bifrost-->>Caller: BifrostResponse (full embedded ID preserved)

    Caller->>Bifrost: Continue conversation (Turn 2, target: OpenAI)
    Note over Bifrost: len(id) > 64 → StripThoughtSignature()
    Note over Bifrost: Clone struct before modifying
    Bifrost->>OpenAI: Stripped IDs (≤64 chars), caller input left intact
    OpenAI-->>Bifrost: Response
    Bifrost-->>Caller: BifrostResponse (caller full IDs still in input)

    Caller->>Bifrost: Next turn back to Gemini (Turn 3)
    Note over Bifrost: Gemini converter extracts thoughtSignature
    Note over Bifrost: Sets skipThoughtSignatureValidator if none found
    Bifrost->>Gemini: Forward with recovered signature
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant Bifrost
    participant Gemini
    participant OpenAI

    Caller->>Bifrost: Send request (Turn 1, target: Gemini)
    Bifrost->>Gemini: Forward request
    Gemini-->>Bifrost: "Response with tool call ID = baseID_ts_signature"
    Bifrost-->>Caller: BifrostResponse (full embedded ID preserved)

    Caller->>Bifrost: Continue conversation (Turn 2, target: OpenAI)
    Note over Bifrost: len(id) > 64 → StripThoughtSignature()
    Note over Bifrost: Clone struct before modifying
    Bifrost->>OpenAI: Stripped IDs (≤64 chars), caller input left intact
    OpenAI-->>Bifrost: Response
    Bifrost-->>Caller: BifrostResponse (caller full IDs still in input)

    Caller->>Bifrost: Next turn back to Gemini (Turn 3)
    Note over Bifrost: Gemini converter extracts thoughtSignature
    Note over Bifrost: Sets skipThoughtSignatureValidator if none found
    Bifrost->>Gemini: Forward with recovered signature
Loading

Reviews (7): Last reviewed commit: "fix: gemini openai through signature com..." | Re-trigger Greptile

Comment thread core/providers/openai/responses_test.go
Comment thread core/providers/openai/utils.go Outdated
Comment thread core/providers/openai/utils.go
@TejasGhatte
TejasGhatte changed the base branch from 06-30-chore_test_fixes to graphite-base/4810 July 1, 2026 08:07
@TejasGhatte
TejasGhatte force-pushed the 06-30-fix_gemini_openai_through_signature_compatibility branch from e175200 to 4e6b674 Compare July 1, 2026 08:07
@TejasGhatte
TejasGhatte force-pushed the graphite-base/4810 branch from 9ac2c54 to af0b891 Compare July 1, 2026 08:07
@TejasGhatte
TejasGhatte changed the base branch from graphite-base/4810 to dev July 1, 2026 08:07

@coderabbitai coderabbitai Bot left a comment

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.

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/providers/openai/utils.go`:
- Around line 44-74: Only strip verified Gemini thought signatures by updating
the shared StripThoughtSignature helper to require a decodable base64url payload
after the ThoughtSignatureSeparator instead of stripping any ID containing the
separator. Then ensure both the ChatToolMessage/ChatAssistantMessage handling in
openaiMessages conversion and the responses converter continue to call that
validated helper so non-Gemini IDs like report_ts_backup are left unchanged.

In `@core/providers/utils/utils.go`:
- Around line 47-51: StripThoughtSignature is too aggressive because it removes
any callID containing ThoughtSignatureSeparator, which collapses raw IDs like
search_ts_a and search_ts_b into the same value. Update this helper to strip
only positively identified Gemini-added thought signatures, not arbitrary IDs
that merely contain "_ts_", and make sure the OpenAI converters that rely on
StripThoughtSignature continue to preserve distinct upstream call IDs. Use the
existing ThoughtSignatureSeparator and StripThoughtSignature symbols to locate
the change.
🪄 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: 5fcb619d-47a1-4be5-b829-2e6dbdc5c1f3

📥 Commits

Reviewing files that changed from the base of the PR and between af0b891 and 4e6b674.

📒 Files selected for processing (8)
  • core/providers/gemini/responses.go
  • core/providers/gemini/types.go
  • core/providers/openai/chat_test.go
  • core/providers/openai/responses.go
  • core/providers/openai/responses_test.go
  • core/providers/openai/utils.go
  • core/providers/utils/utils.go
  • core/providers/utils/utils_test.go

Comment thread core/providers/openai/utils.go
Comment thread core/providers/utils/utils.go
@akshaydeo
akshaydeo requested a review from a team as a code owner July 1, 2026 12:24
@TejasGhatte
TejasGhatte force-pushed the 06-30-fix_gemini_openai_through_signature_compatibility branch from 4e6b674 to a7d71e7 Compare July 2, 2026 03:53
@TejasGhatte
TejasGhatte changed the base branch from dev to graphite-base/4810 July 2, 2026 04:02
@TejasGhatte
TejasGhatte force-pushed the 06-30-fix_gemini_openai_through_signature_compatibility branch from a7d71e7 to 0069e87 Compare July 2, 2026 04:02
@TejasGhatte
TejasGhatte changed the base branch from graphite-base/4810 to 07-01-fix_cancelled_state_in_logs July 2, 2026 04:03
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 2, 2026
@TejasGhatte
TejasGhatte force-pushed the 07-01-fix_cancelled_state_in_logs branch from ee2940f to f9dae1e Compare July 2, 2026 07:32
@TejasGhatte
TejasGhatte force-pushed the 06-30-fix_gemini_openai_through_signature_compatibility branch from 0069e87 to 1b6a465 Compare July 2, 2026 07:32

Pratham-Mishra04 commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Merge activity

  • Jul 2, 1:44 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 2, 1:53 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 2, 1:54 PM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from 07-01-fix_cancelled_state_in_logs to graphite-base/4810 July 2, 2026 13:49
@Pratham-Mishra04
Pratham-Mishra04 changed the base branch from graphite-base/4810 to dev July 2, 2026 13:52
@Pratham-Mishra04
Pratham-Mishra04 dismissed coderabbitai[bot]’s stale review July 2, 2026 13:52

The base branch was changed.

@Pratham-Mishra04
Pratham-Mishra04 force-pushed the 06-30-fix_gemini_openai_through_signature_compatibility branch from 1b6a465 to afebdf9 Compare July 2, 2026 13:52
@Pratham-Mishra04
Pratham-Mishra04 merged commit dc0fcfa into dev Jul 2, 2026
14 of 16 checks passed
@Pratham-Mishra04
Pratham-Mishra04 deleted the 06-30-fix_gemini_openai_through_signature_compatibility branch July 2, 2026 13:54
yangtuooc added a commit to yangtuooc/bifrost that referenced this pull request Jul 2, 2026
* upstream/dev:
  feat(mcp): add per-MCP-server tool execution timeout (maximhq#4472)
  fix: billing on failed responses stream requests anthropic and bedrock (maximhq#4842)
  fix: gemini openai through signature compatibility (maximhq#4810)
  fix: cancelled state in logs (maximhq#4831)
  fix: perplexity responses api compatibility (maximhq#4813)
  docs: clarify two-layer token refresh behavior and disabled-client refresh token expiry (maximhq#4849)
  fix: skip background token refresh for disabled/unconfigured MCP clients and guarantee non-nil logger in sync workers (maximhq#4848)
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.

3 participants