fix(antigravity): complete thinking block signature preservation#256
Conversation
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 SummaryThis 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
ImpactThe changes correctly address the root causes described in the PR:
The implementation follows the existing codebase patterns and correctly uses the Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
|
|
@sisyphus-dev-ai review |
|
👋 Hey @code-yeongyu! I'm on it... |
sisyphus-dev-ai
left a comment
There was a problem hiding this comment.
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:
- Wrong namespace: Signatures at top-level instead of
providerMetadata.anthropic.signature - 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
fetchInstanceIdparameter - 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)
- JSDoc for new exports: Consider adding documentation for
getLastStreamingSignature()andclearLastStreamingSignature()in response.ts - 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)
|
@sisyphus-dev-ai hey can this also applied to our anthropic hook thing? |
|
👋 Hey @code-yeongyu! I'm on it... |
Analysis Complete 🔍I've analyzed PR #256 and the anthropic-auto-compact hook. Here's what I found: TL;DRThe 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 FixesThis PR fixes thinking block signature preservation in the Google Gemini (antigravity) auth module:
Scope: The Anthropic Hook
Key Difference
The hook doesn't transform between API formats—it modifies stored messages directly. Potential Issue Found 🚨In 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. RecommendationThe fix doesn't directly transfer, but I suggest:
Want me to investigate further and implement a fix if needed? |
|
@codewithkenzo is this issue fixed? |
Hi, apologies for the late reply. Yes, the thinking block signature preservation issue was fixed in this PR. The only occasional issues I see are token refreshes, which are easily fixed with 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. 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 |
Summary
Fixes Claude extended thinking + tool use signature error:
Root Cause
Two issues were causing signatures to be lost:
transformCandidateThinking()keptthoughtSignatureat top level, but AI SDK expectsproviderMetadata.anthropic.signatureChanges
1. thinking.ts
transformCandidateThinking(): Wrap signatures inproviderMetadata.anthropic.signatureformattransformAnthropicThinking(): Same namespace fix2. message-converter.ts
convertContentToParts(): Handlethinking,redacted_thinking, andreasoningparts with signature preservationthoughtandthoughtSignaturetoGeminiPartinterface3. response.ts
extractSignatureFromStreamChunk()to capture signatures from SSE chunks_lastStreamingSignaturemodule state for streaming capturecreateSseTransformStream()to store signature on flush viasetThoughtSignature()transformStreamingResponse()to acceptfetchInstanceIdparameter4. fetch.ts
fetchInstanceIdtotransformStreamingResponse()for signature storageEvidence
Before fix - stored parts showed:
metadata.google.thoughtSignature✅After fix - signatures properly wrapped in
providerMetadata.anthropic.signaturefor AI SDK compatibility.Fixes
thinkingorredacted_thinking, but foundtext." anomalyco/opencode#2599thinkingorredacted_thinking, but foundtool_useanomalyco/opencode#3077