Skip to content

fix: preserve Claude thinking signatures in Codex translator#1932

Open
Darley-Wey wants to merge 4 commits intorouter-for-me:devfrom
Darley-Wey:fix/claude2codex
Open

fix: preserve Claude thinking signatures in Codex translator#1932
Darley-Wey wants to merge 4 commits intorouter-for-me:devfrom
Darley-Wey:fix/claude2codex

Conversation

@Darley-Wey
Copy link
Contributor

Summary

  • keep Claude-compatible thinking blocks from Codex responses compliant by always including the signature field
  • delay closing streaming thinking blocks until the upstream reasoning encrypted_content is available and emit it as signature_delta
  • preserve reasoning signatures in non-stream responses and add targeted regression tests

Testing

  • go test ./internal/translator/codex/claude -run TestConvertCodexResponseToClaude -count=1

Copilot AI review requested due to automatic review settings March 7, 2026 10:15
@gemini-code-assist
Copy link
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 enhances the Codex translator to Claude by ensuring that 'thinking' blocks consistently include and correctly handle signature information. This involves modifying the streaming response conversion to delay the completion of thinking blocks until the signature is received, and explicitly adding a 'signature' field to both streaming and non-streaming thinking block outputs, thereby improving compliance and data integrity.

Highlights

  • Signature Preservation: Ensured Claude-compatible thinking blocks from Codex responses always include the 'signature' field for compliance.
  • Streaming Thinking Blocks: Implemented delayed closing of streaming thinking blocks until the 'encrypted_content' (reasoning signature) is available, emitting it as a 'signature_delta'.
  • Non-Stream Signatures: Preserved reasoning signatures in non-streaming responses and added dedicated regression tests to validate this behavior.

🧠 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/codex/claude/codex_claude_response.go
    • Added 'ThinkingBlockOpen', 'ThinkingStopPending', and 'ThinkingSignature' fields to 'ConvertCodexResponseToClaudeParams' for state management.
    • Removed an unused 'requestRawJSON' parameter from the 'ConvertCodexResponseToClaude' function signature.
    • Introduced a new 'finalizeCodexThinkingBlock' helper function to manage the completion and signature emission of thinking blocks.
    • Modified streaming logic to include a 'signature' field in 'content_block_start' for thinking blocks and to emit 'signature_delta' when the 'encrypted_content' becomes available.
    • Updated non-streaming conversion to extract and include the 'encrypted_content' as 'signature' in the 'thinking' content block.
  • internal/translator/codex/claude/codex_claude_response_test.go
    • Added a new test file to cover signature preservation in both streaming and non-streaming thinking blocks.
    • Included tests for streaming thinking blocks with and without immediate reasoning item availability.
Activity
  • No human activity has occurred on this pull request yet.
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 addresses the preservation of Claude thinking signatures when translating from Codex responses, for both streaming and non-streaming scenarios. The logic to delay closing thinking blocks until a signature is available is well-implemented, and the new finalizeCodexThinkingBlock function is a good addition. The changes are supported by a solid set of new regression tests that cover the key use cases. I have one suggestion to refine the logic for sending signature deltas.

Comment on lines +397 to +401
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To avoid sending a redundant signature_delta event when no signature has been received, it's better to make the emission of this event conditional on params.ThinkingSignature being non-empty. This ensures that a signature_delta is only sent when there is an actual signature to convey, aligning more closely with the purpose of a delta event.

Suggested change
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
if params.ThinkingSignature != "" {
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
}

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Codex→Claude response translator to preserve Claude “thinking” block signatures by ensuring the signature field is always present and by carrying encrypted_content through streaming and non-streaming conversions.

Changes:

  • Add streaming state to keep thinking blocks open until encrypted_content is available, then emit it via signature_delta.
  • Preserve encrypted_content as signature in non-stream (single JSON) responses.
  • Add regression tests covering streaming and non-stream signature preservation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
internal/translator/codex/claude/codex_claude_response.go Adds thinking-block state + finalization helper; preserves/enriches thinking signatures in stream and non-stream conversions.
internal/translator/codex/claude/codex_claude_response_test.go Adds targeted tests ensuring thinking blocks include signature and that signatures are preserved/emitted.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +397 to +401
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

finalizeCodexThinkingBlock always emits a signature_delta event even when ThinkingSignature is empty. In cases where the upstream does not provide reasoning.encrypted_content (which appears to be allowed/optional elsewhere in the codebase), this will send a signature_delta with an empty signature, which is likely not a valid/meaningful Claude streaming event. Consider only emitting signature_delta when ThinkingSignature != "", while still emitting content_block_stop to close the thinking block.

