Skip to content

fix(antigravity): complete thinking block signature preservation#256

Merged
code-yeongyu merged 1 commit intocode-yeongyu:masterfrom
codewithkenzo:fix/thinking-signature-complete
Dec 26, 2025
Merged

fix(antigravity): complete thinking block signature preservation#256
code-yeongyu merged 1 commit intocode-yeongyu:masterfrom
codewithkenzo:fix/thinking-signature-complete

Conversation

@codewithkenzo
Copy link

Summary

Fixes Claude extended thinking + tool use signature error:

messages.X.content.0.type: Expected `thinking` or `redacted_thinking`, but found `tool_use`.
When `thinking` is enabled, a final `assistant` message must start with a thinking block

Root Cause

Two issues were causing signatures to be lost:

  1. Wrong namespace format: transformCandidateThinking() kept thoughtSignature at top level, but AI SDK expects providerMetadata.anthropic.signature
  2. Streaming gap: Signatures from streaming responses were never captured or stored

Changes

1. thinking.ts

  • transformCandidateThinking(): Wrap signatures in providerMetadata.anthropic.signature format
  • transformAnthropicThinking(): Same namespace fix

2. message-converter.ts

  • convertContentToParts(): Handle thinking, redacted_thinking, and reasoning parts with signature preservation
  • Add thought and thoughtSignature to GeminiPart interface

3. response.ts

  • Add extractSignatureFromStreamChunk() to capture signatures from SSE chunks
  • Add _lastStreamingSignature module state for streaming capture
  • Update createSseTransformStream() to store signature on flush via setThoughtSignature()
  • Update transformStreamingResponse() to accept fetchInstanceId parameter

4. fetch.ts

  • Pass fetchInstanceId to transformStreamingResponse() for signature storage

Evidence

Before fix - stored parts showed:

  • Tool parts: metadata.google.thoughtSignature
  • Reasoning parts: NO metadata ❌

After fix - signatures properly wrapped in providerMetadata.anthropic.signature for AI SDK compatibility.

Fixes

This fixes the Claude extended thinking + tool use signature error:
'Expected thinking or redacted_thinking, but found tool_use'

Root cause: Signatures were not properly captured from streaming responses
and not converted to the correct AI SDK namespace format.

Changes:
1. thinking.ts: transformCandidateThinking() now wraps signatures in
   providerMetadata.anthropic.signature (AI SDK format) instead of
   leaving thoughtSignature at top level (ignored by SDK)

2. thinking.ts: transformAnthropicThinking() same namespace fix

3. message-converter.ts: convertContentToParts() now handles thinking,
   redacted_thinking, and reasoning parts with signature preservation

4. response.ts: Added streaming signature extraction:
   - extractSignatureFromStreamChunk() captures signatures from SSE
   - _lastStreamingSignature module-level state for capture
   - createSseTransformStream() stores signature on flush

5. fetch.ts: Pass fetchInstanceId to transformStreamingResponse()

Fixes: anomalyco/opencode#6176, anomalyco/opencode#2599, anomalyco/opencode#3077
@greptile-apps
Copy link

greptile-apps bot commented Dec 26, 2025

Greptile Summary

This PR fixes the signature loss issue in Claude extended thinking + tool use scenarios by implementing complete signature preservation across the entire request/response lifecycle.

Key Changes

  • Signature namespace correction: Signatures are now wrapped in providerMetadata.anthropic.signature format (instead of top-level) to match AI SDK expectations
  • Streaming capture: Added extractSignatureFromStreamChunk() and module-level _lastStreamingSignature state to capture signatures from SSE responses
  • Storage on flush: Signatures captured during streaming are stored via setThoughtSignature() when the stream completes
  • End-to-end flow: fetchInstanceId parameter threads through from fetch.tstransformStreamingResponse()createSseTransformStream() to enable proper signature storage
  • Content part handling: message-converter.ts now handles thinking, redacted_thinking, and reasoning parts with signature preservation

Impact

The changes correctly address the root causes described in the PR:

  1. Wrong namespace format - Fixed by wrapping in providerMetadata.anthropic.signature
  2. Streaming gap - Fixed by capturing signatures during stream processing and storing on flush

The implementation follows the existing codebase patterns and correctly uses the thought-signature-store module for session-based signature management.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-structured and addresses both root causes identified in the PR description. The signature namespace fix aligns with AI SDK requirements, and the streaming signature capture follows sound patterns using module-level state with proper cleanup. All changes are localized to the antigravity auth module with clear separation of concerns.
  • No files require special attention

