Skip to content

Commit 021e42c

Browse files
committed
core: fix issue when switching models (mainly between providers) where past reasoning/metadata would be sent to server and cause 400 errors since they came from another account/provider
1 parent 0c4ffec commit 021e42c

4 files changed

Lines changed: 164 additions & 30 deletions

File tree

packages/opencode/src/session/compaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export namespace SessionCompaction {
149149
tools: {},
150150
system: [],
151151
messages: [
152-
...MessageV2.toModelMessages(input.messages),
152+
...MessageV2.toModelMessages(input.messages, model),
153153
{
154154
role: "user",
155155
content: [

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ProviderTransform } from "@/provider/transform"
1111
import { STATUS_CODES } from "http"
1212
import { iife } from "@/util/iife"
1313
import { type SystemError } from "bun"
14+
import type { Provider } from "@/provider/provider"
1415

1516
export namespace MessageV2 {
1617
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
@@ -432,7 +433,7 @@ export namespace MessageV2 {
432433
})
433434
export type WithParts = z.infer<typeof WithParts>
434435

435-
export function toModelMessages(input: WithParts[]): ModelMessage[] {
436+
export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
436437
const result: UIMessage[] = []
437438

438439
for (const msg of input) {
@@ -476,6 +477,8 @@ export namespace MessageV2 {
476477
}
477478

478479
if (msg.info.role === "assistant") {
480+
const differentModel = `${model.providerID}/${model.api.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
481+
479482
if (
480483
msg.info.error &&
481484
!(
@@ -495,7 +498,7 @@ export namespace MessageV2 {
495498
assistantMessage.parts.push({
496499
type: "text",
497500
text: part.text,
498-
providerMetadata: part.metadata,
501+
...(differentModel ? {} : { providerMetadata: part.metadata }),
499502
})
500503
if (part.type === "step-start")
501504
assistantMessage.parts.push({
@@ -527,7 +530,7 @@ export namespace MessageV2 {
527530
toolCallId: part.callID,
528531
input: part.state.input,
529532
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
530-
callProviderMetadata: part.metadata,
533+
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
531534
})
532535
}
533536
if (part.state.status === "error")
@@ -537,7 +540,7 @@ export namespace MessageV2 {
537540
toolCallId: part.callID,
538541
input: part.state.input,
539542
errorText: part.state.error,
540-
callProviderMetadata: part.metadata,
543+
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
541544
})
542545
// Handle pending/running tool calls to prevent dangling tool_use blocks
543546
// Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
@@ -548,14 +551,14 @@ export namespace MessageV2 {
548551
toolCallId: part.callID,
549552
input: part.state.input,
550553
errorText: "[Tool execution was interrupted]",
551-
callProviderMetadata: part.metadata,
554+
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
552555
})
553556
}
554557
if (part.type === "reasoning") {
555558
assistantMessage.parts.push({
556559
type: "reasoning",
557560
text: part.text,
558-
providerMetadata: part.metadata,
561+
...(differentModel ? {} : { providerMetadata: part.metadata }),
559562
})
560563
}
561564
}

packages/opencode/src/session/prompt.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ export namespace SessionPrompt {
598598
sessionID,
599599
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
600600
messages: [
601-
...MessageV2.toModelMessages(sessionMessages),
601+
...MessageV2.toModelMessages(sessionMessages, model),
602602
...(isLastStep
603603
? [
604604
{
@@ -1778,18 +1778,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17781778

17791779
const agent = await Agent.get("title")
17801780
if (!agent) return
1781+
const model = await iife(async () => {
1782+
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
1783+
return (
1784+
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
1785+
)
1786+
})
17811787
const result = await LLM.stream({
17821788
agent,
17831789
user: firstRealUser.info as MessageV2.User,
17841790
system: [],
17851791
small: true,
17861792
tools: {},
1787-
model: await iife(async () => {
1788-
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
1789-
return (
1790-
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
1791-
)
1792-
}),
1793+
model,
17931794
abort: new AbortController().signal,
17941795
sessionID: input.session.id,
17951796
retries: 2,
@@ -1800,7 +1801,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
18001801
},
18011802
...(hasOnlySubtaskParts
18021803
? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }]
1803-
: MessageV2.toModelMessages(contextMessages)),
1804+
: MessageV2.toModelMessages(contextMessages, model)),
18041805
],
18051806
})
18061807
const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))

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

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,56 @@
11
import { describe, expect, test } from "bun:test"
22
import { MessageV2 } from "../../src/session/message-v2"
3+
import type { Provider } from "../../src/provider/provider"
34

45
const sessionID = "session"
6+
const model: Provider.Model = {
7+
id: "test-model",
8+
providerID: "test",
9+
api: {
10+
id: "test-model",
11+
url: "https://example.com",
12+
npm: "@ai-sdk/openai",
13+
},
14+
name: "Test Model",
15+
capabilities: {
16+
temperature: true,
17+
reasoning: false,
18+
attachment: false,
19+
toolcall: true,
20+
input: {
21+
text: true,
22+
audio: false,
23+
image: false,
24+
video: false,
25+
pdf: false,
26+
},
27+
output: {
28+
text: true,
29+
audio: false,
30+
image: false,
31+
video: false,
32+
pdf: false,
33+
},
34+
interleaved: false,
35+
},
36+
cost: {
37+
input: 0,
38+
output: 0,
39+
cache: {
40+
read: 0,
41+
write: 0,
42+
},
43+
},
44+
limit: {
45+
context: 0,
46+
input: 0,
47+
output: 0,
48+
},
49+
status: "active",
50+
options: {},
51+
headers: {},
52+
release_date: "2026-01-01",
53+
}
554

655
function userInfo(id: string): MessageV2.User {
756
return {
@@ -16,16 +65,22 @@ function userInfo(id: string): MessageV2.User {
1665
} as unknown as MessageV2.User
1766
}
1867

19-
function assistantInfo(id: string, parentID: string, error?: MessageV2.Assistant["error"]): MessageV2.Assistant {
68+
function assistantInfo(
69+
id: string,
70+
parentID: string,
71+
error?: MessageV2.Assistant["error"],
72+
meta?: { providerID: string; modelID: string },
73+
): MessageV2.Assistant {
74+
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
2075
return {
2176
id,
2277
sessionID,
2378
role: "assistant",
2479
time: { created: 0 },
2580
error,
2681
parentID,
27-
modelID: "model",
28-
providerID: "provider",
82+
modelID: infoModel.modelID,
83+
providerID: infoModel.providerID,
2984
mode: "",
3085
agent: "agent",
3186
path: { cwd: "/", root: "/" },
@@ -66,7 +121,7 @@ describe("session.message-v2.toModelMessage", () => {
66121
},
67122
]
68123

69-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
124+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
70125
{
71126
role: "user",
72127
content: [{ type: "text", text: "hello" }],
@@ -91,7 +146,7 @@ describe("session.message-v2.toModelMessage", () => {
91146
},
92147
]
93148

94-
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
149+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
95150
})
96151

97152
test("includes synthetic text parts", () => {
@@ -122,7 +177,7 @@ describe("session.message-v2.toModelMessage", () => {
122177
},
123178
]
124179

125-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
180+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
126181
{
127182
role: "user",
128183
content: [{ type: "text", text: "hello" }],
@@ -189,7 +244,7 @@ describe("session.message-v2.toModelMessage", () => {
189244
},
190245
]
191246

192-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
247+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
193248
{
194249
role: "user",
195250
content: [
@@ -259,7 +314,7 @@ describe("session.message-v2.toModelMessage", () => {
259314
},
260315
]
261316

262-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
317+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
263318
{
264319
role: "user",
265320
content: [{ type: "text", text: "run tool" }],
@@ -305,6 +360,81 @@ describe("session.message-v2.toModelMessage", () => {
305360
])
306361
})
307362

363+
test("omits provider metadata when assistant model differs", () => {
364+
const userID = "m-user"
365+
const assistantID = "m-assistant"
366+
367+
const input: MessageV2.WithParts[] = [
368+
{
369+
info: userInfo(userID),
370+
parts: [
371+
{
372+
...basePart(userID, "u1"),
373+
type: "text",
374+
text: "run tool",
375+
},
376+
] as MessageV2.Part[],
377+
},
378+
{
379+
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
380+
parts: [
381+
{
382+
...basePart(assistantID, "a1"),
383+
type: "text",
384+
text: "done",
385+
metadata: { openai: { assistant: "meta" } },
386+
},
387+
{
388+
...basePart(assistantID, "a2"),
389+
type: "tool",
390+
callID: "call-1",
391+
tool: "bash",
392+
state: {
393+
status: "completed",
394+
input: { cmd: "ls" },
395+
output: "ok",
396+
title: "Bash",
397+
metadata: {},
398+
time: { start: 0, end: 1 },
399+
},
400+
metadata: { openai: { tool: "meta" } },
401+
},
402+
] as MessageV2.Part[],
403+
},
404+
]
405+
406+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
407+
{
408+
role: "user",
409+
content: [{ type: "text", text: "run tool" }],
410+
},
411+
{
412+
role: "assistant",
413+
content: [
414+
{ type: "text", text: "done" },
415+
{
416+
type: "tool-call",
417+
toolCallId: "call-1",
418+
toolName: "bash",
419+
input: { cmd: "ls" },
420+
providerExecuted: undefined,
421+
},
422+
],
423+
},
424+
{
425+
role: "tool",
426+
content: [
427+
{
428+
type: "tool-result",
429+
toolCallId: "call-1",
430+
toolName: "bash",
431+
output: { type: "text", value: "ok" },
432+
},
433+
],
434+
},
435+
])
436+
})
437+
308438
test("replaces compacted tool output with placeholder", () => {
309439
const userID = "m-user"
310440
const assistantID = "m-assistant"
@@ -341,7 +471,7 @@ describe("session.message-v2.toModelMessage", () => {
341471
},
342472
]
343473

344-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
474+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
345475
{
346476
role: "user",
347477
content: [{ type: "text", text: "run tool" }],
@@ -408,7 +538,7 @@ describe("session.message-v2.toModelMessage", () => {
408538
},
409539
]
410540

411-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
541+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
412542
{
413543
role: "user",
414544
content: [{ type: "text", text: "run tool" }],
@@ -461,7 +591,7 @@ describe("session.message-v2.toModelMessage", () => {
461591
},
462592
]
463593

464-
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
594+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
465595
})
466596

467597
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
@@ -504,7 +634,7 @@ describe("session.message-v2.toModelMessage", () => {
504634
},
505635
]
506636

507-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
637+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
508638
{
509639
role: "assistant",
510640
content: [
@@ -540,7 +670,7 @@ describe("session.message-v2.toModelMessage", () => {
540670
},
541671
]
542672

543-
expect(MessageV2.toModelMessages(input)).toStrictEqual([
673+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
544674
{
545675
role: "assistant",
546676
content: [{ type: "text", text: "first" }],
@@ -567,7 +697,7 @@ describe("session.message-v2.toModelMessage", () => {
567697
},
568698
]
569699

570-
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
700+
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
571701
})
572702

573703
test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
@@ -614,7 +744,7 @@ describe("session.message-v2.toModelMessage", () => {
614744
},
615745
]
616746

617-
const result = MessageV2.toModelMessages(input)
747+
const result = MessageV2.toModelMessages(input, model)
618748

619749
expect(result).toStrictEqual([
620750
{

0 commit comments

Comments
 (0)