From b66f0b6ca3bb386cebf7379855da04c0d8d0f225 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 10 Jun 2026 14:19:53 +0900 Subject: [PATCH] fix(foundry): use bearer auth for Claude --- extensions/microsoft-foundry/index.test.ts | 27 ++++++++++++++++++++++ extensions/microsoft-foundry/onboard.ts | 2 ++ extensions/microsoft-foundry/provider.ts | 4 ++-- src/llm/providers/anthropic.test.ts | 25 ++++++++++++++++++++ src/llm/providers/anthropic.ts | 21 +++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index 335845e18691..daae8e49a0d8 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -377,6 +377,16 @@ describe("microsoft-foundry plugin", () => { ); }); + it("requests scoped Azure CLI tokens for Foundry Anthropic probes", async () => { + mockAzureCliTokenRaw(JSON.stringify({ accessToken: "scoped-token" })); + + await getAccessTokenResultAsync({ scope: FOUNDRY_ANTHROPIC_SCOPE }); + + expect(execFileMock.mock.calls[0]?.[1]).toEqual( + expect.arrayContaining(["--scope", FOUNDRY_ANTHROPIC_SCOPE]), + ); + }); + it("fails clearly when the selected Azure subscription is not in the enabled list", async () => { const provider = registerProvider(); execFileSyncMock.mockImplementation((_file: string, args: string[]) => { @@ -744,6 +754,23 @@ describe("microsoft-foundry plugin", () => { expect(model?.compat?.supportsReasoningEffort).toBe(true); }); + it("preserves Fable limits when adding a newly selected Foundry deployment", async () => { + const provider = registerProvider(); + const config = buildFoundryConfig({ models: [] }); + + await provider.onModelSelected?.({ + config, + model: "microsoft-foundry/claude-fable-5", + prompter: {} as never, + agentDir: "/tmp/test-agent", + }); + + const model = config.models?.providers?.["microsoft-foundry"]?.models[0]; + expect(model?.id).toBe("claude-fable-5"); + expect(model?.contextWindow).toBe(1_000_000); + expect(model?.maxTokens).toBe(128_000); + }); + it("accepts tenant domains as valid tenant identifiers", () => { expect(isValidTenantIdentifier("contoso.onmicrosoft.com")).toBe(true); expect(isValidTenantIdentifier("00000000-0000-0000-0000-000000000000")).toBe(true); diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index 2323116404e6..6e648882c846 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -26,6 +26,7 @@ import { requiresFoundryMaxCompletionTokens, DEFAULT_API, DEFAULT_GPT5_API, + FOUNDRY_ANTHROPIC_SCOPE, usesFoundryResponsesByDefault, } from "./shared.js"; @@ -529,6 +530,7 @@ export async function testFoundryConnection(params: { }): Promise { try { const { accessToken } = getAccessTokenResult({ + scope: params.api === ANTHROPIC_MESSAGES_API ? FOUNDRY_ANTHROPIC_SCOPE : undefined, subscriptionId: params.subscriptionId, tenantId: params.tenantId, }); diff --git a/extensions/microsoft-foundry/provider.ts b/extensions/microsoft-foundry/provider.ts index 617073049e12..992037a07c34 100644 --- a/extensions/microsoft-foundry/provider.ts +++ b/extensions/microsoft-foundry/provider.ts @@ -148,8 +148,8 @@ export function buildMicrosoftFoundryProvider(): ProviderPlugin { : {}), input: selectedModelCapabilities.input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128_000, - maxTokens: 16_384, + contextWindow: selectedModelCapabilities.contextWindow, + maxTokens: selectedModelCapabilities.maxTokens, ...(selectedModelCapabilities.compat ? { compat: selectedModelCapabilities.compat } : {}), }); } diff --git a/src/llm/providers/anthropic.test.ts b/src/llm/providers/anthropic.test.ts index 5205df6e7d86..234e551ca5b0 100644 --- a/src/llm/providers/anthropic.test.ts +++ b/src/llm/providers/anthropic.test.ts @@ -83,6 +83,31 @@ describe("Anthropic provider", () => { expect(config.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer gateway-token"); }); + it("uses bearer auth for Microsoft Foundry Anthropic requests", async () => { + const model = makeAnthropicModel({ + provider: "microsoft-foundry", + baseUrl: "https://example.services.ai.azure.com/anthropic", + }); + const context = { + messages: [{ role: "user", content: "hello", timestamp: 1 }], + } satisfies Context; + + streamAnthropic(model, context, { + apiKey: "entra-access-token", + }); + + await vi.waitFor(() => expect(anthropicMockState.configs).toHaveLength(1)); + const config = anthropicMockState.configs[0] as { + apiKey?: string | null; + authToken?: string | null; + defaultHeaders?: Record; + }; + + expect(config.apiKey).toBeNull(); + expect(config.authToken).toBe("entra-access-token"); + expect(config.defaultHeaders?.["x-api-key"]).toBeUndefined(); + }); + it("preserves provider-signed Anthropic thinking and drops reasoning_content placeholders", async () => { const highSurrogate = String.fromCharCode(0xd83d); const signedThinking = `keep${highSurrogate}signed`; diff --git a/src/llm/providers/anthropic.ts b/src/llm/providers/anthropic.ts index 4ac247a63a8f..a03a321a54b6 100644 --- a/src/llm/providers/anthropic.ts +++ b/src/llm/providers/anthropic.ts @@ -945,6 +945,27 @@ function createClient( return { client, isOAuthToken: false }; } + if (model.provider === "microsoft-foundry") { + const client = new Anthropic({ + apiKey: null, + authToken: apiKey, + baseURL: model.baseUrl, + dangerouslyAllowBrowser: true, + defaultHeaders: mergeHeaders( + { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), + }, + model.headers, + dynamicHeaders, + optionsHeaders, + ), + }); + + return { client, isOAuthToken: false }; + } + // OAuth: Bearer auth, Claude Code identity headers if (isOAuthToken(apiKey)) { const client = new Anthropic({