From 08655fb02b1ed8e7d323dd1ee387133f08d7be35 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 11 Jun 2026 08:30:03 +0900 Subject: [PATCH] fix(foundry): bind auth and thinking contracts --- extensions/microsoft-foundry/index.test.ts | 91 ++++++++++++++++++---- extensions/microsoft-foundry/runtime.ts | 17 +++- extensions/microsoft-foundry/shared.ts | 25 ++---- 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index 937d7665b112..8eca5e4216f7 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -91,7 +91,18 @@ function requirePrepareRuntimeAuth( } function requireRuntimeAuthResult( - result: { apiKey?: string; baseUrl?: string; expiresAt?: number } | undefined, + result: + | { + apiKey?: string; + baseUrl?: string; + expiresAt?: number; + request?: { + auth?: + | { mode: "authorization-bearer"; token: string } + | { mode: "header"; headerName: string; value: string }; + }; + } + | undefined, ) { if (!result) { throw new Error("expected Microsoft Foundry runtime auth result"); @@ -450,11 +461,41 @@ describe("microsoft-foundry plugin", () => { ); expect(prepared.baseUrl).toBe("https://example.services.ai.azure.com/openai/v1"); + expect(prepared.request?.auth).toEqual({ + mode: "authorization-bearer", + token: "test-token", + }); expect(execFileMock.mock.calls[0]?.[1]).toEqual( expect.arrayContaining(["--resource", COGNITIVE_SERVICES_RESOURCE]), ); }); + it.each([ + ["openai-responses", "api-key"], + ["anthropic-messages", "x-api-key"], + ] as const)("binds %s API-key auth to the active profile", async (api, headerName) => { + const provider = registerProvider(); + const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider); + + const prepared = requireRuntimeAuthResult( + await prepareRuntimeAuth( + buildFoundryRuntimeAuthContext({ + apiKey: "profile-api-key", + profileId: "microsoft-foundry:default", + model: buildFoundryModel({ api }), + }), + ), + ); + + expect(prepared).toEqual({ + apiKey: "profile-api-key", + request: { + auth: { mode: "header", headerName, value: "profile-api-key" }, + }, + }); + expect(execFileMock).not.toHaveBeenCalled(); + }); + it("uses active model routing when Entra metadata points at another deployment", async () => { const provider = registerProvider(); const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider); @@ -1201,7 +1242,7 @@ describe("microsoft-foundry plugin", () => { expect(normalized?.compat?.supportsReasoningEffort).toBe(false); }); - it("writes Azure API key header overrides for API-key auth configs", () => { + it("keeps API-key credentials scoped to auth profiles", () => { const result = buildFoundryAuthResult({ profileId: "microsoft-foundry:default", apiKey: "test-api-key", @@ -1212,9 +1253,9 @@ describe("microsoft-foundry plugin", () => { }); const provider = requireFoundryProviderPatch(result); - expect(provider.apiKey).toBe("test-api-key"); - expect(provider.authHeader).toBe(false); - expect(provider.headers).toEqual({ "api-key": "test-api-key" }); + expect(provider.apiKey).toBeUndefined(); + expect(provider.authHeader).toBeUndefined(); + expect(provider.headers).toBeUndefined(); }); it("uses the minimum supported response token count for GPT-5 connection tests", () => { @@ -1331,7 +1372,7 @@ describe("microsoft-foundry plugin", () => { const provider = result.configPatch?.models?.providers?.["microsoft-foundry"]; expect(provider?.baseUrl).toBe("https://example.services.ai.azure.com/anthropic"); expect(provider?.api).toBe("anthropic-messages"); - expect(provider?.authHeader).toBe(true); + expect(provider?.authHeader).toBeUndefined(); expect(provider?.models[0]).toMatchObject({ id: "prod-fable", name: "claude-fable-5", @@ -1345,7 +1386,7 @@ describe("microsoft-foundry plugin", () => { expect(provider?.models[0]?.compat).toBeUndefined(); }); - it("clears stale API-key credentials when writing Entra provider patches", () => { + it("keeps Entra credentials scoped to auth profiles", () => { const result = buildFoundryAuthResult({ profileId: "microsoft-foundry:entra", apiKey: "__entra_id_dynamic__", @@ -1359,9 +1400,9 @@ describe("microsoft-foundry plugin", () => { const provider = result.configPatch?.models?.providers?.["microsoft-foundry"] as | Record | undefined; - expect(provider?.authHeader).toBe(true); - expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(true); - expect(Object.hasOwn(provider ?? {}, "headers")).toBe(true); + expect(provider?.authHeader).toBeUndefined(); + expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(false); + expect(Object.hasOwn(provider ?? {}, "headers")).toBe(false); expect(provider?.apiKey).toBeUndefined(); expect(provider?.headers).toBeUndefined(); }); @@ -1463,6 +1504,28 @@ describe("microsoft-foundry plugin", () => { ], }); } + for (const modelName of [ + "claude-opus-4-1", + "claude-opus-4-5", + "claude-sonnet-4-5", + "claude-haiku-4-5", + ]) { + expect( + provider.resolveThinkingProfile?.({ + provider: "microsoft-foundry", + modelId: `prod-${modelName}`, + params: { canonicalModelId: modelName }, + }), + ).toMatchObject({ + levels: [ + { id: "off" }, + { id: "minimal" }, + { id: "low" }, + { id: "medium" }, + { id: "high" }, + ], + }); + } expect( provider.resolveThinkingProfile?.({ provider: "microsoft-foundry", @@ -1763,7 +1826,7 @@ describe("microsoft-foundry plugin", () => { await expect(getAccessTokenResultAsync()).rejects.toThrow("Azure CLI is not logged in"); }); - it("keeps Azure API key header overrides when API-key auth uses a secret ref", () => { + it("keeps API-key secret refs scoped to auth profiles", () => { const secretRef = { source: "env" as const, provider: "default", @@ -1779,9 +1842,9 @@ describe("microsoft-foundry plugin", () => { }); const provider = requireFoundryProviderPatch(result); - expect(provider.apiKey).toBe(secretRef); - expect(provider.authHeader).toBe(false); - expect(provider.headers).toEqual({ "api-key": secretRef }); + expect(provider.apiKey).toBeUndefined(); + expect(provider.authHeader).toBeUndefined(); + expect(provider.headers).toBeUndefined(); }); it("moves the selected Foundry auth profile to the front of auth.order", () => { diff --git a/extensions/microsoft-foundry/runtime.ts b/extensions/microsoft-foundry/runtime.ts index e382e50635b3..3a6f18796ddc 100644 --- a/extensions/microsoft-foundry/runtime.ts +++ b/extensions/microsoft-foundry/runtime.ts @@ -51,7 +51,16 @@ async function refreshEntraToken(params?: { export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthContext) { if (ctx.apiKey !== "__entra_id_dynamic__") { - return null; + return { + apiKey: ctx.apiKey, + request: { + auth: { + mode: "header" as const, + headerName: ctx.model.api === ANTHROPIC_MESSAGES_API ? "x-api-key" : "api-key", + value: ctx.apiKey, + }, + }, + }; } try { const authStore = ensureAuthProfileStore(ctx.agentDir, { @@ -102,6 +111,9 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC apiKey: cachedToken.token, expiresAt: cachedToken.expiresAt, ...(baseUrl ? { baseUrl } : {}), + request: { + auth: { mode: "authorization-bearer" as const, token: cachedToken.token }, + }, }; } let refreshPromise = refreshPromises.get(cacheKey); @@ -119,6 +131,9 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC return { ...token, ...(baseUrl ? { baseUrl } : {}), + request: { + auth: { mode: "authorization-bearer" as const, token: token.apiKey }, + }, }; } catch (err) { const details = formatErrorMessage(err); diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index ee3063ccfea1..829af526bbab 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -225,6 +225,13 @@ export function requiresFoundryMandatoryAdaptiveClaudeThinking(value?: string | : false; } +function supportsFoundryManualClaudeThinking(value?: string | null): boolean { + const normalized = normalizeFoundryModelName(value)?.replace(/\./g, "-"); + return normalized + ? /(?:^|-)claude-(?:opus-4-(?:1|5)|sonnet-4-5|haiku-4-5)(?=$|[^a-z0-9])/.test(normalized) + : false; +} + function resolveFoundryModelTokenLimits(value?: string | null): { contextWindow: number; maxTokens: number; @@ -448,6 +455,7 @@ export function resolveFoundryModelCapabilities( const supportsClaudeThinking = isAnthropic && (supportsClaudeAdaptiveThinking({ id: modelName }) || + supportsFoundryManualClaudeThinking(modelName) || requiresFoundryMandatoryAdaptiveClaudeThinking(modelName)); const supportsClaudeXhighThinking = isAnthropic && supportsClaudeNativeXhighEffort({ id: modelName }); @@ -502,14 +510,9 @@ function buildFoundryProviderConfig( modelNameHint?: string | null, options?: { api?: FoundryProviderApi; - authMethod?: "api-key" | "entra-id"; - apiKey?: SecretInput; deployments?: FoundryDeploymentConfigInput[]; }, ): FoundryProviderConfigPatch { - const runtimeApiKey = options?.authMethod === "api-key" ? options.apiKey : undefined; - const isApiKeyAuth = options?.authMethod === "api-key"; - const isEntraIdAuth = options?.authMethod === "entra-id"; const resolvedApi = resolveFoundryApi(modelId, modelNameHint, options?.api); const deployments = options?.deployments?.length ? options.deployments @@ -517,16 +520,6 @@ function buildFoundryProviderConfig( return { baseUrl: buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, resolvedApi), api: resolvedApi, - ...(isApiKeyAuth - ? { - authHeader: false, - ...(runtimeApiKey !== undefined - ? { apiKey: runtimeApiKey, headers: { "api-key": runtimeApiKey } } - : {}), - } - : isEntraIdAuth - ? { authHeader: true, apiKey: undefined, headers: undefined } - : {}), models: deployments.map((deployment) => { const capabilities = resolveFoundryModelCapabilities( deployment.name, @@ -734,8 +727,6 @@ export function buildFoundryAuthResult(params: { params.modelNameHint, { api: params.api, - authMethod: params.authMethod, - apiKey: params.apiKey, deployments: params.deployments, }, ) as unknown as ModelProviderConfig,