Skip to content

cersei-types/provider: no signature_delta stream event → Anthropic thinking blocks round-trip with empty signature → HTTP 400 on turn 2 #21

Description

@RobbieMcKinstry

Crates: cersei-types (missing stream event) + cersei-provider (parser drops it) Version: 0.1.9
AI disclosure: Claude Opus 4.8 high effort was used to write this issue, but I am indeed a real human who encountered this bug and will respond to comments on the issue.

Summary

When Anthropic extended thinking is enabled and the assistant uses tools, each thinking block comes back with a cryptographic signature that must be echoed back unchanged on the next turn. cersei never captures that signature off the stream, so the thinking block it sends back on the second turn has an empty signature, and the API rejects the conversation.

Error (second turn, first tool-using turn)

HTTP 400: {"type":"error","error":{"type":"invalid_request_error",
"message":"messages.1.content.0: Invalid `signature` in `thinking` block"}}

Root cause

The data model in cersei-types can hold a signature:

// crates/cersei-types/src/lib.rs
pub enum ContentBlock {
    // ...
    Thinking { thinking: String, signature: String },
    // ...
}

…but the streaming layer has no way to deliver one. StreamEvent only has a thinking-text delta:

// crates/cersei-types/src/lib.rs
pub enum StreamEvent {
    // ...
    ThinkingDelta { index: usize, thinking: String },
    // no SignatureDelta variant
}

Anthropic streams the signature as a separate content_block_delta of type: "signature_delta". The provider's SSE parser doesn't handle it — it falls through to _ => None and is discarded:

// crates/cersei-provider/src/anthropic.rs
"content_block_delta" => {
    match delta_type {
        "text_delta" => Some(StreamEvent::TextDelta { .. }),
        "input_json_delta" => Some(StreamEvent::InputJsonDelta { .. }),
        "thinking_delta" => Some(StreamEvent::ThinkingDelta { .. }),
        _ => None,   // "signature_delta" lands here and is dropped
    }
}

So the accumulated ContentBlock::Thinking.signature stays empty, and when the agent loop re-sends history on the next turn the API rejects the unsigned thinking block.

Impact

Any multi-turn Anthropic conversation with extended thinking + tool use (i.e. the normal agentic loop in cersei-agent) fails on the second turn. Effectively, extended thinking can't be used with tools.

Suggested fix

  1. Add a StreamEvent::SignatureDelta { index, signature } variant in cersei-types.
  2. Parse "signature_delta" in cersei-provider's content_block_delta handler and emit it.
  3. Accumulate it onto the corresponding ContentBlock::Thinking { signature } so it round-trips unchanged on subsequent requests.

(Anthropic also supports redacted_thinking blocks, which carry an opaque data field that must be preserved the same way.)

Environment

  • cersei-types / cersei-provider / cersei-agent 0.1.9
  • Anthropic API, model claude-sonnet-4-6, thinking enabled with tool use

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions