diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 07410e08be0..21caca0904a 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -744,9 +744,12 @@ const endsInServerToolUse = (message: LLMRequest["messages"][number]) => { return message.role === "assistant" && last?.type === "tool-call" && last.providerExecuted === true } -const canUseNativeSystemUpdate = (messages: LLMRequest["messages"], index: number) => { - const previous = messages[index - 1] - const next = messages[index + 1] +const canUseNativeSystemUpdate = (request: LLMRequest, index: number) => { + const previous = request.messages[index - 1] + const next = request.messages[index + 1] + // Vertex currently rejects/404s for a system message after local tool results, + // so fold it into the user tool-result turn across continuations and history. + if (request.model.route.id === "google-vertex-messages" && previous?.role === "tool") return false return ( previous !== undefined && previous.role !== "system" && @@ -793,7 +796,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* ( if (message.role === "system") { if (splitsLocalToolResults(request.messages, index)) return yield* invalid("Anthropic Messages system updates cannot split a local tool call from its tool result") - if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request.messages, index)) { + if (supportsNativeSystemUpdates(request) && canUseNativeSystemUpdate(request, index)) { messages.push(yield* lowerNativeSystemUpdate(message, breakpoints)) continue } diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index ca63e5ad0ee..3c5d8d48bc4 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -5,6 +5,7 @@ import { CacheHint, LLM, AIError, LLMRequest, Message, ToolCallPart, ToolDefinit import { Auth, LLMClient } from "../../src/route.js" import { compileRequest } from "../../src/route/client.js" import * as AnthropicMessages from "../../src/protocols/anthropic-messages.js" +import { GoogleVertexMessages } from "../../src/providers.js" import { continuationRequest, nativeAnthropicMessagesContinuation } from "../continuation-scenarios.js" import { it } from "../lib/effect.js" import { dynamicResponse, fixedResponse } from "../lib/http.js" @@ -27,6 +28,12 @@ const compileUnsignedReasoning = (model: LLMRequest["model"]) => }), ) +const vertexOpus48 = GoogleVertexMessages.configure({ + accessToken: "test", + location: "global", + project: "test", +}).model("claude-opus-4-8") + const request = LLM.request({ id: "req_1", model, @@ -286,6 +293,149 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("keeps a terminal Vertex system update in the tool-result turn", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: vertexOpus48, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]), + Message.tool({ id: "call_1", name: "lookup", result: "Done." }), + Message.system("Operator update."), + ], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_1", + content: '"Done."', + is_error: undefined, + cache_control: undefined, + }, + { + type: "text", + text: "\nOperator update.\n", + cache_control: undefined, + }, + ], + }, + ]) + }), + ) + + it.effect("preserves folded tool-result system updates across multi-turn Vertex history", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: vertexOpus48, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]), + Message.tool({ id: "call_1", name: "lookup", result: "Done." }), + Message.system("Operator update."), + Message.assistant("Acknowledged."), + Message.user("Next step."), + ], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_1", + content: '"Done."', + is_error: undefined, + cache_control: undefined, + }, + { + type: "text", + text: "\nOperator update.\n", + cache_control: undefined, + }, + ], + }, + { role: "assistant", content: [{ type: "text", text: "Acknowledged." }] }, + { role: "user", content: [{ type: "text", text: "Next step." }] }, + ]) + }), + ) + + it.effect("keeps a terminal direct Anthropic system update native", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: opus48, + messages: [ + Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]), + Message.tool({ id: "call_1", name: "lookup", result: "Done." }), + Message.system("Operator update."), + ], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { + role: "assistant", + content: [{ type: "tool_use", id: "call_1", name: "lookup", input: {} }], + }, + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "call_1", + content: '"Done."', + is_error: undefined, + cache_control: undefined, + }, + ], + }, + { + role: "system", + content: [{ type: "text", text: "Operator update.", cache_control: undefined }], + }, + ]) + }), + ) + + it.effect("keeps an ordinary terminal Vertex system update native", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model: vertexOpus48, + messages: [Message.user("Before."), Message.system("Operator update.")], + cache: "none", + }), + ) + + expect(prepared.body.messages).toEqual([ + { role: "user", content: [{ type: "text", text: "Before." }] }, + { + role: "system", + content: [{ type: "text", text: "Operator update.", cache_control: undefined }], + }, + ]) + }), + ) + it.effect("rejects a system update between a local tool call and its result", () => Effect.gen(function* () { const error = yield* compileRequest(