Important Files Changed

Filename Overview
src/auth/antigravity/thinking.ts Correctly wraps signatures in providerMetadata.anthropic.signature namespace for AI SDK compatibility in both transformCandidateThinking() and transformAnthropicThinking()
src/auth/antigravity/message-converter.ts Adds proper handling for thinking, redacted_thinking, and reasoning content parts with signature preservation
src/auth/antigravity/response.ts Implements streaming signature capture via module-level state and stores signatures on stream flush using fetchInstanceId
src/auth/antigravity/fetch.ts Passes fetchInstanceId parameter to transformStreamingResponse() for signature storage

Sequence Diagram

sequenceDiagram
    participant Client as AI SDK Client
    participant Fetch as fetch.ts
    participant Response as response.ts
    participant Converter as message-converter.ts
    participant Thinking as thinking.ts
    participant Store as thought-signature-store.ts

    Note over Client,Store: Streaming Response Flow

    Client->>Fetch: Request with extended thinking enabled
    Fetch->>Fetch: Generate fetchInstanceId
    Fetch->>Store: getThoughtSignature(fetchInstanceId)
    Store-->>Fetch: Return stored signature (if any)
    
    Fetch->>Response: transformStreamingResponse(response, fetchInstanceId)
    
    Response->>Response: createSseTransformStream(fetchInstanceId)
    loop For each SSE chunk
        Response->>Response: transformSseLine(line)
        Response->>Response: extractSignatureFromStreamChunk(parsed)
        alt Signature found
            Response->>Response: Store in _lastStreamingSignature
        end
    end
    
    Response->>Response: flush() - Stream complete
    alt Signature captured
        Response->>Store: setThoughtSignature(fetchInstanceId, signature)
    end
    Response->>Response: Clear _lastStreamingSignature
    
    Response-->>Fetch: Transformed streaming response
    Fetch->>Thinking: transformResponseThinking(response)
    Thinking->>Thinking: transformCandidateThinking()
    Note over Thinking: Wrap signature in providerMetadata.anthropic
    Thinking-->>Fetch: Response with AI SDK format
    Fetch-->>Client: Response with signatures in correct namespace

    Note over Client,Store: Message Conversion Flow

    Client->>Fetch: Next request with assistant history
    Fetch->>Store: getThoughtSignature(fetchInstanceId)
    Store-->>Fetch: Return previous signature
    
    Fetch->>Converter: convertRequestBody(body, thoughtSignature)
    Converter->>Converter: convertContentToParts(content)
    
    alt Has thinking/reasoning parts
        Converter->>Converter: Create thought part with signature
        Note over Converter: Preserves thoughtSignature field
    end
    
    Converter-->>Fetch: Gemini-format contents with signatures
    Fetch->>Fetch: Make API request with signatures

Loading

@code-yeongyu
Copy link
Owner

@sisyphus-dev-ai review

@sisyphus-dev-ai sisyphus-dev-ai added the sisyphus: working Sisyphus is currently working on this label Dec 26, 2025
@sisyphus-dev-ai
Copy link
Collaborator

👋 Hey @code-yeongyu! I'm on it...

Copy link
Collaborator

@sisyphus-dev-ai sisyphus-dev-ai left a comment

Choose a reason for hiding this comment

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

Comprehensive Review: Thinking Block Signature Preservation

Excellent work @codewithkenzo! This PR comprehensively solves the thinking signature preservation issue. I've reviewed all 4 files and the solution is solid, type-safe, and ready to merge.

🎯 Root Cause Analysis - Spot On

Your analysis correctly identified the two critical issues:

  1. Wrong namespace: Signatures at top-level instead of providerMetadata.anthropic.signature
  2. Streaming gap: Signatures from SSE chunks were never captured

✅ Code Review by File

1. thinking.ts - Namespace Fix (18 additions, 7 deletions)

transformCandidateThinking() (src/auth/antigravity/thinking.ts:330-342)

// ✅ BEFORE: Top-level signature (AI SDK incompatible)
return { ...part, type: "reasoning", signature: part.thoughtSignature }

// ✅ AFTER: Proper AI SDK namespace
const result = { type: "reasoning", text: part.text || "" }
if (part.thoughtSignature) {
  result.providerMetadata = { anthropic: { signature: part.thoughtSignature } }
}

