From eb3c4c82feb32b330c9fbd326a195a9b94ad1432 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 18 Aug 2026 00:22:19 -0500 Subject: [PATCH] fix(ai): support newer Anthropic system updates (#43180) --- .../ai/src/protocols/anthropic-messages.ts | 17 ++++-- .../test/provider/anthropic-messages.test.ts | 55 +++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/protocols/anthropic-messages.ts b/packages/ai/src/protocols/anthropic-messages.ts index 04cedf5999..65b405cc92 100644 --- a/packages/ai/src/protocols/anthropic-messages.ts +++ b/packages/ai/src/protocols/anthropic-messages.ts @@ -437,10 +437,19 @@ const lowerToolResultContent = Effect.fnUntraced(function* (part: ToolResultPart return yield* Effect.forEach(content, lowerToolResultContentItem) }) -// Mid-conversation system messages are a native Claude API feature only for -// Opus 4.8. Other Anthropic models intentionally use the same visible wrapped- -// user fallback as non-Anthropic routes rather than sending a role they reject. -const supportsNativeSystemUpdates = (request: LLMRequest) => String(request.model.id) === "claude-opus-4-8" +// Mid-conversation system messages became available with Opus 4.8 and version +// 5 of the other supported Claude families. Treat later family versions as +// compatible without assuming that every Anthropic Messages model is Claude. +const supportsNativeSystemUpdates = (request: LLMRequest) => { + const match = /(?:^|[./])claude-(fable|haiku|mythos|opus|sonnet)-(\d+)(?:[.-](\d+))?/.exec( + String(request.model.id).toLowerCase(), + ) + if (!match) return false + const major = Number(match[2]) + if (match[1] !== "opus") return major >= 5 + if (major !== 4) return major >= 5 + return match[3] !== undefined && match[3].length <= 2 && Number(match[3]) >= 8 +} const endsInServerToolUse = (message: LLMRequest["messages"][number]) => { const last = message.content.at(-1) diff --git a/packages/ai/test/provider/anthropic-messages.test.ts b/packages/ai/test/provider/anthropic-messages.test.ts index f4e2fc7dcf..8c26fb5fd6 100644 --- a/packages/ai/test/provider/anthropic-messages.test.ts +++ b/packages/ai/test/provider/anthropic-messages.test.ts @@ -136,6 +136,33 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("supports native chronological system updates on documented and later Claude family versions", () => + Effect.gen(function* () { + const ids = [ + "claude-opus-4-8", + "claude-opus-5-1", + "claude-sonnet-5", + "claude-haiku-5-1", + "claude-fable-6", + "anthropic/claude-mythos-7.2", + ] + + const prepared = yield* Effect.forEach(ids, (id) => + compileRequest( + LLM.request({ + model: AnthropicMessages.route + .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) + .model({ id }), + messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], + cache: "none", + }), + ), + ) + + expect(prepared.map((item) => item.body.messages[1]?.role)).toEqual(ids.map(() => "system")) + }), + ) + it.effect("lowers chronological system updates to wrapped user text for unsupported Anthropic models", () => Effect.gen(function* () { const prepared = yield* compileRequest( @@ -163,6 +190,34 @@ describe("Anthropic Messages route", () => { }), ) + it.effect("does not infer native system update support for older or undocumented Claude families", () => + Effect.gen(function* () { + const ids = [ + "claude-opus-4-7", + "claude-opus-4-20250514", + "claude-sonnet-4-9", + "claude-haiku-4-9", + "custom-model-7", + ] + + const prepared = yield* Effect.forEach(ids, (id) => + compileRequest( + LLM.request({ + model: AnthropicMessages.route + .with({ endpoint: { baseURL: "https://api.anthropic.test/v1/" }, auth: Auth.header("x-api-key", "test") }) + .model({ id }), + messages: [Message.user("Before."), Message.system("Update."), Message.assistant("After.")], + cache: "none", + }), + ), + ) + + expect(prepared.map((item) => item.body.messages.some((message) => message.role === "system"))).toEqual( + ids.map(() => false), + ) + }), + ) + it.effect("rejects non-text chronological system update content before send", () => Effect.gen(function* () { const error = yield* compileRequest(