Add advisor functionality and tests

This commit is contained in:
TDFSE 2026-04-10 13:01:33 -04:00
parent 7adbfec2a4
commit d59fa64cfd
No known key found for this signature in database
GPG key ID: 3E111FF1FD10B349
7 changed files with 217 additions and 1 deletions

View file

@ -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(),

View file

@ -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({

View file

@ -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.",
},

View file

@ -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", () => {

View file

@ -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),
}

View file

@ -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
</div>
</div>
)}
{modelInfo?.supportsAdvisorTool && (
<div>
<Checkbox
checked={apiConfiguration?.anthropicAdvisorEnabled ?? false}
onChange={(checked: boolean) => {
setApiConfigurationField("anthropicAdvisorEnabled", checked)
}}>
{t("settings:providers.anthropicAdvisorToolLabel")}
</Checkbox>
{apiConfiguration?.anthropicAdvisorEnabled && (
<div className="mt-2 ml-6 space-y-2">
<div>
<label className="block text-sm font-medium mb-1">
{t("settings:providers.anthropicAdvisorModelLabel")}
</label>
<select
value={apiConfiguration?.anthropicAdvisorModel ?? "claude-opus-4-6"}
onChange={(event) => {
setApiConfigurationField("anthropicAdvisorModel", event.target.value)
}}
className="w-full px-2 py-1 rounded border border-vscode-input-border bg-vscode-input-background text-vscode-foreground">
<option value="claude-opus-4-6">claude-opus-4-6</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
{t("settings:providers.anthropicAdvisorMaxUsesLabel")}
</label>
<input
type="number"
min="1"
value={apiConfiguration?.anthropicAdvisorMaxUses ?? 3}
onChange={(event) => {
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"
/>
</div>
</div>
)}
</div>
)}
</>
)
}

View file

@ -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)",