From 69bcc4aeab5e62bb61da17560b168af3ae66da66 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:25:34 -0400 Subject: [PATCH] Support Claude Opus 4.7 via adaptive thinking provider options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opus 4.7 rejects the old thinking.type: "enabled" + budgetTokens shape with a 400 error and requires thinking.type: "adaptive" + effort. Adaptive mode is also preferred (but not required) for Opus 4.6 and Sonnet 4.6, while older Anthropic models (Sonnet 4.5, Opus 4.5, Haiku 3.x) only support the legacy enabled shape. Extract a model-aware helper that returns the correct AnthropicProviderOptions shape for each model instead of hardcoding one config for every Anthropic model. Legacy config remains the safe default for unknown or non-Anthropic model IDs. Upgrade @ai-sdk/anthropic 3.0.13 → 3.0.70 so the SDK schema accepts the adaptive discriminant and the top-level effort parameter. Verified via changelog review that 3.0.14 → 3.0.70 contains no breaking changes (all patch-level additions) and that the ai-v6 track is unaffected. Verification: - All 1906 existing tests pass after the SDK bump - 9 new unit tests for the helper (adaptive / legacy / defensive branches) - 3 new integration tests on getGeneralAgent verifying wiring per model - Lint clean Made-with: Cursor --- .../getAnthropicProviderOptions.test.ts | 78 +++++++++++++++++++ .../__tests__/getGeneralAgent.test.ts | 54 ++++++++++++- .../getAnthropicProviderOptions.ts | 40 ++++++++++ lib/agents/generalAgent/getGeneralAgent.ts | 6 +- package.json | 2 +- pnpm-lock.yaml | 35 +++++++-- 6 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 lib/agents/generalAgent/__tests__/getAnthropicProviderOptions.test.ts create mode 100644 lib/agents/generalAgent/getAnthropicProviderOptions.ts diff --git a/lib/agents/generalAgent/__tests__/getAnthropicProviderOptions.test.ts b/lib/agents/generalAgent/__tests__/getAnthropicProviderOptions.test.ts new file mode 100644 index 000000000..cbbc0d014 --- /dev/null +++ b/lib/agents/generalAgent/__tests__/getAnthropicProviderOptions.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect } from "vitest"; +import { getAnthropicProviderOptions } from "../getAnthropicProviderOptions"; + +describe("getAnthropicProviderOptions", () => { + describe("models that require/prefer adaptive thinking", () => { + it("returns adaptive thinking config for Opus 4.7", () => { + const result = getAnthropicProviderOptions("anthropic/claude-opus-4.7"); + expect(result).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }); + }); + + it("returns adaptive thinking config for Opus 4.6", () => { + const result = getAnthropicProviderOptions("anthropic/claude-opus-4.6"); + expect(result).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }); + }); + + it("returns adaptive thinking config for Sonnet 4.6", () => { + const result = getAnthropicProviderOptions("anthropic/claude-sonnet-4.6"); + expect(result).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }); + }); + + it("omits budgetTokens on the adaptive branch", () => { + const result = getAnthropicProviderOptions("anthropic/claude-opus-4.7"); + expect(result.thinking).not.toHaveProperty("budgetTokens"); + }); + }); + + describe("legacy Anthropic models that only support manual thinking", () => { + it("returns enabled thinking config with budgetTokens for Sonnet 4.5", () => { + const result = getAnthropicProviderOptions("anthropic/claude-sonnet-4.5"); + expect(result).toEqual({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }); + }); + + it("returns enabled thinking config with budgetTokens for Opus 4.5", () => { + const result = getAnthropicProviderOptions("anthropic/claude-opus-4.5"); + expect(result).toEqual({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }); + }); + + it("omits effort parameter on the manual branch", () => { + const result = getAnthropicProviderOptions("anthropic/claude-sonnet-4.5"); + expect(result).not.toHaveProperty("effort"); + }); + }); + + describe("defensive handling of non-Anthropic and unknown model IDs", () => { + it("defaults to enabled thinking config for non-Anthropic model IDs", () => { + // Even though this config won't be applied to non-Anthropic models + // (providerOptions.anthropic is ignored for e.g. OpenAI), the function + // must still return a valid AnthropicProviderOptions shape. + const result = getAnthropicProviderOptions("openai/gpt-5-mini"); + expect(result).toEqual({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }); + }); + + it("defaults to enabled thinking config for unknown/new Anthropic models", () => { + // A future model not yet in our allowlist falls back to the legacy config. + // This is the safe default because every Anthropic model predating 4.6 + // required this shape; the allowlist is explicit for 4.6+ models. + const result = getAnthropicProviderOptions("anthropic/claude-haiku-3.5"); + expect(result).toEqual({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }); + }); + }); +}); diff --git a/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts b/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts index 209b467d5..9918c75cc 100644 --- a/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts +++ b/lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts @@ -466,7 +466,7 @@ describe("getGeneralAgent", () => { expect(typeof result.stopWhen).toBe("function"); }); - it("creates ToolLoopAgent with providerOptions for thinking/reasoning", async () => { + it("creates ToolLoopAgent with providerOptions for thinking/reasoning (default/legacy model path)", async () => { const body: ChatRequestBody = { accountId: "account-123", orgId: null, @@ -478,6 +478,8 @@ describe("getGeneralAgent", () => { // providerOptions should be baked into the agent constructor (stored in settings) const settings = (result.agent as any).settings; expect(settings.providerOptions).toBeDefined(); + // DEFAULT_MODEL is a non-Anthropic model, so the anthropic branch falls + // back to the legacy "enabled" config (safe default for all older models). expect(settings.providerOptions.anthropic).toEqual( expect.objectContaining({ thinking: { type: "enabled", budgetTokens: 12000 }, @@ -499,6 +501,56 @@ describe("getGeneralAgent", () => { ); }); + it("selects adaptive anthropic providerOptions when body.model is Opus 4.7", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + model: "anthropic/claude-opus-4.7", + }; + + const result = await getGeneralAgent(body); + + const settings = (result.agent as any).settings; + expect(settings.providerOptions.anthropic).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }); + }); + + it("selects adaptive anthropic providerOptions when body.model is Sonnet 4.6", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + model: "anthropic/claude-sonnet-4.6", + }; + + const result = await getGeneralAgent(body); + + const settings = (result.agent as any).settings; + expect(settings.providerOptions.anthropic).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }); + }); + + it("keeps legacy enabled anthropic providerOptions for Sonnet 4.5", async () => { + const body: ChatRequestBody = { + accountId: "account-123", + orgId: null, + messages: [{ id: "1", role: "user", content: "Hello" }], + model: "anthropic/claude-sonnet-4.5", + }; + + const result = await getGeneralAgent(body); + + const settings = (result.agent as any).settings; + expect(settings.providerOptions.anthropic).toEqual({ + thinking: { type: "enabled", budgetTokens: 12000 }, + }); + }); + it("creates ToolLoopAgent with prepareStep function", async () => { const body: ChatRequestBody = { accountId: "account-123", diff --git a/lib/agents/generalAgent/getAnthropicProviderOptions.ts b/lib/agents/generalAgent/getAnthropicProviderOptions.ts new file mode 100644 index 000000000..fa8f13d00 --- /dev/null +++ b/lib/agents/generalAgent/getAnthropicProviderOptions.ts @@ -0,0 +1,40 @@ +import { AnthropicProviderOptions } from "@ai-sdk/anthropic"; + +/** + * Models that require or prefer Anthropic's adaptive thinking mode. + * + * - Opus 4.7: adaptive is the ONLY supported mode; manual `type: "enabled"` is rejected. + * - Opus 4.6 / Sonnet 4.6: adaptive is preferred; manual `type: "enabled"` is deprecated. + * + * All older Anthropic models (Sonnet 4.5, Opus 4.5, Haiku 3.x, etc.) do NOT support + * adaptive and must continue using `type: "enabled"` with `budgetTokens`. + * + * Reference: https://docs.claude.com/en/docs/build-with-claude/adaptive-thinking + */ +const ADAPTIVE_THINKING_MODELS = new Set([ + "anthropic/claude-opus-4.7", + "anthropic/claude-opus-4.6", + "anthropic/claude-sonnet-4.6", +]); + +/** + * Returns Anthropic provider options shaped correctly for the given model. + * + * The shape differs between modern (4.6+) and legacy Anthropic models because + * Anthropic changed its thinking API. See `ADAPTIVE_THINKING_MODELS` above. + * + * @param modelId - Full model ID from the AI Gateway (e.g., "anthropic/claude-opus-4.7") + * @returns Provider options valid for the given model. Unknown or non-Anthropic + * model IDs fall back to the legacy shape, which is a safe default. + */ +export function getAnthropicProviderOptions(modelId: string): AnthropicProviderOptions { + if (ADAPTIVE_THINKING_MODELS.has(modelId)) { + return { + thinking: { type: "adaptive", display: "summarized" }, + effort: "medium", + }; + } + return { + thinking: { type: "enabled", budgetTokens: 12000 }, + }; +} diff --git a/lib/agents/generalAgent/getGeneralAgent.ts b/lib/agents/generalAgent/getGeneralAgent.ts index 7c2c9407b..6362d3800 100644 --- a/lib/agents/generalAgent/getGeneralAgent.ts +++ b/lib/agents/generalAgent/getGeneralAgent.ts @@ -1,5 +1,4 @@ import { stepCountIs, ToolLoopAgent } from "ai"; -import { AnthropicProviderOptions } from "@ai-sdk/anthropic"; import { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"; import { OpenAIResponsesProviderOptions } from "@ai-sdk/openai"; import { DEFAULT_MODEL } from "@/lib/const"; @@ -14,6 +13,7 @@ import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo import { getKnowledgeBaseText } from "@/lib/files/getKnowledgeBaseText"; import { getAccountWithDetails } from "@/lib/supabase/accounts/getAccountWithDetails"; import getPrepareStepResult from "@/lib/chat/toolChains/getPrepareStepResult"; +import { getAnthropicProviderOptions } from "@/lib/agents/generalAgent/getAnthropicProviderOptions"; /** * Gets the general agent for the chat @@ -65,9 +65,7 @@ export default async function getGeneralAgent(body: ChatRequestBody): Promise=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -253,6 +253,12 @@ packages: effect: optional: true + '@ai-sdk/provider-utils@4.0.23': + resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@4.0.6': resolution: {integrity: sha512-o/SP1GQOrpXAzHjMosPHI0Pu+YkwxIMndSjSLrEXtcVixdrjqrGaA9I7xJcWf+XpRFJ9byPHrKYnprwS+36gMg==} engines: {node: '>=18'} @@ -275,6 +281,10 @@ packages: resolution: {integrity: sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==} engines: {node: '>=18'} + '@ai-sdk/provider@3.0.8': + resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} + engines: {node: '>=18'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -6607,10 +6617,10 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} - '@ai-sdk/anthropic@3.0.13(zod@4.1.13)': + '@ai-sdk/anthropic@3.0.70(zod@4.1.13)': dependencies: - '@ai-sdk/provider': 3.0.3 - '@ai-sdk/provider-utils': 4.0.6(zod@4.1.13) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.23(zod@4.1.13) zod: 4.1.13 '@ai-sdk/gateway@2.0.0-beta.66(zod@4.1.13)': @@ -6664,6 +6674,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.1.13 + '@ai-sdk/provider-utils@4.0.23(zod@4.1.13)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 4.1.13 + '@ai-sdk/provider-utils@4.0.6(zod@4.1.13)': dependencies: '@ai-sdk/provider': 3.0.3 @@ -6687,6 +6704,10 @@ snapshots: dependencies: json-schema: 0.4.0 + '@ai-sdk/provider@3.0.8': + dependencies: + json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} '@apify/consts@2.48.0': {}