fix(foundry): use bearer auth for Claude

This commit is contained in:
Vincent Koc
2026-06-10 14:19:53 +09:00
parent 2fa9a5eaa0
commit b66f0b6ca3
5 changed files with 77 additions and 2 deletions

View File

@@ -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);

View File

@@ -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<void> {
try {
const { accessToken } = getAccessTokenResult({
scope: params.api === ANTHROPIC_MESSAGES_API ? FOUNDRY_ANTHROPIC_SCOPE : undefined,
subscriptionId: params.subscriptionId,
tenantId: params.tenantId,
});

View File

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

View File

@@ -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<string, string | null>;
};
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`;

View File

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