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(