Skip to content

Commit 876c79f

Browse files
omer-korenedevil
authored andcommitted
fix: extend empty-text gate to Bedrock and Vertex signature namespaces
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.
1 parent ed9eb70 commit 876c79f

2 files changed

Lines changed: 60 additions & 2 deletions

File tree

packages/opencode/src/session/message-v2.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,11 +854,17 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
854854
role: "assistant",
855855
parts: [],
856856
}
857-
// Substitute a space for empty text between signed Anthropic reasoning
857+
// Substitute a space for empty text between signed reasoning
858858
// blocks to keep thinking block positions (and signatures) valid without
859859
// triggering Anthropic's empty-content rejection or the AI SDK's filter.
860+
// Signatures live under the provider-namespaced metadata key: anthropic
861+
// (direct API), bedrock (AWS Bedrock), or vertex (GCP Vertex AI).
860862
const hasSignedReasoning = msg.parts.some(
861-
(p) => p.type === "reasoning" && (p.metadata as any)?.anthropic?.signature != null,
863+
(p) =>
864+
p.type === "reasoning" &&
865+
((p.metadata as any)?.anthropic?.signature != null ||
866+
(p.metadata as any)?.bedrock?.signature != null ||
867+
(p.metadata as any)?.vertex?.signature != null),
862868
)
863869
for (const part of msg.parts) {
864870
if (part.type === "text") {

packages/opencode/test/session/message-v2.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,58 @@ describe("session.message-v2.toModelMessage", () => {
11261126
expect((result[1].content as any[]).find((p) => p.type === "text").text).toBe("the answer")
11271127
})
11281128

1129+
test("substitutes space for empty text when reasoning signature is under 'bedrock' namespace", async () => {
1130+
// AWS Bedrock hosts Anthropic Claude but stores signatures under metadata.bedrock
1131+
const assistantID = "m-assistant-bedrock"
1132+
const input: MessageV2.WithParts[] = [
1133+
{
1134+
info: assistantInfo(assistantID, "m-parent"),
1135+
parts: [
1136+
{
1137+
...basePart(assistantID, "p1"),
1138+
type: "reasoning",
1139+
text: "thinking-bedrock",
1140+
metadata: { bedrock: { signature: "bedrock-sig" } },
1141+
},
1142+
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
1143+
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
1144+
] as MessageV2.Part[],
1145+
},
1146+
]
1147+
1148+
const result = await MessageV2.toModelMessages(input, model)
1149+
1150+
expect(result).toHaveLength(1)
1151+
const texts = (result[0].content as any[]).filter((p) => p.type === "text")
1152+
expect(texts.map((t) => t.text)).toStrictEqual([" ", "answer"])
1153+
})
1154+
1155+
test("substitutes space for empty text when reasoning signature is under 'vertex' namespace", async () => {
1156+
// GCP Vertex AI hosts Anthropic Claude but stores signatures under metadata.vertex
1157+
const assistantID = "m-assistant-vertex"
1158+
const input: MessageV2.WithParts[] = [
1159+
{
1160+
info: assistantInfo(assistantID, "m-parent"),
1161+
parts: [
1162+
{
1163+
...basePart(assistantID, "p1"),
1164+
type: "reasoning",
1165+
text: "thinking-vertex",
1166+
metadata: { vertex: { signature: "vertex-sig" } },
1167+
},
1168+
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
1169+
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
1170+
] as MessageV2.Part[],
1171+
},
1172+
]
1173+
1174+
const result = await MessageV2.toModelMessages(input, model)
1175+
1176+
expect(result).toHaveLength(1)
1177+
const texts = (result[0].content as any[]).filter((p) => p.type === "text")
1178+
expect(texts.map((t) => t.text)).toStrictEqual([" ", "answer"])
1179+
})
1180+
11291181
test("leaves empty text alone when reasoning has no Anthropic signature", async () => {
11301182
// Non-Anthropic providers' reasoning doesn't position-validate, so empty text
11311183
// should be filtered normally rather than substituted.

0 commit comments

Comments
 (0)