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
This commit is contained in:
@@ -149,7 +149,7 @@ export namespace SessionCompaction {
|
|||||||
tools: {},
|
tools: {},
|
||||||
system: [],
|
system: [],
|
||||||
messages: [
|
messages: [
|
||||||
...MessageV2.toModelMessages(input.messages),
|
...MessageV2.toModelMessages(input.messages, model),
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [
|
content: [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ProviderTransform } from "@/provider/transform"
|
|||||||
import { STATUS_CODES } from "http"
|
import { STATUS_CODES } from "http"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { type SystemError } from "bun"
|
import { type SystemError } from "bun"
|
||||||
|
import type { Provider } from "@/provider/provider"
|
||||||
|
|
||||||
export namespace MessageV2 {
|
export namespace MessageV2 {
|
||||||
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
|
||||||
@@ -432,7 +433,7 @@ export namespace MessageV2 {
|
|||||||
})
|
})
|
||||||
export type WithParts = z.infer<typeof WithParts>
|
export type WithParts = z.infer<typeof WithParts>
|
||||||
|
|
||||||
export function toModelMessages(input: WithParts[]): ModelMessage[] {
|
export function toModelMessages(input: WithParts[], model: Provider.Model): ModelMessage[] {
|
||||||
const result: UIMessage[] = []
|
const result: UIMessage[] = []
|
||||||
|
|
||||||
for (const msg of input) {
|
for (const msg of input) {
|
||||||
@@ -476,6 +477,8 @@ export namespace MessageV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (msg.info.role === "assistant") {
|
if (msg.info.role === "assistant") {
|
||||||
|
const differentModel = `${model.providerID}/${model.api.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
|
||||||
|
|
||||||
if (
|
if (
|
||||||
msg.info.error &&
|
msg.info.error &&
|
||||||
!(
|
!(
|
||||||
@@ -495,7 +498,7 @@ export namespace MessageV2 {
|
|||||||
assistantMessage.parts.push({
|
assistantMessage.parts.push({
|
||||||
type: "text",
|
type: "text",
|
||||||
text: part.text,
|
text: part.text,
|
||||||
providerMetadata: part.metadata,
|
...(differentModel ? {} : { providerMetadata: part.metadata }),
|
||||||
})
|
})
|
||||||
if (part.type === "step-start")
|
if (part.type === "step-start")
|
||||||
assistantMessage.parts.push({
|
assistantMessage.parts.push({
|
||||||
@@ -527,7 +530,7 @@ export namespace MessageV2 {
|
|||||||
toolCallId: part.callID,
|
toolCallId: part.callID,
|
||||||
input: part.state.input,
|
input: part.state.input,
|
||||||
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
|
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
|
||||||
callProviderMetadata: part.metadata,
|
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (part.state.status === "error")
|
if (part.state.status === "error")
|
||||||
@@ -537,7 +540,7 @@ export namespace MessageV2 {
|
|||||||
toolCallId: part.callID,
|
toolCallId: part.callID,
|
||||||
input: part.state.input,
|
input: part.state.input,
|
||||||
errorText: part.state.error,
|
errorText: part.state.error,
|
||||||
callProviderMetadata: part.metadata,
|
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
|
||||||
})
|
})
|
||||||
// Handle pending/running tool calls to prevent dangling tool_use blocks
|
// Handle pending/running tool calls to prevent dangling tool_use blocks
|
||||||
// Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
|
// Anthropic/Claude APIs require every tool_use to have a corresponding tool_result
|
||||||
@@ -548,14 +551,14 @@ export namespace MessageV2 {
|
|||||||
toolCallId: part.callID,
|
toolCallId: part.callID,
|
||||||
input: part.state.input,
|
input: part.state.input,
|
||||||
errorText: "[Tool execution was interrupted]",
|
errorText: "[Tool execution was interrupted]",
|
||||||
callProviderMetadata: part.metadata,
|
...(differentModel ? {} : { callProviderMetadata: part.metadata }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (part.type === "reasoning") {
|
if (part.type === "reasoning") {
|
||||||
assistantMessage.parts.push({
|
assistantMessage.parts.push({
|
||||||
type: "reasoning",
|
type: "reasoning",
|
||||||
text: part.text,
|
text: part.text,
|
||||||
providerMetadata: part.metadata,
|
...(differentModel ? {} : { providerMetadata: part.metadata }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ export namespace SessionPrompt {
|
|||||||
sessionID,
|
sessionID,
|
||||||
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
|
system: [...(await SystemPrompt.environment()), ...(await SystemPrompt.custom())],
|
||||||
messages: [
|
messages: [
|
||||||
...MessageV2.toModelMessages(sessionMessages),
|
...MessageV2.toModelMessages(sessionMessages, model),
|
||||||
...(isLastStep
|
...(isLastStep
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -1778,18 +1778,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
|
|
||||||
const agent = await Agent.get("title")
|
const agent = await Agent.get("title")
|
||||||
if (!agent) return
|
if (!agent) return
|
||||||
|
const model = await iife(async () => {
|
||||||
|
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
|
||||||
|
return (
|
||||||
|
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
||||||
|
)
|
||||||
|
})
|
||||||
const result = await LLM.stream({
|
const result = await LLM.stream({
|
||||||
agent,
|
agent,
|
||||||
user: firstRealUser.info as MessageV2.User,
|
user: firstRealUser.info as MessageV2.User,
|
||||||
system: [],
|
system: [],
|
||||||
small: true,
|
small: true,
|
||||||
tools: {},
|
tools: {},
|
||||||
model: await iife(async () => {
|
model,
|
||||||
if (agent.model) return await Provider.getModel(agent.model.providerID, agent.model.modelID)
|
|
||||||
return (
|
|
||||||
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
sessionID: input.session.id,
|
sessionID: input.session.id,
|
||||||
retries: 2,
|
retries: 2,
|
||||||
@@ -1800,7 +1801,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
},
|
},
|
||||||
...(hasOnlySubtaskParts
|
...(hasOnlySubtaskParts
|
||||||
? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }]
|
? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }]
|
||||||
: MessageV2.toModelMessages(contextMessages)),
|
: MessageV2.toModelMessages(contextMessages, model)),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))
|
const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))
|
||||||
|
|||||||
@@ -1,7 +1,56 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
import { MessageV2 } from "../../src/session/message-v2"
|
||||||
|
import type { Provider } from "../../src/provider/provider"
|
||||||
|
|
||||||
const sessionID = "session"
|
const sessionID = "session"
|
||||||
|
const model: Provider.Model = {
|
||||||
|
id: "test-model",
|
||||||
|
providerID: "test",
|
||||||
|
api: {
|
||||||
|
id: "test-model",
|
||||||
|
url: "https://example.com",
|
||||||
|
npm: "@ai-sdk/openai",
|
||||||
|
},
|
||||||
|
name: "Test Model",
|
||||||
|
capabilities: {
|
||||||
|
temperature: true,
|
||||||
|
reasoning: false,
|
||||||
|
attachment: false,
|
||||||
|
toolcall: true,
|
||||||
|
input: {
|
||||||
|
text: true,
|
||||||
|
audio: false,
|
||||||
|
image: false,
|
||||||
|
video: false,
|
||||||
|
pdf: false,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
text: true,
|
||||||
|
audio: false,
|
||||||
|
image: false,
|
||||||
|
video: false,
|
||||||
|
pdf: false,
|
||||||
|
},
|
||||||
|
interleaved: false,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
cache: {
|
||||||
|
read: 0,
|
||||||
|
write: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
context: 0,
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
},
|
||||||
|
status: "active",
|
||||||
|
options: {},
|
||||||
|
headers: {},
|
||||||
|
release_date: "2026-01-01",
|
||||||
|
}
|
||||||
|
|
||||||
function userInfo(id: string): MessageV2.User {
|
function userInfo(id: string): MessageV2.User {
|
||||||
return {
|
return {
|
||||||
@@ -16,7 +65,13 @@ function userInfo(id: string): MessageV2.User {
|
|||||||
} as unknown as MessageV2.User
|
} as unknown as MessageV2.User
|
||||||
}
|
}
|
||||||
|
|
||||||
function assistantInfo(id: string, parentID: string, error?: MessageV2.Assistant["error"]): MessageV2.Assistant {
|
function assistantInfo(
|
||||||
|
id: string,
|
||||||
|
parentID: string,
|
||||||
|
error?: MessageV2.Assistant["error"],
|
||||||
|
meta?: { providerID: string; modelID: string },
|
||||||
|
): MessageV2.Assistant {
|
||||||
|
const infoModel = meta ?? { providerID: model.providerID, modelID: model.api.id }
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sessionID,
|
sessionID,
|
||||||
@@ -24,8 +79,8 @@ function assistantInfo(id: string, parentID: string, error?: MessageV2.Assistant
|
|||||||
time: { created: 0 },
|
time: { created: 0 },
|
||||||
error,
|
error,
|
||||||
parentID,
|
parentID,
|
||||||
modelID: "model",
|
modelID: infoModel.modelID,
|
||||||
providerID: "provider",
|
providerID: infoModel.providerID,
|
||||||
mode: "",
|
mode: "",
|
||||||
agent: "agent",
|
agent: "agent",
|
||||||
path: { cwd: "/", root: "/" },
|
path: { cwd: "/", root: "/" },
|
||||||
@@ -66,7 +121,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "hello" }],
|
content: [{ type: "text", text: "hello" }],
|
||||||
@@ -91,7 +146,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("includes synthetic text parts", () => {
|
test("includes synthetic text parts", () => {
|
||||||
@@ -122,7 +177,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "hello" }],
|
content: [{ type: "text", text: "hello" }],
|
||||||
@@ -189,7 +244,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [
|
content: [
|
||||||
@@ -259,7 +314,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "run tool" }],
|
content: [{ type: "text", text: "run tool" }],
|
||||||
@@ -305,6 +360,81 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("omits provider metadata when assistant model differs", () => {
|
||||||
|
const userID = "m-user"
|
||||||
|
const assistantID = "m-assistant"
|
||||||
|
|
||||||
|
const input: MessageV2.WithParts[] = [
|
||||||
|
{
|
||||||
|
info: userInfo(userID),
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
...basePart(userID, "u1"),
|
||||||
|
type: "text",
|
||||||
|
text: "run tool",
|
||||||
|
},
|
||||||
|
] as MessageV2.Part[],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: assistantInfo(assistantID, userID, undefined, { providerID: "other", modelID: "other" }),
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
...basePart(assistantID, "a1"),
|
||||||
|
type: "text",
|
||||||
|
text: "done",
|
||||||
|
metadata: { openai: { assistant: "meta" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...basePart(assistantID, "a2"),
|
||||||
|
type: "tool",
|
||||||
|
callID: "call-1",
|
||||||
|
tool: "bash",
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
input: { cmd: "ls" },
|
||||||
|
output: "ok",
|
||||||
|
title: "Bash",
|
||||||
|
metadata: {},
|
||||||
|
time: { start: 0, end: 1 },
|
||||||
|
},
|
||||||
|
metadata: { openai: { tool: "meta" } },
|
||||||
|
},
|
||||||
|
] as MessageV2.Part[],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "run tool" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "done" },
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "call-1",
|
||||||
|
toolName: "bash",
|
||||||
|
input: { cmd: "ls" },
|
||||||
|
providerExecuted: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "tool",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool-result",
|
||||||
|
toolCallId: "call-1",
|
||||||
|
toolName: "bash",
|
||||||
|
output: { type: "text", value: "ok" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
test("replaces compacted tool output with placeholder", () => {
|
test("replaces compacted tool output with placeholder", () => {
|
||||||
const userID = "m-user"
|
const userID = "m-user"
|
||||||
const assistantID = "m-assistant"
|
const assistantID = "m-assistant"
|
||||||
@@ -341,7 +471,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "run tool" }],
|
content: [{ type: "text", text: "run tool" }],
|
||||||
@@ -408,7 +538,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
content: [{ type: "text", text: "run tool" }],
|
content: [{ type: "text", text: "run tool" }],
|
||||||
@@ -461,7 +591,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
|
test("includes aborted assistant messages only when they have non-step-start/reasoning content", () => {
|
||||||
@@ -504,7 +634,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
@@ -540,7 +670,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [{ type: "text", text: "first" }],
|
content: [{ type: "text", text: "first" }],
|
||||||
@@ -567,7 +697,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(MessageV2.toModelMessages(input)).toStrictEqual([])
|
expect(MessageV2.toModelMessages(input, model)).toStrictEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
|
test("converts pending/running tool calls to error results to prevent dangling tool_use", () => {
|
||||||
@@ -614,7 +744,7 @@ describe("session.message-v2.toModelMessage", () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const result = MessageV2.toModelMessages(input)
|
const result = MessageV2.toModelMessages(input, model)
|
||||||
|
|
||||||
expect(result).toStrictEqual([
|
expect(result).toStrictEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user