From d59fa64cfd0b10bac221bc72d793216fe6b8c27a Mon Sep 17 00:00:00 2001 From: TDFSE <77169190+TDFSE@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:01:33 -0400 Subject: [PATCH] Add advisor functionality and tests --- packages/types/src/model.ts | 1 + packages/types/src/provider-settings.ts | 3 + packages/types/src/providers/anthropic.ts | 3 + src/api/providers/__tests__/anthropic.spec.ts | 135 ++++++++++++++++++ src/api/providers/anthropic.ts | 24 +++- .../settings/providers/Anthropic.tsx | 48 +++++++ webview-ui/src/i18n/locales/en/settings.json | 4 + 7 files changed, 217 insertions(+), 1 deletion(-) diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 926f29a0a1..98fb396188 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -98,6 +98,7 @@ export const modelInfoSchema = z.object({ outputPrice: z.number().optional(), cacheWritesPrice: z.number().optional(), cacheReadsPrice: z.number().optional(), + supportsAdvisorTool: z.boolean().optional(), longContextPricing: z .object({ thresholdTokens: z.number(), diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 43135577e1..1500ab81d4 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -207,6 +207,9 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ anthropicBaseUrl: z.string().optional(), anthropicUseAuthToken: z.boolean().optional(), anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window. + anthropicAdvisorEnabled: z.boolean().optional(), + anthropicAdvisorModel: z.string().optional(), + anthropicAdvisorMaxUses: z.number().int().min(1).optional(), }) const openRouterSchema = baseProviderSettingsSchema.extend({ diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index 40a3d885d8..72ad88ea57 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -17,6 +17,7 @@ export const anthropicModels = { cacheWritesPrice: 3.75, // $3.75 per million tokens cacheReadsPrice: 0.3, // $0.30 per million tokens supportsReasoningBudget: true, + supportsAdvisorTool: true, // Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07') tiers: [ { @@ -80,6 +81,7 @@ export const anthropicModels = { cacheWritesPrice: 6.25, // $6.25 per million tokens cacheReadsPrice: 0.5, // $0.50 per million tokens supportsReasoningBudget: true, + supportsAdvisorTool: true, // Tiered pricing for extended context (requires beta flag) tiers: [ { @@ -196,6 +198,7 @@ export const anthropicModels = { cacheWritesPrice: 1.25, cacheReadsPrice: 0.1, supportsReasoningBudget: true, + supportsAdvisorTool: true, description: "Claude Haiku 4.5 delivers near-frontier intelligence at lightning speeds with extended thinking, vision, and multilingual support.", }, diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 3731f3a068..7dbad50dcd 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -209,6 +209,141 @@ describe("AnthropicHandler", () => { const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] expect(requestOptions?.headers?.["anthropic-beta"]).toContain("context-1m-2025-08-07") }) + + describe("advisor tool feature", () => { + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + ] + + it("should include advisor tool beta header when advisor tool is enabled", async () => { + const advisorHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-4-6", + anthropicAdvisorEnabled: true, + }) + + const stream = advisorHandler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + expect(requestOptions?.headers?.["anthropic-beta"]).toContain("advisor-tool-2026-03-01") + }) + + it("should inject advisor tool definition when advisor tool is enabled", async () => { + const advisorHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-4-6", + anthropicAdvisorEnabled: true, + }) + + const stream = advisorHandler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + expect(callArgs?.tools).toBeDefined() + expect(callArgs?.tools).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: "advisor_20260301", + name: "advisor", + model: "claude-opus-4-6", + }), + ]), + ) + }) + + it("should include max_uses in advisor tool definition when configured", async () => { + const customMaxUses = 5 + const advisorHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-4-6", + anthropicAdvisorEnabled: true, + anthropicAdvisorMaxUses: customMaxUses, + }) + + const stream = advisorHandler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301") + expect(advisorTool).toBeDefined() + expect(advisorTool?.max_uses).toBe(customMaxUses) + }) + + it("should use custom advisor model when configured", async () => { + const customModel = "claude-opus-4-5-20251101" + const advisorHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-4-6", + anthropicAdvisorEnabled: true, + anthropicAdvisorModel: customModel, + }) + + const stream = advisorHandler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301") + expect(advisorTool).toBeDefined() + expect(advisorTool?.model).toBe(customModel) + }) + + it("should not include advisor tool when advisor tool is disabled", async () => { + const stream = handler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301") + expect(advisorTool).toBeUndefined() + + // Also verify beta header is not present + const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + const betaHeader = requestOptions?.headers?.["anthropic-beta"] + if (betaHeader && typeof betaHeader === "string") { + expect(betaHeader).not.toContain("advisor-tool-2026-03-01") + } + }) + + it("should use default advisor model and max_uses when not configured", async () => { + const advisorHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-sonnet-4-6", + anthropicAdvisorEnabled: true, + // No anthropicAdvisorModel or anthropicAdvisorMaxUses provided + }) + + const stream = advisorHandler.createMessage(systemPrompt, messages) + + for await (const _chunk of stream) { + // Consume stream + } + + const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301") + expect(advisorTool).toBeDefined() + expect(advisorTool?.model).toBe("claude-opus-4-6") // default + expect(advisorTool?.max_uses).toBe(3) // default + }) + }) }) describe("completePrompt", () => { diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 1786a105a5..010d0df38d 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -75,8 +75,30 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa betas.push("context-1m-2025-08-07") } + // Add advisor tool beta flag if enabled + if (this.options.anthropicAdvisorEnabled) { + betas.push("advisor-tool-2026-03-01") + } + + let tools = convertOpenAIToolsToAnthropic(metadata?.tools ?? []) + + // Add advisor tool if enabled + if (this.options.anthropicAdvisorEnabled) { + const advisorModel = this.options.anthropicAdvisorModel ?? "claude-opus-4-6" + const maxUses = this.options.anthropicAdvisorMaxUses ?? 3 + const advisorToolDef: { type: string; name: string; model: string; max_uses?: number } = { + type: "advisor_20260301", + name: "advisor", + model: advisorModel, + } + if (maxUses >= 1) { + advisorToolDef.max_uses = maxUses + } + tools.push(advisorToolDef as unknown as Anthropic.Tool) + } + const nativeToolParams = { - tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []), + tools, tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls), } diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index 6417e34b8b..a766914ebb 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -19,6 +19,7 @@ type AnthropicProps = { export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => { const { t } = useAppTranslation() const selectedModel = useSelectedModel(apiConfiguration) + const modelInfo = selectedModel?.info const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) @@ -103,6 +104,53 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro )} + {modelInfo?.supportsAdvisorTool && ( +
+ { + setApiConfigurationField("anthropicAdvisorEnabled", checked) + }}> + {t("settings:providers.anthropicAdvisorToolLabel")} + + {apiConfiguration?.anthropicAdvisorEnabled && ( +
+
+ + +
+
+ + { + const value = event.target.value + setApiConfigurationField( + "anthropicAdvisorMaxUses", + value === "" ? undefined : parseInt(value, 10), + ) + }} + placeholder={t("settings:providers.anthropicAdvisorMaxUsesPlaceholder")} + className="w-full px-2 py-1 rounded border border-vscode-input-border bg-vscode-input-background text-vscode-foreground" + /> +
+
+ )} +
+ )} ) } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 183cd663e3..5a6b24aeb1 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -437,6 +437,10 @@ "anthropicUseAuthToken": "Pass Anthropic API Key as Authorization header instead of X-Api-Key", "anthropic1MContextBetaLabel": "Enable 1M context window (Beta)", "anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6", + "anthropicAdvisorToolLabel": "Enable Advisor Tool (Beta)", + "anthropicAdvisorModelLabel": "Advisor Model", + "anthropicAdvisorMaxUsesLabel": "Max Uses", + "anthropicAdvisorMaxUsesPlaceholder": "No limit", "awsBedrock1MContextBetaLabel": "Enable 1M context window (Beta)", "awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6", "vertex1MContextBetaLabel": "Enable 1M context window (Beta)",