Skip to content

Commit 1385351

Browse files
committed
fix: strip trailing text-only assistant when thinking rejects prefill
Some models (Opus 4.6) reject assistant-last messages as unsupported prefill when thinking is enabled. During tool-call loops, multi-step assistant messages can produce a trailing text-only model message after toModelMessages conversion. The loop continues (finish=tool-calls) but the next request ends with assistant. Strip trailing text-only assistant messages at the end of ProviderTransform.message() when options.thinking is set.
1 parent f954854 commit 1385351

File tree

2 files changed

+74
-0
lines changed

2 files changed

+74
-0
lines changed

packages/opencode/src/provider/transform.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ export namespace ProviderTransform {
322322
})
323323
}
324324

325+
if (options.thinking) {
326+
while (msgs.length > 0) {
327+
const last = msgs[msgs.length - 1]
328+
if (last.role !== "assistant") break
329+
if (typeof last.content === "string") { msgs = msgs.slice(0, -1); continue }
330+
if (!Array.isArray(last.content)) break
331+
if (!last.content.every((p) => p.type === "text")) break
332+
msgs = msgs.slice(0, -1)
333+
}
334+
}
335+
325336
return msgs
326337
}
327338

packages/opencode/test/provider/transform.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,69 @@ describe("ProviderTransform.message - anthropic empty content filtering", () =>
12141214
expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
12151215
})
12161216

1217+
test("removes trailing text-only assistant when thinking is active", () => {
1218+
const msgs = [
1219+
{ role: "user", content: [{ type: "text", text: "Run ls" }] },
1220+
{ role: "assistant", content: [{ type: "tool-call", toolCallId: "t1", toolName: "bash", args: {} }] },
1221+
{ role: "tool", content: [{ type: "tool-result", toolCallId: "t1", result: "file.txt" }] },
1222+
{ role: "assistant", content: [{ type: "text", text: "Now I will edit." }] },
1223+
] as any[]
1224+
1225+
const result = ProviderTransform.message(msgs, anthropicModel, { thinking: { type: "enabled", budgetTokens: 10000 } })
1226+
1227+
expect(result).toHaveLength(3)
1228+
expect(result[result.length - 1].role).toBe("tool")
1229+
})
1230+
1231+
test("keeps trailing text-only assistant when thinking is NOT active", () => {
1232+
const msgs = [
1233+
{ role: "user", content: [{ type: "text", text: "Run ls" }] },
1234+
{ role: "assistant", content: [{ type: "text", text: "Planning..." }] },
1235+
] as any[]
1236+
1237+
const result = ProviderTransform.message(msgs, anthropicModel, {})
1238+
1239+
expect(result).toHaveLength(2)
1240+
expect(result[result.length - 1].role).toBe("assistant")
1241+
})
1242+
1243+
test("does not remove assistant with tool calls even when thinking is active", () => {
1244+
const msgs = [
1245+
{ role: "user", content: [{ type: "text", text: "Run ls" }] },
1246+
{ role: "assistant", content: [{ type: "text", text: "Let me check" }, { type: "tool-call", toolCallId: "t1", toolName: "bash", args: {} }] },
1247+
] as any[]
1248+
1249+
const result = ProviderTransform.message(msgs, anthropicModel, { thinking: { type: "enabled", budgetTokens: 10000 } })
1250+
1251+
expect(result).toHaveLength(2)
1252+
expect(result[result.length - 1].role).toBe("assistant")
1253+
})
1254+
1255+
test("removes consecutive trailing text-only assistants when thinking is active", () => {
1256+
const msgs = [
1257+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
1258+
{ role: "assistant", content: [{ type: "text", text: "Step 1" }] },
1259+
{ role: "assistant", content: [{ type: "text", text: "Step 2" }] },
1260+
] as any[]
1261+
1262+
const result = ProviderTransform.message(msgs, anthropicModel, { thinking: { type: "adaptive" } })
1263+
1264+
expect(result).toHaveLength(1)
1265+
expect(result[0].role).toBe("user")
1266+
})
1267+
1268+
test("does not remove assistant with reasoning blocks even when thinking is active", () => {
1269+
const msgs = [
1270+
{ role: "user", content: [{ type: "text", text: "Hello" }] },
1271+
{ role: "assistant", content: [{ type: "reasoning", text: "thinking...", providerOptions: { anthropic: { signature: "sig" } } }, { type: "text", text: "Answer" }] },
1272+
] as any[]
1273+
1274+
const result = ProviderTransform.message(msgs, anthropicModel, { thinking: { type: "enabled", budgetTokens: 10000 } })
1275+
1276+
expect(result).toHaveLength(2)
1277+
expect(result[result.length - 1].role).toBe("assistant")
1278+
})
1279+
12171280
test("filters empty content for bedrock provider", () => {
12181281
const bedrockModel = {
12191282
...anthropicModel,

0 commit comments

Comments
 (0)