Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 },
});
});
});
});
54 changes: 53 additions & 1 deletion lib/agents/generalAgent/__tests__/getGeneralAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 },
Expand All @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions lib/agents/generalAgent/getAnthropicProviderOptions.ts
Original file line number Diff line number Diff line change
@@ -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<string>([
"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 },
};
}
6 changes: 2 additions & 4 deletions lib/agents/generalAgent/getGeneralAgent.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -65,9 +65,7 @@ export default async function getGeneralAgent(body: ChatRequestBody): Promise<Ro
return options;
},
providerOptions: {
anthropic: {
thinking: { type: "enabled", budgetTokens: 12000 },
} satisfies AnthropicProviderOptions,
anthropic: getAnthropicProviderOptions(model),
google: {
thinkingConfig: {
thinkingBudget: 8192,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"eval": "braintrust eval --external-packages playwright playwright-core chromium-bidi @browserbasehq/stagehand @composio/core @composio/vercel"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.13",
"@ai-sdk/anthropic": "^3.0.70",
"@ai-sdk/gateway": "^3.0.14",
"@ai-sdk/google": "^3.0.8",
"@ai-sdk/mcp": "^0.0.12",
Expand Down
35 changes: 28 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading