Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> | undefined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔵 LOW] According to the review checklist, avoid using the any type without providing an explanatory comment. If the exact type is unknown, consider using unknown (e.g., Record<string, unknown>), or add a comment explaining why any is necessary.

Suggested change:

Suggested change
const options = part.providerOptions as Record<string, any> | undefined
const options = part.providerOptions as Record<string, unknown> | undefined

const metadata = options?.[model.providerID] ?? options?.bedrock
return (
metadata?.signature != null || metadata?.redactedContent != null || metadata?.redactedData != null
)
Comment on lines +146 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[🔵 LOW] The review checklist strictly prohibits the use of non-strict equality operators (!=). Please replace != null with strict inequality checks (!== null and/or !== undefined).

Suggested change:

Suggested change
return (
metadata?.signature != null || metadata?.redactedContent != null || metadata?.redactedData != null
)
return (
(metadata?.signature !== undefined && metadata?.signature !== null) ||
(metadata?.redactedContent !== undefined && metadata?.redactedContent !== null) ||
(metadata?.redactedData !== undefined && 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
Expand Down
137 changes: 136 additions & 1 deletion packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<typeof fetch>) => {
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: ModelMessage["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: ModelMessage["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,
Expand Down
Loading