Skip to content

Commit 8263d85

Browse files
committed
local: extend empty-text substitution gate to Bedrock and Vertex
PR anomalyco#21370 only checks metadata.anthropic.signature, but Anthropic Claude hosted on AWS Bedrock (amazon-bedrock/anthropic.*) stores signatures under metadata.bedrock.signature, and on GCP Vertex AI under metadata.vertex.signature. Without this extension, the hasSignedReasoning gate never fires for Bedrock/Vertex users, and the empty-text-between-thinking-blocks bug still triggers 'thinking or redacted_thinking blocks in the latest assistant message cannot be modified' on compaction.
1 parent bb79d93 commit 8263d85

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
@@ -859,11 +859,17 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
859859
role: "assistant",
860860
parts: [],
861861
}
862-
// Substitute a space for empty text between signed Anthropic reasoning
862+
// Substitute a space for empty text between signed reasoning
863863
// blocks to keep thinking block positions (and signatures) valid without
864864
// triggering Anthropic's empty-content rejection or the AI SDK's filter.
865+
// Signatures live under the provider-namespaced metadata key: anthropic
866+
// (direct API), bedrock (AWS Bedrock), or vertex (GCP Vertex AI).
865867
const hasSignedReasoning = msg.parts.some(
866-
(p) => p.type === "reasoning" && (p.metadata as any)?.anthropic?.signature != null,
868+
(p) =>
869+
p.type === "reasoning" &&
870+
((p.metadata as any)?.anthropic?.signature != null ||
871+
(p.metadata as any)?.bedrock?.signature != null ||
872+
(p.metadata as any)?.vertex?.signature != null),
867873
)
868874
for (const part of msg.parts) {
869875
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
@@ -1338,6 +1338,58 @@ describe("session.message-v2.toModelMessage", () => {
13381338
expect((result[1].content as any[]).find((p) => p.type === "text").text).toBe("the answer")
13391339
})
13401340

1341+
test("substitutes space for empty text when reasoning signature is under 'bedrock' namespace", async () => {
1342+
// AWS Bedrock hosts Anthropic Claude but stores signatures under metadata.bedrock
1343+
const assistantID = "m-assistant-bedrock"
1344+
const input: MessageV2.WithParts[] = [
1345+
{
1346+
info: assistantInfo(assistantID, "m-parent"),
1347+
parts: [
1348+
{
1349+
...basePart(assistantID, "p1"),
1350+
type: "reasoning",
1351+
text: "thinking-bedrock",
1352+
metadata: { bedrock: { signature: "bedrock-sig" } },
1353+
},
1354+
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
1355+
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
1356+
] as MessageV2.Part[],
1357+
},
1358+
]
1359+
1360+
const result = await MessageV2.toModelMessages(input, model)
1361+
1362+
expect(result).toHaveLength(1)
1363+
const texts = (result[0].content as any[]).filter((p) => p.type === "text")
1364+
expect(texts.map((t) => t.text)).toStrictEqual([" ", "answer"])
1365+
})
1366+
1367+
test("substitutes space for empty text when reasoning signature is under 'vertex' namespace", async () => {
1368+
// GCP Vertex AI hosts Anthropic Claude but stores signatures under metadata.vertex
1369+
const assistantID = "m-assistant-vertex"
1370+
const input: MessageV2.WithParts[] = [
1371+
{
1372+
info: assistantInfo(assistantID, "m-parent"),
1373+
parts: [
1374+
{
1375+
...basePart(assistantID, "p1"),
1376+
type: "reasoning",
1377+
text: "thinking-vertex",
1378+
metadata: { vertex: { signature: "vertex-sig" } },
1379+
},
1380+
{ ...basePart(assistantID, "p2"), type: "text", text: "" },
1381+
{ ...basePart(assistantID, "p3"), type: "text", text: "answer" },
1382+
] as MessageV2.Part[],
1383+
},
1384+
]
1385+
1386+
const result = await MessageV2.toModelMessages(input, model)
1387+
1388+
expect(result).toHaveLength(1)
1389+
const texts = (result[0].content as any[]).filter((p) => p.type === "text")
1390+
expect(texts.map((t) => t.text)).toStrictEqual([" ", "answer"])
1391+
})
1392+
13411393
test("leaves empty text alone when reasoning has no Anthropic signature", async () => {
13421394
// Non-Anthropic providers' reasoning doesn't position-validate, so empty text
13431395
// should be filtered normally rather than substituted.

0 commit comments

Comments
 (0)