Claude extended thinking + tool use: signature not preserved in message history #3872

Closed
opened 2026-02-16 17:41:46 -05:00 by yindo · 6 comments
Owner

Originally created by @codewithkenzo on GitHub (Dec 25, 2025).

Originally assigned to: @rekram1-node on GitHub.

Description

When using Claude models with extended thinking (e.g., claude-opus-4-5-thinking, claude-4-5-sonnet with thinking enabled) via custom providers (like oh-my-opencode's Antigravity), multi-turn conversations with tool use fail with:

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
(preceeding the lastmost set of `tool_use` and `tool_result` blocks).

Root Cause

Claude's API requires:

  1. When thinking is enabled, every assistant message must start with a thinking block
  2. Thinking blocks in message history MUST include the signature field (returned by Claude in original response)

The signature appears to not be preserved when storing assistant messages in OpenCode's message history. When the message is sent back in subsequent requests, the thinking block is either:

  • Missing entirely, OR
  • Missing the required signature field

Related Issues

Expected Behavior

When Claude returns:

{
  "content": [
    { "type": "thinking", "thinking": "...", "signature": "abc123..." },
    { "type": "tool_use", "id": "...", "name": "bash", "input": {...} }
  ]
}

The signature field should be preserved in message history so it can be sent back in subsequent requests.

Workaround

For oh-my-opencode users, I've submitted a PR that strips unsigned thinking blocks from history to prevent the API error: https://github.com/code-yeongyu/oh-my-opencode/pull/253

This is a workaround, not a fix - the proper fix is to preserve signatures in OpenCode's message storage.

Environment

  • OpenCode version: 1.0.x (latest)
  • Provider: Custom (oh-my-opencode Antigravity)
  • Model: claude-opus-4-5-thinking, claude-4-5-sonnet with thinking
  • OS: Linux
Originally created by @codewithkenzo on GitHub (Dec 25, 2025). Originally assigned to: @rekram1-node on GitHub. ## Description When using Claude models with extended thinking (e.g., `claude-opus-4-5-thinking`, `claude-4-5-sonnet` with thinking enabled) via custom providers (like oh-my-opencode's Antigravity), multi-turn conversations with tool use fail with: ``` 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 (preceeding the lastmost set of `tool_use` and `tool_result` blocks). ``` ## Root Cause Claude's API requires: 1. When `thinking` is enabled, every assistant message must start with a `thinking` block 2. Thinking blocks in message history MUST include the `signature` field (returned by Claude in original response) The signature appears to not be preserved when storing assistant messages in OpenCode's message history. When the message is sent back in subsequent requests, the thinking block is either: - Missing entirely, OR - Missing the required `signature` field ## Related Issues - #2599 - Original thinking block bug (closed but issue persists) - #3077 - Same error message - anthropics/claude-code#11432 - Same error in Claude Code ## Expected Behavior When Claude returns: ```json { "content": [ { "type": "thinking", "thinking": "...", "signature": "abc123..." }, { "type": "tool_use", "id": "...", "name": "bash", "input": {...} } ] } ``` The `signature` field should be preserved in message history so it can be sent back in subsequent requests. ## Workaround For oh-my-opencode users, I've submitted a PR that strips unsigned thinking blocks from history to prevent the API error: https://github.com/code-yeongyu/oh-my-opencode/pull/253 This is a workaround, not a fix - the proper fix is to preserve signatures in OpenCode's message storage. ## Environment - OpenCode version: 1.0.x (latest) - Provider: Custom (oh-my-opencode Antigravity) - Model: claude-opus-4-5-thinking, claude-4-5-sonnet with thinking - OS: Linux
yindo closed this issue 2026-02-16 17:41:46 -05:00
Author
Owner

@github-actions[bot] commented on GitHub (Dec 25, 2025):

This issue might be a duplicate of existing issues. Please check:

  • #2599: [bug] v0.8.0 Introduced thinking block bug with Claude Sonnet 4 - Same error about thinking blocks not being preserved properly in message history after tool calls
  • #3077: Expected thinking or redacted_thinking, but found tool_use - Related issue with the same API error when using thinking with tool calls

Feel free to ignore if none of these address your specific case.

@github-actions[bot] commented on GitHub (Dec 25, 2025): This issue might be a duplicate of existing issues. Please check: - #2599: [bug] v0.8.0 Introduced thinking block bug with Claude Sonnet 4 - Same error about thinking blocks not being preserved properly in message history after tool calls - #3077: Expected `thinking` or `redacted_thinking`, but found `tool_use` - Related issue with the same API error when using thinking with tool calls Feel free to ignore if none of these address your specific case.
Author
Owner

@codewithkenzo commented on GitHub (Dec 25, 2025):

Deep Dive Analysis Complete

I've traced the complete data flow and found the exact locations where signatures are handled (and potentially lost).

Data Flow Summary

Claude API Response
    ↓
{ type: "thinking", thinking: "...", signature: "abc123" }
    ↓
@ai-sdk/anthropic v2.0.50 (converts to AI SDK format)
    ↓
{ type: "reasoning", text: "...", providerMetadata: { anthropic: { signature: "abc123" } } }
    ↓
OpenCode processor.ts (stores in reasoningMap, then Session.updatePart)
    ↓
Storage as ReasoningPart with metadata field
    ↓
message-v2.ts toModelMessages() (retrieves and converts back)
    ↓
{ type: "reasoning", text: "...", providerMetadata: part.metadata }
    ↓
AI SDK convertToModelMessages() (maps providerMetadata → providerOptions)
    ↓
@ai-sdk/anthropic (reads providerOptions.anthropic.signature)
    ↓
Claude API Request

Key Findings in OpenCode

1. processor.ts (lines 58-97) - Stream event handling:

case "reasoning-start":
  reasoningMap[value.id] = {
    type: "reasoning",
    text: "",
    metadata: value.providerMetadata,  // Initial metadata (often undefined)
  }
  break

case "reasoning-delta":
  if (value.providerMetadata) part.metadata = value.providerMetadata  // Signature arrives here!
  if (part.text) await Session.updatePart({ part, delta: value.text })
  break

case "reasoning-end":
  if (value.providerMetadata) part.metadata = value.providerMetadata  // No metadata in end event
  await Session.updatePart(part)  // Final storage
  break

2. message-v2.ts (lines 531-536) - Retrieval:

if (part.type === "reasoning") {
  assistantMessage.parts.push({
    type: "reasoning",
    text: part.text,
    providerMetadata: part.metadata,  // Should contain { anthropic: { signature } }
  })
}

3. ReasoningPart Schema (line 80-91) - Has metadata field:

export const ReasoningPart = PartBase.extend({
  type: z.literal("reasoning"),
  text: z.string(),
  metadata: z.record(z.string(), z.any()).optional(),  // ← Defined but not stored!
  time: z.object({ start: z.number(), end: z.number().optional() }),
})

Evidence: Stored Parts Missing Metadata

Checked actual stored reasoning parts in ~/.local/share/opencode/storage/part/:

{
  "id": "prt_b57dc1066001...",
  "type": "reasoning",
  "text": "The user is clarifying...",
  "time": { "start": 1766705598566, "end": 1766705620131 }
  // NO metadata field! Signature is lost!
}

Signature Event Details

The signature arrives via a special stream event from @ai-sdk/anthropic:

// signature_delta event (dist/index.js ~line 2682)
case "signature_delta": {
  controller.enqueue({
    type: "reasoning-delta",
    id: String(value.index),
    delta: "",  // Empty text delta
    providerMetadata: {
      anthropic: {
        signature: value.delta.signature  // Signature here!
      }
    }
  });
}

This is then remapped by AI SDK (dist/index.js ~line 5185):

case "reasoning-delta": {
  controller.enqueue({
    type: "reasoning-delta",
    id: chunk.id,
    text: chunk.delta,  // "" for signature event
    providerMetadata: chunk.providerMetadata  // { anthropic: { signature } }
  });
}

Potential Issues

  1. Timing: The signature arrives in a late reasoning-delta event after all text. If Session.updatePart is called but storage doesn't persist metadata, it's lost.

  2. Zod parsing: The fn() wrapper uses schema.parse() which validates against the schema. The schema DOES include metadata, but something may be stripping it.

  3. Storage write: Need to verify that Storage.write() correctly serializes the metadata field.

Suggested Investigation

  1. Add debug logging in processor.ts to verify value.providerMetadata is populated when signature arrives
  2. Check if Session.updatePart correctly persists the metadata field
  3. Verify the stored JSON file immediately after a reasoning block completes

Workaround in oh-my-opencode

I've identified a fix in oh-my-opencode's Antigravity adapter that preserves signatures when converting message formats. PR forthcoming.

Related Code Locations

File Lines Purpose
session/processor.ts 58-97 Stream event handling
session/message-v2.ts 80-91, 531-536 ReasoningPart schema & conversion
session/index.ts 371-380 updatePart function
provider/transform.ts 123-138 Anthropic-specific normalization

Let me know if you'd like me to submit a PR with debug logging to help trace where exactly the metadata is being lost.

@codewithkenzo commented on GitHub (Dec 25, 2025): ## Deep Dive Analysis Complete I've traced the complete data flow and found the exact locations where signatures are handled (and potentially lost). ### Data Flow Summary ``` Claude API Response ↓ { type: "thinking", thinking: "...", signature: "abc123" } ↓ @ai-sdk/anthropic v2.0.50 (converts to AI SDK format) ↓ { type: "reasoning", text: "...", providerMetadata: { anthropic: { signature: "abc123" } } } ↓ OpenCode processor.ts (stores in reasoningMap, then Session.updatePart) ↓ Storage as ReasoningPart with metadata field ↓ message-v2.ts toModelMessages() (retrieves and converts back) ↓ { type: "reasoning", text: "...", providerMetadata: part.metadata } ↓ AI SDK convertToModelMessages() (maps providerMetadata → providerOptions) ↓ @ai-sdk/anthropic (reads providerOptions.anthropic.signature) ↓ Claude API Request ``` ### Key Findings in OpenCode **1. processor.ts (lines 58-97)** - Stream event handling: ```typescript case "reasoning-start": reasoningMap[value.id] = { type: "reasoning", text: "", metadata: value.providerMetadata, // Initial metadata (often undefined) } break case "reasoning-delta": if (value.providerMetadata) part.metadata = value.providerMetadata // Signature arrives here! if (part.text) await Session.updatePart({ part, delta: value.text }) break case "reasoning-end": if (value.providerMetadata) part.metadata = value.providerMetadata // No metadata in end event await Session.updatePart(part) // Final storage break ``` **2. message-v2.ts (lines 531-536)** - Retrieval: ```typescript if (part.type === "reasoning") { assistantMessage.parts.push({ type: "reasoning", text: part.text, providerMetadata: part.metadata, // Should contain { anthropic: { signature } } }) } ``` **3. ReasoningPart Schema (line 80-91)** - Has metadata field: ```typescript export const ReasoningPart = PartBase.extend({ type: z.literal("reasoning"), text: z.string(), metadata: z.record(z.string(), z.any()).optional(), // ← Defined but not stored! time: z.object({ start: z.number(), end: z.number().optional() }), }) ``` ### Evidence: Stored Parts Missing Metadata Checked actual stored reasoning parts in `~/.local/share/opencode/storage/part/`: ```json { "id": "prt_b57dc1066001...", "type": "reasoning", "text": "The user is clarifying...", "time": { "start": 1766705598566, "end": 1766705620131 } // NO metadata field! Signature is lost! } ``` ### Signature Event Details The signature arrives via a special stream event from `@ai-sdk/anthropic`: ```typescript // signature_delta event (dist/index.js ~line 2682) case "signature_delta": { controller.enqueue({ type: "reasoning-delta", id: String(value.index), delta: "", // Empty text delta providerMetadata: { anthropic: { signature: value.delta.signature // Signature here! } } }); } ``` This is then remapped by AI SDK (dist/index.js ~line 5185): ```typescript case "reasoning-delta": { controller.enqueue({ type: "reasoning-delta", id: chunk.id, text: chunk.delta, // "" for signature event providerMetadata: chunk.providerMetadata // { anthropic: { signature } } }); } ``` ### Potential Issues 1. **Timing**: The signature arrives in a late `reasoning-delta` event after all text. If `Session.updatePart` is called but storage doesn't persist `metadata`, it's lost. 2. **Zod parsing**: The `fn()` wrapper uses `schema.parse()` which validates against the schema. The schema DOES include `metadata`, but something may be stripping it. 3. **Storage write**: Need to verify that `Storage.write()` correctly serializes the `metadata` field. ### Suggested Investigation 1. Add debug logging in `processor.ts` to verify `value.providerMetadata` is populated when signature arrives 2. Check if `Session.updatePart` correctly persists the `metadata` field 3. Verify the stored JSON file immediately after a reasoning block completes ### Workaround in oh-my-opencode I've identified a fix in oh-my-opencode's Antigravity adapter that preserves signatures when converting message formats. PR forthcoming. ### Related Code Locations | File | Lines | Purpose | |------|-------|---------| | `session/processor.ts` | 58-97 | Stream event handling | | `session/message-v2.ts` | 80-91, 531-536 | ReasoningPart schema & conversion | | `session/index.ts` | 371-380 | updatePart function | | `provider/transform.ts` | 123-138 | Anthropic-specific normalization | Let me know if you'd like me to submit a PR with debug logging to help trace where exactly the metadata is being lost.
Author
Owner

@rekram1-node commented on GitHub (Dec 25, 2025):

opencode already persists them, id have to see your plugin code for antigravity I think the loss of them is there

@rekram1-node commented on GitHub (Dec 25, 2025): opencode already persists them, id have to see your plugin code for antigravity I think the loss of them is there
Author
Owner

@codewithkenzo commented on GitHub (Dec 25, 2025):

Update: Verified OpenCode persists metadata correctly

@rekram1-node you're right - OpenCode does persist metadata. I found tool parts with metadata.google.thoughtSignature stored properly.

However, the issue is in oh-my-opencode's Antigravity adapter transformation:

The Bug

In transformCandidateThinking():

// Current: keeps thoughtSignature but wrong format
return {
  ...part,
  type: "reasoning",
  thought: undefined
};
// Result: { type: "reasoning", text: "...", thoughtSignature: "abc123" }

But AI SDK expects:

// Expected format
{
  type: "reasoning",
  text: "...",
  providerMetadata: { anthropic: { signature: "abc123" } }
}

Evidence

Found in stored parts:

  • Tool parts have metadata.google.thoughtSignature
  • Reasoning parts have NO metadata

Root Cause Chain

  1. Antigravity returns { thought: true, thoughtSignature: "..." } (Google format)
  2. oh-my-opencode transforms to { type: "reasoning", thoughtSignature: "..." }
  3. But doesn't wrap in providerMetadata.anthropic.signature
  4. AI SDK doesn't recognize it as a signature
  5. OpenCode stores reasoning without metadata
  6. Next request has no signature to inject

Fix Required (in oh-my-opencode)

// transformCandidateThinking should do:
return {
  type: "reasoning",
  text: part.text || "",
  providerMetadata: part.thoughtSignature ? {
    anthropic: { signature: part.thoughtSignature }
  } : undefined
};

Will update our PR #253 with this fix.

@codewithkenzo commented on GitHub (Dec 25, 2025): ## Update: Verified OpenCode persists metadata correctly ✅ @rekram1-node you're right - OpenCode does persist metadata. I found tool parts with `metadata.google.thoughtSignature` stored properly. **However**, the issue is in oh-my-opencode's Antigravity adapter transformation: ### The Bug In `transformCandidateThinking()`: ```javascript // Current: keeps thoughtSignature but wrong format return { ...part, type: "reasoning", thought: undefined }; // Result: { type: "reasoning", text: "...", thoughtSignature: "abc123" } ``` But AI SDK expects: ```javascript // Expected format { type: "reasoning", text: "...", providerMetadata: { anthropic: { signature: "abc123" } } } ``` ### Evidence Found in stored parts: - Tool parts have `metadata.google.thoughtSignature` ✅ - Reasoning parts have NO metadata ❌ ### Root Cause Chain 1. Antigravity returns `{ thought: true, thoughtSignature: "..." }` (Google format) 2. oh-my-opencode transforms to `{ type: "reasoning", thoughtSignature: "..." }` 3. But doesn't wrap in `providerMetadata.anthropic.signature` 4. AI SDK doesn't recognize it as a signature 5. OpenCode stores reasoning without metadata 6. Next request has no signature to inject ### Fix Required (in oh-my-opencode) ```javascript // transformCandidateThinking should do: return { type: "reasoning", text: part.text || "", providerMetadata: part.thoughtSignature ? { anthropic: { signature: part.thoughtSignature } } : undefined }; ``` Will update our PR #253 with this fix.
Author
Owner

@codewithkenzo commented on GitHub (Dec 25, 2025):

Fix Submitted

Created comprehensive fix PR: https://github.com/code-yeongyu/oh-my-opencode/pull/256

This PR addresses all the issues identified:

  1. Namespace conversion - transformCandidateThinking() and transformAnthropicThinking() now wrap signatures in providerMetadata.anthropic.signature format that AI SDK expects

  2. Streaming signature capture - Added extractSignatureFromStreamChunk() to capture signatures from SSE chunks during streaming, stored via setThoughtSignature() on stream flush

  3. Message converter - convertContentToParts() now handles thinking, redacted_thinking, and reasoning parts with proper signature preservation

The fix ensures signatures flow correctly through the entire pipeline:

Antigravity Response → thoughtSignature captured → 
wrapped in providerMetadata.anthropic.signature → 
OpenCode stores in metadata → 
retrieved for next request → 
injected back into Claude API request
@codewithkenzo commented on GitHub (Dec 25, 2025): ## Fix Submitted Created comprehensive fix PR: https://github.com/code-yeongyu/oh-my-opencode/pull/256 This PR addresses all the issues identified: 1. **Namespace conversion** - `transformCandidateThinking()` and `transformAnthropicThinking()` now wrap signatures in `providerMetadata.anthropic.signature` format that AI SDK expects 2. **Streaming signature capture** - Added `extractSignatureFromStreamChunk()` to capture signatures from SSE chunks during streaming, stored via `setThoughtSignature()` on stream flush 3. **Message converter** - `convertContentToParts()` now handles `thinking`, `redacted_thinking`, and `reasoning` parts with proper signature preservation The fix ensures signatures flow correctly through the entire pipeline: ``` Antigravity Response → thoughtSignature captured → wrapped in providerMetadata.anthropic.signature → OpenCode stores in metadata → retrieved for next request → injected back into Claude API request ```
Author
Owner

@rekram1-node commented on GitHub (Dec 25, 2025):

Im going to close the issue here since it isn't an opencode issue

@rekram1-node commented on GitHub (Dec 25, 2025): Im going to close the issue here since it isn't an opencode issue
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#3872