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': {}