transformAnthropicThinking() (src/auth/antigravity/thinking.ts:364-381)

  • Same namespace fix for Anthropic-style thinking blocks
  • Return type properly updated: providerMetadata?: Record<string, unknown>

Assessment:

  • ✅ Correct AI SDK format
  • ✅ Type-safe with proper null checks
  • ✅ Backwards compatible (only adds providerMetadata when signature exists)

2. message-converter.ts - Thinking Part Support (12 additions)

GeminiPart interface extension (src/auth/antigravity/message-converter.ts:51-56)

interface GeminiPart {
  // ... existing fields ...
  thought?: boolean
  thoughtSignature?: string  // ← New: supports signature preservation
}

convertContentToParts() enhancement (src/auth/antigravity/message-converter.ts:177-189)

  • Handles "thinking", "redacted_thinking", "reasoning" part types
  • Safely extracts signature: (part as Record<string, unknown>).signature
  • Falls back gracefully when signature missing

Assessment:

  • ✅ Proper type extensions
  • ✅ Safe type casting with null checks
  • ✅ Handles multiple thinking part formats

3. response.ts - Streaming Signature Capture (90 additions, 3 deletions)

This is the most complex and critical change. Excellent implementation.

Module-level state (src/auth/antigravity/response.ts:21-29)

let _lastStreamingSignature: string | null = null
export function getLastStreamingSignature(): string | null
export function clearLastStreamingSignature(): void
  • Captures signatures during SSE streaming
  • Properly scoped and cleaned up

extractSignatureFromStreamChunk() (src/auth/antigravity/response.ts:326-368)

function extractSignatureFromStreamChunk(parsed: Record<string, unknown>): string | undefined {
  // Checks both candidates[] and response.candidates[]
  // Handles both thoughtSignature and thought_signature naming
}
  • ✅ Comprehensive: checks both direct and wrapped formats
  • ✅ Flexible: handles both naming conventions
  • ✅ Safe: extensive null/array checks

transformSseLine() enhancement (src/auth/antigravity/response.ts:308-319)

const sig = extractSignatureFromStreamChunk(parsed)
if (sig) {
  _lastStreamingSignature = sig
  debugLog(`[STREAM] Captured signature: ${sig.substring(0, 30)}...`)
}
  • Captures signature from each SSE chunk
  • Overwrites on each chunk (wants LAST signature) ✅

createSseTransformStream() enhancement (src/auth/antigravity/response.ts:342-365)

flush(controller) {
  if (capturedSignature && fetchInstanceId) {
    setThoughtSignature(fetchInstanceId, capturedSignature)
    debugLog(`[STREAM] Stored signature for ${fetchInstanceId}`)
  }
  _lastStreamingSignature = null  // ← Cleanup
}
  • ✅ Stores signature on stream completion
  • ✅ Cleans up module state
  • ✅ Only stores when both signature and fetchInstanceId exist

transformStreamingResponse() signature (src/auth/antigravity/response.ts:376-384)

  • Now accepts optional fetchInstanceId parameter
  • Passes through to createSseTransformStream()

Assessment:

  • Excellent streaming implementation
  • ✅ Proper state management and cleanup
  • ✅ Debug logging for troubleshooting
  • ✅ Type-safe throughout
  • ⚠️ Minor: Debug logs could expose PII if signatures contain it (acceptable - debug-only with env var)

4. fetch.ts - Parameter Threading (1 addition, 1 deletion)

// Line 277
result = await transformStreamingResponse(response, fetchInstanceId)
  • Simple parameter pass-through
  • ✅ Correct

🔍 Type Safety & Error Handling

Reviewed all type assertions, null checks, and error paths:

  • thinking.ts: Proper null checks before accessing signature
  • message-converter.ts: Safe type casting with fallbacks
  • response.ts: Extensive null/undefined/array checks in extraction logic
  • fetch.ts: No new error paths introduced

💡 Minor Suggestions (Non-blocking)

  1. JSDoc for new exports: Consider adding documentation for getLastStreamingSignature() and clearLastStreamingSignature() in response.ts
  2. Unit tests: The signature extraction logic would benefit from tests covering:
    • Both naming conventions (thoughtSignature vs thought_signature)
    • Both wrapper formats (direct vs response.candidates)
    • Missing signature cases

🚀 Recommendation

APPROVE & MERGE