Suggested change
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
if params.ThinkingSignature != "" {
signatureDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":""}}`
signatureDelta, _ = sjson.Set(signatureDelta, "index", params.BlockIndex)
signatureDelta, _ = sjson.Set(signatureDelta, "delta.signature", params.ThinkingSignature)
output += "event: content_block_delta\n"
output += fmt.Sprintf("data: %s\n\n", signatureDelta)
}

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +70
if params.ThinkingBlockOpen {
switch rootResult.Get("type").String() {
case "response.content_part.added", "response.completed":
output += finalizeCodexThinkingBlock(params)
}
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The pre-switch finalization closes an open thinking block on response.content_part.added / response.completed regardless of whether the thinking block has actually finished (ThinkingStopPending) and regardless of whether a signature has arrived. If encrypted_content arrives later via response.output_item.{added|done} (which this PR is trying to preserve), this early close will prevent emitting the correct signature_delta because the block is already stopped and state has been reset. Consider only auto-finalizing once ThinkingStopPending is true, or buffering the subsequent event until the signature is known (or deciding explicitly that late signatures are unsupported).

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +95
for _, line := range lines {
if !strings.HasPrefix(line, "data: ") {
continue
}
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "thinking" {
if !data.Get("content_block.signature").Exists() {
t.Fatalf("thinking start block missing signature field: %s", line)
}
return
}
}

t.Fatal("expected thinking content_block_start event")
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The streaming test for the no-reasoning-item case only asserts that the thinking start event includes a signature field, but it doesn't assert what happens when the thinking block is closed (e.g., whether content_block_stop is emitted and whether signature_delta is omitted when no encrypted_content exists). Adding assertions around the close sequence would help prevent regressions, especially if signature_delta emission becomes conditional.

Suggested change
for _, line := range lines {
if !strings.HasPrefix(line, "data: ") {
continue
}
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "thinking" {
if !data.Get("content_block.signature").Exists() {
t.Fatalf("thinking start block missing signature field: %s", line)
}
return
}
}
t.Fatal("expected thinking content_block_start event")
var (
thinkingStartFound bool
thinkingStopFound bool
thinkingIndex int64
signatureDeltaFound bool
)
for _, line := range lines {
if !strings.HasPrefix(line, "data: ") {
continue
}
data := gjson.Parse(strings.TrimPrefix(line, "data: "))
// Track thinking content_block_start and ensure it has a signature field.
if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "thinking" {
if !data.Get("content_block.signature").Exists() {
t.Fatalf("thinking start block missing signature field: %s", line)
}
thinkingStartFound = true
thinkingIndex = data.Get("content_block.index").Int()
}
// Track corresponding content_block_stop for the same index.
if data.Get("type").String() == "content_block_stop" {
if data.Get("content_block.index").Int() == thinkingIndex {
thinkingStopFound = true
}
}
// Ensure no signature_delta is emitted when there is no encrypted_content.
if data.Get("delta.signature_delta").Exists() || data.Get("signature_delta").Exists() {
signatureDeltaFound = true
}
}
if !thinkingStartFound {
t.Fatal("expected thinking content_block_start event")
}
if !thinkingStopFound {
t.Fatal("expected thinking content_block_stop event for the thinking content block")
}
if signatureDeltaFound {
t.Fatal("did not expect signature_delta to be emitted when no encrypted_content exists")
}

Copilot uses AI. Check for mistakes.
@Darley-Wey
Copy link
Contributor Author

Addressed the review feedback in follow-up commit a9aab5b.

  • only emit signature_delta when encrypted_content is actually present
  • only auto-close an open thinking block after response.reasoning_summary_part.done has been seen
  • strengthened the no-signature streaming regression test to assert the block still closes cleanly without emitting signature_delta
    Retested with:
  • go test ./internal/translator/codex/claude -run TestConvertCodexResponseToClaude -count=1

# Conflicts:
#	internal/translator/codex/claude/codex_claude_response.go
@luispater luispater changed the base branch from main to dev March 8, 2026 14:51
Copy link
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.

From a translator-behavior perspective, this looks good to me.

I re-checked the thinking signature flow against the actual Codex event ordering here: response.output_item.added for the reasoning item already carries encrypted_content, then the reasoning summary starts streaming, and response.output_item.done carries encrypted_content again before the thinking block is finalized. With that event order, the updated state machine preserves the Claude thinking signature correctly.

Non-blocking suggestion: it would still be great to add a regression test for that exact event ordering so the assumption stays encoded in the test suite.

Requesting changes only because this PR currently has merge conflicts. Please rebase/merge the latest main and resolve the conflicts, then this should be good to re-review.

@Darley-Wey
Copy link
Contributor Author

@luispater Conflicts have been resolved in 8bb1ede by merging the latest dev into this branch. The PR is mergeable again now, and checks are rerunning. Thanks for the review.

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