Skip to content

Commit d59fa64

Browse files
committed
Add advisor functionality and tests
1 parent 7adbfec commit d59fa64

File tree

7 files changed

+217
-1
lines changed

7 files changed

+217
-1
lines changed

packages/types/src/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const modelInfoSchema = z.object({
9898
outputPrice: z.number().optional(),
9999
cacheWritesPrice: z.number().optional(),
100100
cacheReadsPrice: z.number().optional(),
101+
supportsAdvisorTool: z.boolean().optional(),
101102
longContextPricing: z
102103
.object({
103104
thresholdTokens: z.number(),

packages/types/src/provider-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
207207
anthropicBaseUrl: z.string().optional(),
208208
anthropicUseAuthToken: z.boolean().optional(),
209209
anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
210+
anthropicAdvisorEnabled: z.boolean().optional(),
211+
anthropicAdvisorModel: z.string().optional(),
212+
anthropicAdvisorMaxUses: z.number().int().min(1).optional(),
210213
})
211214

212215
const openRouterSchema = baseProviderSettingsSchema.extend({

packages/types/src/providers/anthropic.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const anthropicModels = {
1717
cacheWritesPrice: 3.75, // $3.75 per million tokens
1818
cacheReadsPrice: 0.3, // $0.30 per million tokens
1919
supportsReasoningBudget: true,
20+
supportsAdvisorTool: true,
2021
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
2122
tiers: [
2223
{
@@ -80,6 +81,7 @@ export const anthropicModels = {
8081
cacheWritesPrice: 6.25, // $6.25 per million tokens
8182
cacheReadsPrice: 0.5, // $0.50 per million tokens
8283
supportsReasoningBudget: true,
84+
supportsAdvisorTool: true,
8385
// Tiered pricing for extended context (requires beta flag)
8486
tiers: [
8587
{
@@ -196,6 +198,7 @@ export const anthropicModels = {
196198
cacheWritesPrice: 1.25,
197199
cacheReadsPrice: 0.1,
198200
supportsReasoningBudget: true,
201+
supportsAdvisorTool: true,
199202
description:
200203
"Claude Haiku 4.5 delivers near-frontier intelligence at lightning speeds with extended thinking, vision, and multilingual support.",
201204
},

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,141 @@ describe("AnthropicHandler", () => {
209209
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
210210
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("context-1m-2025-08-07")
211211
})
212+
213+
describe("advisor tool feature", () => {
214+
const systemPrompt = "You are a helpful assistant."
215+
const messages: Anthropic.Messages.MessageParam[] = [
216+
{
217+
role: "user",
218+
content: [{ type: "text" as const, text: "Hello" }],
219+
},
220+
]
221+
222+
it("should include advisor tool beta header when advisor tool is enabled", async () => {
223+
const advisorHandler = new AnthropicHandler({
224+
apiKey: "test-api-key",
225+
apiModelId: "claude-sonnet-4-6",
226+
anthropicAdvisorEnabled: true,
227+
})
228+
229+
const stream = advisorHandler.createMessage(systemPrompt, messages)
230+
231+
for await (const _chunk of stream) {
232+
// Consume stream
233+
}
234+
235+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
236+
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("advisor-tool-2026-03-01")
237+
})
238+
239+
it("should inject advisor tool definition when advisor tool is enabled", async () => {
240+
const advisorHandler = new AnthropicHandler({
241+
apiKey: "test-api-key",
242+
apiModelId: "claude-sonnet-4-6",
243+
anthropicAdvisorEnabled: true,
244+
})
245+
246+
const stream = advisorHandler.createMessage(systemPrompt, messages)
247+
248+
for await (const _chunk of stream) {
249+
// Consume stream
250+
}
251+
252+
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
253+
expect(callArgs?.tools).toBeDefined()
254+
expect(callArgs?.tools).toEqual(
255+
expect.arrayContaining([
256+
expect.objectContaining({
257+
type: "advisor_20260301",
258+
name: "advisor",
259+
model: "claude-opus-4-6",
260+
}),
261+
]),
262+
)
263+
})
264+
265+
it("should include max_uses in advisor tool definition when configured", async () => {
266+
const customMaxUses = 5
267+
const advisorHandler = new AnthropicHandler({
268+
apiKey: "test-api-key",
269+
apiModelId: "claude-sonnet-4-6",
270+
anthropicAdvisorEnabled: true,
271+
anthropicAdvisorMaxUses: customMaxUses,
272+
})
273+
274+
const stream = advisorHandler.createMessage(systemPrompt, messages)
275+
276+
for await (const _chunk of stream) {
277+
// Consume stream
278+
}
279+
280+
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
281+
const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301")
282+
expect(advisorTool).toBeDefined()
283+
expect(advisorTool?.max_uses).toBe(customMaxUses)
284+
})
285+
286+
it("should use custom advisor model when configured", async () => {
287+
const customModel = "claude-opus-4-5-20251101"
288+
const advisorHandler = new AnthropicHandler({
289+
apiKey: "test-api-key",
290+
apiModelId: "claude-sonnet-4-6",
291+
anthropicAdvisorEnabled: true,
292+
anthropicAdvisorModel: customModel,
293+
})
294+
295+
const stream = advisorHandler.createMessage(systemPrompt, messages)
296+
297+
for await (const _chunk of stream) {
298+
// Consume stream
299+
}
300+
301+
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
302+
const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301")
303+
expect(advisorTool).toBeDefined()
304+
expect(advisorTool?.model).toBe(customModel)
305+
})
306+
307+
it("should not include advisor tool when advisor tool is disabled", async () => {
308+
const stream = handler.createMessage(systemPrompt, messages)
309+
310+
for await (const _chunk of stream) {
311+
// Consume stream
312+
}
313+
314+
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
315+
const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301")
316+
expect(advisorTool).toBeUndefined()
317+
318+
// Also verify beta header is not present
319+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
320+
const betaHeader = requestOptions?.headers?.["anthropic-beta"]
321+
if (betaHeader && typeof betaHeader === "string") {
322+
expect(betaHeader).not.toContain("advisor-tool-2026-03-01")
323+
}
324+
})
325+
326+
it("should use default advisor model and max_uses when not configured", async () => {
327+
const advisorHandler = new AnthropicHandler({
328+
apiKey: "test-api-key",
329+
apiModelId: "claude-sonnet-4-6",
330+
anthropicAdvisorEnabled: true,
331+
// No anthropicAdvisorModel or anthropicAdvisorMaxUses provided
332+
})
333+
334+
const stream = advisorHandler.createMessage(systemPrompt, messages)
335+
336+
for await (const _chunk of stream) {
337+
// Consume stream
338+
}
339+
340+
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
341+
const advisorTool = callArgs?.tools?.find((tool: any) => tool.type === "advisor_20260301")
342+
expect(advisorTool).toBeDefined()
343+
expect(advisorTool?.model).toBe("claude-opus-4-6") // default
344+
expect(advisorTool?.max_uses).toBe(3) // default
345+
})
346+
})
212347
})
213348

214349
describe("completePrompt", () => {

src/api/providers/anthropic.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,30 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
7575
betas.push("context-1m-2025-08-07")
7676
}
7777

78+
// Add advisor tool beta flag if enabled
79+
if (this.options.anthropicAdvisorEnabled) {
80+
betas.push("advisor-tool-2026-03-01")
81+
}
82+
83+
let tools = convertOpenAIToolsToAnthropic(metadata?.tools ?? [])
84+
85+
// Add advisor tool if enabled
86+
if (this.options.anthropicAdvisorEnabled) {
87+
const advisorModel = this.options.anthropicAdvisorModel ?? "claude-opus-4-6"
88+
const maxUses = this.options.anthropicAdvisorMaxUses ?? 3
89+
const advisorToolDef: { type: string; name: string; model: string; max_uses?: number } = {
90+
type: "advisor_20260301",
91+
name: "advisor",
92+
model: advisorModel,
93+
}
94+
if (maxUses >= 1) {
95+
advisorToolDef.max_uses = maxUses
96+
}
97+
tools.push(advisorToolDef as unknown as Anthropic.Tool)
98+
}
99+
78100
const nativeToolParams = {
79-
tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []),
101+
tools,
80102
tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls),
81103
}
82104

webview-ui/src/components/settings/providers/Anthropic.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type AnthropicProps = {
1919
export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => {
2020
const { t } = useAppTranslation()
2121
const selectedModel = useSelectedModel(apiConfiguration)
22+
const modelInfo = selectedModel?.info
2223

2324
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
2425

@@ -103,6 +104,53 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
103104
</div>
104105
</div>
105106
)}
107+
{modelInfo?.supportsAdvisorTool && (
108+
<div>
109+
<Checkbox
110+
checked={apiConfiguration?.anthropicAdvisorEnabled ?? false}
111+
onChange={(checked: boolean) => {
112+
setApiConfigurationField("anthropicAdvisorEnabled", checked)
113+
}}>
114+
{t("settings:providers.anthropicAdvisorToolLabel")}
115+
</Checkbox>
116+
{apiConfiguration?.anthropicAdvisorEnabled && (
117+
<div className="mt-2 ml-6 space-y-2">
118+
<div>
119+
<label className="block text-sm font-medium mb-1">
120+
{t("settings:providers.anthropicAdvisorModelLabel")}
121+
</label>
122+
<select
123+
value={apiConfiguration?.anthropicAdvisorModel ?? "claude-opus-4-6"}
124+
onChange={(event) => {
125+
setApiConfigurationField("anthropicAdvisorModel", event.target.value)
126+
}}
127+
className="w-full px-2 py-1 rounded border border-vscode-input-border bg-vscode-input-background text-vscode-foreground">
128+
<option value="claude-opus-4-6">claude-opus-4-6</option>
129+
</select>
130+
</div>
131+
<div>
132+
<label className="block text-sm font-medium mb-1">
133+
{t("settings:providers.anthropicAdvisorMaxUsesLabel")}
134+
</label>
135+
<input
136+
type="number"
137+
min="1"
138+
value={apiConfiguration?.anthropicAdvisorMaxUses ?? 3}
139+
onChange={(event) => {
140+
const value = event.target.value
141+
setApiConfigurationField(
142+
"anthropicAdvisorMaxUses",
143+
value === "" ? undefined : parseInt(value, 10),
144+
)
145+
}}
146+
placeholder={t("settings:providers.anthropicAdvisorMaxUsesPlaceholder")}
147+
className="w-full px-2 py-1 rounded border border-vscode-input-border bg-vscode-input-background text-vscode-foreground"
148+
/>
149+
</div>
150+
</div>
151+
)}
152+
</div>
153+
)}
106154
</>
107155
)
108156
}

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@
437437
"anthropicUseAuthToken": "Pass Anthropic API Key as Authorization header instead of X-Api-Key",
438438
"anthropic1MContextBetaLabel": "Enable 1M context window (Beta)",
439439
"anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6",
440+
"anthropicAdvisorToolLabel": "Enable Advisor Tool (Beta)",
441+
"anthropicAdvisorModelLabel": "Advisor Model",
442+
"anthropicAdvisorMaxUsesLabel": "Max Uses",
443+
"anthropicAdvisorMaxUsesPlaceholder": "No limit",
440444
"awsBedrock1MContextBetaLabel": "Enable 1M context window (Beta)",
441445
"awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6",
442446
"vertex1MContextBetaLabel": "Enable 1M context window (Beta)",

0 commit comments

Comments
 (0)