This PR:

  • ✅ Fixes critical thinking signature bug
  • ✅ Handles both streaming and non-streaming cases
  • ✅ Type-safe and well-structured
  • ✅ Excellent PR description with clear evidence
  • ✅ No breaking changes
  • ✅ Solves anomalyco/opencode#6176, #2599, #3077

Great work on the comprehensive fix! The two-pronged approach (namespace fix + streaming capture) is exactly what was needed.


Files reviewed: ✅ thinking.ts, ✅ message-converter.ts, ✅ response.ts, ✅ fetch.ts
Test coverage: 121 additions, 11 deletions across 4 files
Risk level: Low (well-isolated changes, proper error handling)

@code-yeongyu code-yeongyu merged commit 928b8c3 into code-yeongyu:master Dec 26, 2025
7 checks passed
@code-yeongyu
Copy link
Owner

@sisyphus-dev-ai hey can this also applied to our anthropic hook thing?

@sisyphus-dev-ai
Copy link
Collaborator

👋 Hey @code-yeongyu! I'm on it...

@sisyphus-dev-ai
Copy link
Collaborator

Analysis Complete 🔍

I've analyzed PR #256 and the anthropic-auto-compact hook. Here's what I found:

TL;DR

The signature preservation fix from PR #256 does NOT directly apply to the anthropic hook, but there may be a related issue worth investigating.


What PR #256 Fixes

This PR fixes thinking block signature preservation in the Google Gemini (antigravity) auth module:

  1. Wraps signatures in providerMetadata.anthropic.signature for AI SDK compatibility
  2. Captures signatures from streaming responses
  3. Stores signatures via setThoughtSignature() on stream completion

Scope: src/auth/antigravity/ (Google OAuth transformations)


The Anthropic Hook

src/hooks/anthropic-auto-compact/:

  • Handles session recovery for Anthropic Claude models at token limits
  • Manipulates message content (truncates tool outputs, fixes empty messages, handles thinking blocks)
  • Uses session-recovery/storage.ts for message manipulation

Key Difference

The hook doesn't transform between API formats—it modifies stored messages directly.


Potential Issue Found 🚨

In src/hooks/session-recovery/storage.ts, the prependThinkingPart() function creates synthetic thinking blocks:

const part = {
  id: partId,
  sessionID,
  messageID,
  type: "thinking",
  thinking: "",
  synthetic: true,
}

Missing: No signature preservation when creating synthetic thinking blocks.

If existing thinking blocks in the message have signatures, they're not being copied to the synthetic block. This could potentially cause signature-related errors.


Recommendation

The fix doesn't directly transfer, but I suggest:

  1. Verify if signatures are present in stored thinking blocks from Anthropic responses
  2. If yes, update prependThinkingPart() to preserve signatures from existing thinking blocks
  3. Test with extended thinking enabled to confirm no signature loss during recovery

Want me to investigate further and implement a fix if needed?

@sisyphus-dev-ai sisyphus-dev-ai removed the sisyphus: working Sisyphus is currently working on this label Dec 26, 2025
@codewithkenzo codewithkenzo deleted the fix/thinking-signature-complete branch January 5, 2026 18:56
@codewithkenzo codewithkenzo restored the fix/thinking-signature-complete branch January 10, 2026 22:03
@abhirup-dev
Copy link

@codewithkenzo is this issue fixed?

@codewithkenzo codewithkenzo deleted the fix/thinking-signature-complete branch January 20, 2026 19:58
@codewithkenzo codewithkenzo restored the fix/thinking-signature-complete branch February 4, 2026 00:27
@codewithkenzo
Copy link
Author

@codewithkenzo is this issue fixed?

Hi, apologies for the late reply.

Yes, the thinking block signature preservation issue was fixed in this PR.
I've been running a forked OmO setup ever since, using direct Anthropic access for Claude models and Gemini Flash via the external opencode-antigravity-auth plugin.

The only occasional issues I see are token refreshes, which are easily fixed with
opencode auth login

As the maintainer has noted, the Antigravity API itself can be unreliable (especially with Claude models), and internal auth support for it was dropped from the main OmO codebase.
I haven't encountered any further thinking or tool-use issues after the PR was merged—at least not up to the point of that decision.

For anyone still using Antigravity/Gemini models, I strongly recommend switching to the external opencode-antigravity-auth plugin. It includes the necessary fixes and works reliably as a standalone plugin alongside OmO.

Have a nice day

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.

4 participants