Skip to content

fix: extend empty-text gate to Bedrock and Vertex signature namespaces#1

Open
omer-koren wants to merge 13 commits intoedevil:fix/preserve-thinking-block-signaturesfrom
omer-koren:contrib/bedrock-vertex-signature-gate
Open

fix: extend empty-text gate to Bedrock and Vertex signature namespaces#1
omer-koren wants to merge 13 commits intoedevil:fix/preserve-thinking-block-signaturesfrom
omer-koren:contrib/bedrock-vertex-signature-gate

Conversation

@omer-koren
Copy link
Copy Markdown

Small follow-up to your PR that extends the hasSignedReasoning gate to cover AWS Bedrock and GCP Vertex AI. Feel free to squash into your PR, take the ideas, or close — whichever is easiest for you.

The bug

The gate currently only matches direct-Anthropic signatures:

const hasSignedReasoning = msg.parts.some(
  (p) => p.type === "reasoning" && (p.metadata as any)?.anthropic?.signature != null,
)

When Claude is hosted on AWS Bedrock (amazon-bedrock/anthropic.*) or GCP Vertex AI, the reasoning part's metadata stores the signature under a provider-scoped namespace:

  • metadata.bedrock.signature — AWS Bedrock
  • metadata.vertex.signature — GCP Vertex AI

The AI SDK's Anthropic adapter keeps the namespace on the host that answered the call, so the gate never fires for Bedrock/Vertex users. The empty-text substitution is skipped, and on compaction/replay the message still trips Anthropic's:

messages.N.content.M: 'thinking' or 'redacted_thinking' blocks in the latest assistant message cannot be modified.

Repro

On Bedrock Claude Opus 4.7 (amazon-bedrock/anthropic.claude-opus-4-7), inspecting a reasoning part from the SQLite session DB:

{
  "type": "reasoning",
  "text": "...",
  "metadata": { "bedrock": { "signature": "Es4EClkIDRABGAIq..." } }
}

With only the anthropic namespace checked, hasSignedReasoning returns false → empty text parts between signed reasoning blocks remain "" → Anthropic rejects on the next turn or manual compact.

The fix

Extend the check to any of the three provider namespaces:

const hasSignedReasoning = msg.parts.some(
  (p) =>
    p.type === "reasoning" &&
    ((p.metadata as any)?.anthropic?.signature != null ||
      (p.metadata as any)?.bedrock?.signature != null ||
      (p.metadata as any)?.vertex?.signature != null),
)

The "non-Anthropic providers' reasoning doesn't position-validate" rationale from your existing comment still holds — OpenAI Responses, Gemini thinking, Copilot reasoningOpaque don't write under these three keys, so they continue to have empty text filtered normally.

Tests

Two new tests alongside your existing one, covering the Bedrock and Vertex paths. Same structure as leaves empty text alone when reasoning has no Anthropic signature — just flipping the metadata namespace.

  • 29 tests pass locally (bun test test/session/message-v2.test.ts)
  • LSP diagnostics clean on the changed file
  • Production-verified against a real Bedrock Opus 4.7 session that was reliably reproducing the cannot be modified error on manual compact — the error no longer triggers after this change

Verification

The aws-sdk/client-bedrock-runtime adapter and google-cloud/vertexai both preserve the provider namespace on streamed response metadata rather than normalizing to anthropic — so the extension is necessary, not redundant.


Thanks for the original fix — saved us from a persistent production issue. Happy to adjust/squash/close as you prefer.

rekram1-node and others added 13 commits April 22, 2026 00:35
…ng blocks

Anthropic adaptive thinking (Opus 4.6+) emits empty text parts between
thinking blocks. Dropping them invalidates thinking block signatures
('thinking blocks cannot be modified'), but sending them as '' fails
validation ('text content blocks must be non-empty'). Substitute a
single space so the part survives all filters and Anthropic accepts it.

Gated on reasoning having an Anthropic signature (metadata.anthropic.
signature != null) so other providers' reasoning — which don't
position-validate — continue to have empty text filtered normally.

Closes anomalyco#16748
The hasSignedReasoning gate introduced in this PR only matches Anthropic's

direct-API signature path (metadata.anthropic.signature). When Claude is

hosted on AWS Bedrock or GCP Vertex AI, the reasoning part metadata stores

the signature under metadata.bedrock.signature / metadata.vertex.signature

respectively. The gate never fires for those providers, so empty text parts

between signed reasoning blocks are not substituted with a space, and

Anthropic/Bedrock/Vertex still reject the compacted message with:

  messages.N.content.M: 'thinking' or 'redacted_thinking' blocks in the

  latest assistant message cannot be modified

Extend the check to match signatures under any of the three provider

namespaces. Adds two tests covering the Bedrock and Vertex paths.
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.

6 participants