From e7cd0ffb781ed28e0c0921a934084263c56bb66f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:55:30 +0000 Subject: [PATCH 1/2] fix(provider): port upstream Bedrock unsigned reasoning filter (anomalyco/opencode#45769) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 14 +- .../opencode/test/provider/transform.test.ts | 137 +++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d0d7ffdfb1..706f7c8cf7 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -137,8 +137,18 @@ export namespace ProviderTransform { } // altimate_change start — preserve signed empty reasoning blocks returned by Anthropic/Bedrock if (part.type === "reasoning") { - const providerKey = model.api.npm === "@ai-sdk/amazon-bedrock" ? "bedrock" : "anthropic" - const opts = (part.providerOptions as any)?.[providerKey] + // altimate_change start — upstream_fix: drop unreplayable Bedrock reasoning before caching (anomalyco/opencode#45769) + // The Bedrock SDK only replays signed or redacted reasoning. Unsigned reasoning must go + // before cache points are assigned, or it can leave an empty or cache-point-only message. + if (model.api.npm === "@ai-sdk/amazon-bedrock") { + const options = part.providerOptions as Record | undefined + const metadata = options?.[model.providerID] ?? options?.bedrock + return ( + metadata?.signature != null || metadata?.redactedContent != null || metadata?.redactedData != null + ) + } + // altimate_change end + const opts = (part.providerOptions as any)?.anthropic return part.text.trim().length > 0 || opts?.signature != null || opts?.redactedData != null } // altimate_change end diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 90e6a8dc0a..2dcbfae699 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" -import { jsonSchema, tool, type ModelMessage, type Tool } from "ai" +import { generateText, jsonSchema, tool, type ModelMessage, type Tool } from "ai" +import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock" import { ProviderTransform } from "@/provider/transform" import { clampOutputTokens, @@ -2231,6 +2232,140 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[1].content[0]).toEqual({ type: "text", text: "Answer" }) }) + // altimate_change start — upstream_fix: drop unreplayable Bedrock reasoning before caching (anomalyco/opencode#45769) + describe("Bedrock reasoning replay", () => { + const model = { + ...anthropicModel, + id: "amazon-bedrock/anthropic.claude-opus-4-6", + providerID: "amazon-bedrock", + api: { + id: "anthropic.claude-opus-4-6", + url: "https://bedrock-runtime.us-east-1.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + } + + for (const cached of [false, true]) { + test(`omits unsigned reasoning before SDK conversion (caching: ${cached})`, async () => { + const selected = cached + ? model + : { ...model, id: "amazon-bedrock/openai.gpt-oss-120b", api: { ...model.api, id: "openai.gpt-oss-120b" } } + const messages = ProviderTransform.message( + [ + { role: "user", content: "Think" }, + { role: "assistant", content: [{ type: "text", text: "Earlier answer" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "Partial thought" }, + { type: "text", text: "" }, + ], + }, + { role: "user", content: "Continue" }, + ] as ModelMessage[], + selected as any, + {}, + ) + expect(messages.map((message) => message.role)).toEqual(["user", "assistant", "user"]) + expect(messages[1].providerOptions?.bedrock?.cachePoint).toEqual(cached ? { type: "default" } : undefined) + const provider = createAmazonBedrock({ + apiKey: "test-key", + region: "us-east-1", + fetch: Object.assign( + async (...args: Parameters) => { + const body = JSON.parse(String(args[1]?.body)) + expect(body.messages).toEqual([ + { role: "user", content: [{ text: "Think" }] }, + { + role: "assistant", + content: [{ text: "Earlier answer" }, ...(cached ? [{ cachePoint: { type: "default" } }] : [])], + }, + { + role: "user", + content: [{ text: "Continue" }, ...(cached ? [{ cachePoint: { type: "default" } }] : [])], + }, + ]) + return Response.json({ + output: { message: { role: "assistant", content: [{ text: "Recovered" }] } }, + stopReason: "end_turn", + usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 }, + }) + }, + { preconnect: () => undefined }, + ) as typeof fetch, + }) + const result = await generateText({ model: provider(selected.api.id), messages, maxRetries: 0 }) + expect(result.text).toBe("Recovered") + }) + } + + for (const namespace of ["bedrock", "amazon-bedrock", "custom-bedrock"]) { + for (const field of ["signature", "redactedContent", "redactedData"]) { + test(`preserves ${namespace}.${field} on empty reasoning`, () => { + const result = ProviderTransform.message( + [ + { + role: "assistant", + content: [{ type: "reasoning", text: "", providerOptions: { [namespace]: { [field]: "opaque" } } }], + }, + ] as ModelMessage[], + (namespace === "custom-bedrock" ? { ...model, providerID: namespace } : model) as any, + {}, + ) + expect(result).toHaveLength(1) + expect(result[0].content).toEqual([ + { type: "reasoning", text: "", providerOptions: { bedrock: { [field]: "opaque" } } }, + ]) + }) + } + } + + test("uses stored provider metadata when it will overwrite the SDK namespace", () => { + const result = ProviderTransform.message( + [ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "Partial thought", + providerOptions: { "amazon-bedrock": {}, bedrock: { signature: "overwritten" } }, + }, + ], + }, + ] as ModelMessage[], + model as any, + {}, + ) + expect(result).toEqual([]) + }) + + test("keeps text and tool calls next to unsigned reasoning", () => { + const content = [ + { type: "text", text: "Answer" }, + { type: "tool-call", toolCallId: "call_1", toolName: "lookup", input: {} }, + ] + const result = ProviderTransform.message( + [{ role: "assistant", content: [{ type: "reasoning", text: "Partial thought" }, ...content] }] as ModelMessage[], + model as any, + {}, + ) + expect(result).toHaveLength(1) + expect(result[0].content).toEqual(content) + }) + + test("does not remove unsigned reasoning for other providers", () => { + const content = [{ type: "reasoning", text: "Partial thought" }] + const result = ProviderTransform.message( + [{ role: "assistant", content }] as ModelMessage[], + anthropicModel as any, + {}, + ) + expect(result[0].content).toEqual(content) + }) + }) + // altimate_change end + test("does not filter for non-anthropic providers", () => { const openaiModel = { ...anthropicModel, From d5f94698007e164827825ee76163a1a651f58963 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:56:47 +0000 Subject: [PATCH 2/2] test(provider): type Bedrock reasoning replay fixtures Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/opencode/test/provider/transform.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2dcbfae699..111b1cb0d8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -2341,7 +2341,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => }) test("keeps text and tool calls next to unsigned reasoning", () => { - const content = [ + const content: ModelMessage["content"] = [ { type: "text", text: "Answer" }, { type: "tool-call", toolCallId: "call_1", toolName: "lookup", input: {} }, ] @@ -2355,7 +2355,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => }) test("does not remove unsigned reasoning for other providers", () => { - const content = [{ type: "reasoning", text: "Partial thought" }] + const content: ModelMessage["content"] = [{ type: "reasoning", text: "Partial thought" }] const result = ProviderTransform.message( [{ role: "assistant", content }] as ModelMessage[], anthropicModel as any,