diff --git a/extensions/microsoft-foundry/index.test.ts b/extensions/microsoft-foundry/index.test.ts index daae8e49a0d8..6a43243ab0a9 100644 --- a/extensions/microsoft-foundry/index.test.ts +++ b/extensions/microsoft-foundry/index.test.ts @@ -10,6 +10,7 @@ import { buildFoundryConnectionTest, isValidTenantIdentifier, promptApiKeyEndpointAndModel, + promptEndpointAndModelManually, selectFoundryDeployment, } from "./onboard.js"; import { resetFoundryRuntimeAuthCaches } from "./runtime.js"; @@ -20,8 +21,8 @@ import { isAnthropicFoundryDeployment, isFoundryMaiImageModel, normalizeFoundryEndpoint, - partitionFoundryDeployments, requiresFoundryMaxCompletionTokens, + requiresFoundryEntraIdClaudeAuth, supportsFoundryReasoningContent, supportsFoundryReasoningEffort, supportsFoundryImageInput, @@ -895,6 +896,105 @@ describe("microsoft-foundry plugin", () => { expect(result.defaultModel).toBeUndefined(); }); + it("keeps API-key manual setup defaulted to chat completions for GPT deployments", async () => { + const text = vi + .fn() + .mockResolvedValueOnce("https://example.services.ai.azure.com") + .mockResolvedValueOnce("gpt-4o"); + const select = vi + .fn() + .mockImplementationOnce(async (params: { initialValue?: string }) => { + expect(params.initialValue).toBe("other-chat"); + return "other-chat"; + }) + .mockImplementationOnce(async (params: { initialValue?: string }) => { + expect(params.initialValue).toBe("openai-completions"); + return "openai-completions"; + }); + + const selection = await promptApiKeyEndpointAndModel({ + prompter: { + text, + select, + }, + } as never); + + expect(selection).toEqual({ + endpoint: "https://example.services.ai.azure.com", + modelId: "gpt-4o", + api: "openai-completions", + }); + }); + + it("rejects Entra-only Claude Mythos deployments during API-key manual setup", async () => { + const text = vi.fn( + async (params: { message: string; validate?: (value: string) => string | undefined }) => { + if (params.message === "Microsoft Foundry endpoint URL") { + return "https://example.services.ai.azure.com"; + } + if (params.message === "Default model/deployment name") { + return "prod-mythos"; + } + if (params.message === "Claude base model") { + expect(params.validate?.("claude-fable-5")).toBeUndefined(); + expect(params.validate?.("claude-mythos-preview")).toContain("Entra ID auth"); + return "claude-fable-5"; + } + throw new Error(`unexpected prompt: ${params.message}`); + }, + ); + const select = vi.fn().mockResolvedValueOnce("claude"); + + const selection = await promptApiKeyEndpointAndModel({ + prompter: { + text, + select, + }, + } as never); + + expect(selection).toEqual({ + endpoint: "https://example.services.ai.azure.com", + modelId: "prod-mythos", + modelNameHint: "claude-fable-5", + api: "anthropic-messages", + }); + expect(requiresFoundryEntraIdClaudeAuth("claude-mythos-preview")).toBe(true); + expect(requiresFoundryEntraIdClaudeAuth("claude-fable-5")).toBe(false); + }); + + it("allows Entra-only Claude Mythos deployments during Entra manual setup", async () => { + const text = vi.fn( + async (params: { message: string; validate?: (value: string) => string | undefined }) => { + if (params.message === "Microsoft Foundry endpoint URL") { + return "https://example.services.ai.azure.com"; + } + if (params.message === "Default model/deployment name") { + return "prod-mythos"; + } + if (params.message === "Claude base model") { + expect(params.validate?.("claude-mythos-preview")).toBeUndefined(); + return "claude-mythos-preview"; + } + throw new Error(`unexpected prompt: ${params.message}`); + }, + ); + const select = vi.fn().mockResolvedValueOnce("claude"); + + const selection = await promptEndpointAndModelManually({ + prompter: { + text, + select, + }, + } as never); + + expect(selection).toEqual({ + endpoint: "https://example.services.ai.azure.com", + modelId: "prod-mythos", + modelNameHint: "claude-mythos-preview", + api: "anthropic-messages", + }); + }); + it("uses discovered deployment metadata for MAI image defaults", () => { const result = buildFoundryAuthResult({ profileId: "microsoft-foundry:entra", @@ -1128,6 +1228,85 @@ describe("microsoft-foundry plugin", () => { expect(provider?.models[0]?.compat?.maxTokensField).toBe("max_tokens"); }); + it("routes Claude deployments through Foundry Anthropic Messages", () => { + const result = buildFoundryAuthResult({ + profileId: "microsoft-foundry:entra", + apiKey: "__entra_id_dynamic__", + endpoint: "https://example.services.ai.azure.com/openai/v1", + modelId: "prod-fable", + modelNameHint: "claude-fable-5", + api: "anthropic-messages", + authMethod: "entra-id", + }); + + 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?.models[0]).toMatchObject({ + id: "prod-fable", + name: "claude-fable-5", + api: "anthropic-messages", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 128_000, + thinkingLevelMap: { xhigh: "xhigh", max: "max" }, + }); + expect(provider?.models[0]?.compat).toBeUndefined(); + }); + + it.each([ + "claude-mythos-preview", + "claude-fable-5", + "claude-opus-4.8", + "claude-opus-4.7", + "claude-opus-4.6", + "claude-sonnet-4.6", + ])("preserves Foundry Claude 1M token limits for %s", (modelNameHint) => { + const result = buildFoundryAuthResult({ + profileId: "microsoft-foundry:entra", + apiKey: "__entra_id_dynamic__", + endpoint: "https://example.services.ai.azure.com", + modelId: `prod-${modelNameHint.replaceAll(".", "-")}`, + modelNameHint, + api: "anthropic-messages", + authMethod: "entra-id", + }); + + expect(result.configPatch?.models?.providers?.["microsoft-foundry"]?.models[0]).toMatchObject({ + name: modelNameHint, + api: "anthropic-messages", + contextWindow: 1_000_000, + maxTokens: 128_000, + }); + }); + + it("keeps older Foundry Claude deployments out of Fable-class thinking limits", () => { + const result = buildFoundryAuthResult({ + profileId: "microsoft-foundry:entra", + apiKey: "__entra_id_dynamic__", + endpoint: "https://example.services.ai.azure.com", + modelId: "prod-claude-35", + modelNameHint: "claude-3.5-sonnet", + api: "anthropic-messages", + authMethod: "entra-id", + }); + + const model = result.configPatch?.models?.providers?.["microsoft-foundry"]?.models[0]; + expect(model).toMatchObject({ + id: "prod-claude-35", + name: "claude-3.5-sonnet", + api: "anthropic-messages", + reasoning: false, + input: ["text", "image"], + contextWindow: 128_000, + maxTokens: 16_384, + }); + expect(model?.thinkingLevelMap).toBeUndefined(); + expect(model?.compat).toBeUndefined(); + }); + it("keeps Foundry chat reasoning_effort enabled for GPT-5 reasoning deployments", () => { const result = buildFoundryAuthResult({ profileId: "microsoft-foundry:default", @@ -1366,6 +1545,22 @@ describe("microsoft-foundry plugin", () => { expect(testRequest.body.max_tokens).toBe(1); }); + it("builds Anthropic Messages connection tests for Claude deployments", () => { + const testRequest = buildFoundryConnectionTest({ + endpoint: "https://example.services.ai.azure.com/openai/v1", + modelId: "prod-fable", + modelNameHint: "claude-fable-5", + api: "anthropic-messages", + }); + + expect(testRequest.url).toBe("https://example.services.ai.azure.com/anthropic/v1/messages"); + expect(testRequest.body).toEqual({ + model: "prod-fable", + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + }); + }); + it("returns actionable Azure CLI login errors", async () => { mockAzureCliLoginFailure(); @@ -1475,49 +1670,6 @@ describe("microsoft-foundry plugin", () => { }); }); -describe("partitionFoundryDeployments", () => { - it("keeps OpenAI-compatible deployments and skips Claude in mixed resources", () => { - const { supported, anthropic } = partitionFoundryDeployments([ - { name: "prod-gpt", modelName: "gpt-5.4" }, - { name: "prod-claude", modelName: "claude-opus-4-6" }, - { name: "prod-mini", modelName: "gpt-4o-mini" }, - ]); - - expect(supported.map((deployment) => deployment.name)).toEqual(["prod-gpt", "prod-mini"]); - expect(anthropic.map((deployment) => deployment.name)).toEqual(["prod-claude"]); - }); - - it("returns no supported deployments when only Anthropic deployments exist", () => { - const { supported, anthropic } = partitionFoundryDeployments([ - { name: "only-claude", modelName: "claude-3.5-sonnet" }, - ]); - - expect(supported).toEqual([]); - expect(anthropic.map((deployment) => deployment.name)).toEqual(["only-claude"]); - }); - - it("is a no-op for all-OpenAI resources", () => { - const deployments = [ - { name: "prod-gpt", modelName: "gpt-5.4" }, - { name: "prod-mini", modelName: "gpt-4o-mini" }, - ]; - const { supported, anthropic } = partitionFoundryDeployments(deployments); - - expect(supported).toEqual(deployments); - expect(anthropic).toEqual([]); - }); - - it("classifies by deployment name when modelName is missing", () => { - const { supported, anthropic } = partitionFoundryDeployments([ - { name: "claude-opus-4-6" }, - { name: "gpt-5.4-prod" }, - ]); - - expect(supported.map((deployment) => deployment.name)).toEqual(["gpt-5.4-prod"]); - expect(anthropic.map((deployment) => deployment.name)).toEqual(["claude-opus-4-6"]); - }); -}); - describe("selectFoundryDeployment", () => { function makeCtx(overrides: { selectValue?: string } = {}) { const noteCalls: Array<{ message: string; title: string }> = []; @@ -1545,7 +1697,7 @@ describe("selectFoundryDeployment", () => { projects: [], }; - it("offers and returns only supported deployments for mixed GPT and Claude resources", async () => { + it("offers and returns Claude deployments alongside GPT resources", async () => { const { ctx, selectCalls, noteCalls } = makeCtx({ selectValue: "prod-gpt" }); const result = await selectFoundryDeployment(ctx, fakeResource, [ { name: "prod-gpt", modelName: "gpt-5.4", state: "Succeeded" }, @@ -1555,25 +1707,30 @@ describe("selectFoundryDeployment", () => { expect(result.supported.map((deployment) => deployment.name)).toEqual([ "prod-gpt", + "prod-claude", "prod-mini", ]); expect(result.selected.name).toBe("prod-gpt"); expect(selectCalls[0]?.options.map((option) => option.value)).toEqual([ "prod-gpt", + "prod-claude", "prod-mini", ]); - expect(noteCalls.some((call) => call.title === "Unsupported Deployments")).toBe(true); + expect(noteCalls.some((call) => call.title === "Unsupported Deployments")).toBe(false); }); - it("throws an actionable error when only Anthropic deployments exist", async () => { + it("uses Anthropic-only deployment resources directly", async () => { const { ctx, noteCalls } = makeCtx(); - await expect( - selectFoundryDeployment(ctx, fakeResource, [ - { name: "only-claude", modelName: "claude-3.5-sonnet", state: "Succeeded" }, - ]), - ).rejects.toThrow(/Only Anthropic deployments/); - expect(noteCalls.some((call) => call.title === "Unsupported Deployments")).toBe(true); + const result = await selectFoundryDeployment(ctx, fakeResource, [ + { name: "only-claude", modelName: "claude-3.5-sonnet", state: "Succeeded" }, + ]); + + expect(result).toEqual({ + selected: { name: "only-claude", modelName: "claude-3.5-sonnet", state: "Succeeded" }, + supported: [{ name: "only-claude", modelName: "claude-3.5-sonnet", state: "Succeeded" }], + }); + expect(noteCalls.some((call) => call.title === "Unsupported Deployments")).toBe(false); }); it("leaves all-OpenAI resources unchanged", async () => { diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index 6e648882c846..0f3b5319763c 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -22,8 +22,9 @@ import { type FoundrySelection, buildFoundryProviderBaseUrl, extractFoundryEndpoint, - partitionFoundryDeployments, requiresFoundryMaxCompletionTokens, + requiresFoundryEntraIdClaudeAuth, + ANTHROPIC_MESSAGES_API, DEFAULT_API, DEFAULT_GPT5_API, FOUNDRY_ANTHROPIC_SCOPE, @@ -169,33 +170,14 @@ export async function selectFoundryDeployment( resource: FoundryResourceOption, deployments: AzDeploymentSummary[], ): Promise<{ selected: AzDeploymentSummary; supported: AzDeploymentSummary[] }> { - const { supported, anthropic } = partitionFoundryDeployments(deployments); - const anthropicNames = anthropic.map((deployment) => deployment.name); - if (anthropicNames.length > 0) { - await ctx.prompter.note( - [ - `Skipping ${anthropicNames.length} Anthropic deployment(s) (${anthropicNames.join(", ")}):`, - "the built-in Microsoft Foundry provider only supports OpenAI-compatible APIs.", - "To use Claude on Azure, configure a custom provider with base URL", - "https://.services.ai.azure.com/anthropic and API: anthropic-messages.", - ].join(" "), - "Unsupported Deployments", - ); - } + const supported = deployments; if (supported.length === 0) { - const hint = - anthropicNames.length > 0 - ? [ - `Only Anthropic deployments were found in ${resource.accountName},`, - "which are not supported by this provider.", - "Use a custom provider with the Anthropic Foundry endpoint, or", - "deploy an OpenAI-compatible model and rerun onboard.", - ].join(" ") - : [ - `No model deployments were found in ${resource.accountName}.`, - "Deploy a model in Azure AI Foundry or Azure OpenAI, then rerun onboard.", - ].join("\n"); - throw new Error(hint); + throw new Error( + [ + `No model deployments were found in ${resource.accountName}.`, + "Deploy a model in Microsoft Foundry or Azure OpenAI, then rerun onboard.", + ].join("\n"), + ); } if (supported.length === 1) { const only = supported[0]; @@ -224,6 +206,11 @@ async function promptFoundryApi( return await ctx.prompter.select({ message: "Select request API", options: [ + { + value: ANTHROPIC_MESSAGES_API, + label: "Anthropic Messages API", + hint: "Use for Claude deployments through Microsoft Foundry /anthropic", + }, { value: DEFAULT_GPT5_API, label: "Responses API", @@ -239,7 +226,7 @@ async function promptFoundryApi( }); } -type ManualFoundryModelFamilyChoice = "reasoning-family" | "mai-image" | "other-chat"; +type ManualFoundryModelFamilyChoice = "claude" | "reasoning-family" | "mai-image" | "other-chat"; type ManualFoundryMaiImageModel = | "MAI-Image-2.5-Flash" | "MAI-Image-2.5" @@ -248,10 +235,16 @@ type ManualFoundryMaiImageModel = async function promptFoundryModelFamily( ctx: ProviderAuthContext, + initialValue: ManualFoundryModelFamilyChoice, ): Promise { return await ctx.prompter.select({ message: "Model family", options: [ + { + value: "claude", + label: "Claude", + hint: "Use for Anthropic Claude deployments", + }, { value: "reasoning-family", label: "GPT-5 series / o-series / Codex", @@ -268,7 +261,7 @@ async function promptFoundryModelFamily( hint: "Use for other chat/completions style Foundry models", }, ], - initialValue: "reasoning-family", + initialValue, }); } @@ -303,17 +296,45 @@ async function promptFoundryMaiImageModel( }); } +async function promptFoundryClaudeModel( + ctx: ProviderAuthContext, + options?: { allowEntraOnlyModels?: boolean }, +): Promise { + return ( + await ctx.prompter.text({ + message: "Claude base model", + initialValue: "claude-fable-5", + placeholder: "claude-fable-5", + validate: (v) => { + const val = normalizeStringifiedOptionalString(v) ?? ""; + if (!val) { + return "Claude base model is required"; + } + if (!val.toLowerCase().startsWith("claude-")) { + return "Use a Claude model name such as claude-fable-5"; + } + if (options?.allowEntraOnlyModels === false && requiresFoundryEntraIdClaudeAuth(val)) { + return "Claude Mythos deployments require Microsoft Entra ID auth; choose Entra ID auth or use a Claude model that supports API-key auth."; + } + return undefined; + }, + }) + ).trim(); +} + async function promptEndpointAndModelBase( ctx: ProviderAuthContext, options?: { endpointInitialValue?: string; modelInitialValue?: string; + modelFamilyInitialValue?: ManualFoundryModelFamilyChoice; + allowEntraOnlyClaudeModels?: boolean; }, ): Promise { const endpoint = ( await ctx.prompter.text({ message: "Microsoft Foundry endpoint URL", - placeholder: "https://xxx.openai.azure.com or https://xxx.services.ai.azure.com", + placeholder: "https://xxx.services.ai.azure.com or https://xxx.openai.azure.com", ...(options?.endpointInitialValue ? { initialValue: options.endpointInitialValue } : {}), validate: (v) => { const val = normalizeStringifiedOptionalString(v) ?? ""; @@ -328,7 +349,7 @@ async function promptEndpointAndModelBase( await ctx.prompter.text({ message: "Default model/deployment name", ...(options?.modelInitialValue ? { initialValue: options.modelInitialValue } : {}), - placeholder: "gpt-4o", + placeholder: "claude-fable-5", validate: (v) => { const val = normalizeStringifiedOptionalString(v) ?? ""; if (!val) { @@ -338,7 +359,10 @@ async function promptEndpointAndModelBase( }, }) ).trim(); - const familyChoice = await promptFoundryModelFamily(ctx); + const familyChoice = await promptFoundryModelFamily( + ctx, + options?.modelFamilyInitialValue ?? "claude", + ); if (familyChoice === "mai-image") { return { endpoint, @@ -347,6 +371,16 @@ async function promptEndpointAndModelBase( api: DEFAULT_API, }; } + if (familyChoice === "claude") { + return { + endpoint, + modelId, + modelNameHint: await promptFoundryClaudeModel(ctx, { + allowEntraOnlyModels: options?.allowEntraOnlyClaudeModels ?? true, + }), + api: ANTHROPIC_MESSAGES_API, + }; + } const resolvedModelName = familyChoice === "reasoning-family" ? usesFoundryResponsesByDefault(modelId) || requiresFoundryMaxCompletionTokens(modelId) @@ -377,6 +411,8 @@ export async function promptApiKeyEndpointAndModel( return promptEndpointAndModelBase(ctx, { endpointInitialValue: process.env.AZURE_OPENAI_ENDPOINT, modelInitialValue: "gpt-4o", + modelFamilyInitialValue: "other-chat", + allowEntraOnlyClaudeModels: false, }); } @@ -402,6 +438,16 @@ export function buildFoundryConnectionTest(params: { }, }; } + if (params.api === ANTHROPIC_MESSAGES_API) { + return { + url: `${baseUrl}/v1/messages`, + body: { + model: params.modelId, + messages: [{ role: "user", content: "hi" }], + max_tokens: 1, + }, + }; + } return { url: `${baseUrl}/chat/completions`, body: { @@ -547,6 +593,7 @@ export async function testFoundryConnection(params: { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", + ...(params.api === ANTHROPIC_MESSAGES_API ? { "anthropic-version": "2023-06-01" } : {}), }, body: JSON.stringify(testRequest.body), }, diff --git a/extensions/microsoft-foundry/shared.ts b/extensions/microsoft-foundry/shared.ts index 684ce6ab3acb..ca4bdc50a56f 100644 --- a/extensions/microsoft-foundry/shared.ts +++ b/extensions/microsoft-foundry/shared.ts @@ -6,7 +6,13 @@ import { type ProviderAuthResult, type SecretInput, } from "openclaw/plugin-sdk/provider-auth"; -import type { ModelApi, ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { + isClaudeMandatoryAdaptiveThinkingModelId, + supportsClaudeAdaptiveThinking, + supportsClaudeNativeXhighEffort, + type ModelApi, + type ModelProviderConfig, +} from "openclaw/plugin-sdk/provider-model-shared"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, @@ -15,6 +21,7 @@ import { export const PROVIDER_ID = "microsoft-foundry"; export const DEFAULT_API = "openai-completions"; export const DEFAULT_GPT5_API = "openai-responses"; +export const ANTHROPIC_MESSAGES_API = "anthropic-messages"; export const COGNITIVE_SERVICES_RESOURCE = "https://cognitiveservices.azure.com"; export const FOUNDRY_ANTHROPIC_SCOPE = "https://ai.azure.com/.default"; export const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000; @@ -81,7 +88,10 @@ export type CachedTokenEntry = { expiresAt: number; }; -export type FoundryProviderApi = typeof DEFAULT_API | typeof DEFAULT_GPT5_API; +export type FoundryProviderApi = + | typeof DEFAULT_API + | typeof DEFAULT_GPT5_API + | typeof ANTHROPIC_MESSAGES_API; type FoundryDeploymentConfigInput = { name: string; @@ -141,22 +151,6 @@ export function isAnthropicFoundryDeployment(modelName?: string | null): boolean return normalized ? normalized.startsWith("claude") : false; } -export function partitionFoundryDeployments( - deployments: readonly T[], -): { supported: T[]; anthropic: T[] } { - const supported: T[] = []; - const anthropic: T[] = []; - for (const deployment of deployments) { - const classifier = resolveConfiguredModelNameHint(deployment.name, deployment.modelName); - if (isAnthropicFoundryDeployment(classifier)) { - anthropic.push(deployment); - } else { - supported.push(deployment); - } - } - return { supported, anthropic }; -} - export function usesFoundryResponsesByDefault(value?: string | null): boolean { const normalized = normalizeFoundryModelName(value); if (!normalized) { @@ -197,6 +191,7 @@ export function supportsFoundryImageInput(value?: string | null): boolean { return false; } return ( + isAnthropicFoundryDeployment(normalized) || normalized.startsWith("gpt-") || normalized.startsWith("o1") || normalized.startsWith("o3") || @@ -205,11 +200,21 @@ export function supportsFoundryImageInput(value?: string | null): boolean { ); } +export function requiresFoundryEntraIdClaudeAuth(value?: string | null): boolean { + const normalized = normalizeFoundryModelName(value); + return normalized + ? normalized === "claude-mythos-preview" || normalized.startsWith("claude-mythos-") + : false; +} + function resolveFoundryModelTokenLimits(value?: string | null): { contextWindow: number; maxTokens: number; } { const normalized = normalizeFoundryModelName(value); + if (normalized && supportsClaudeAdaptiveThinking(normalized)) { + return { contextWindow: 1_000_000, maxTokens: 128_000 }; + } if (normalized === "mai-ds-r1") { return { contextWindow: 163_840, maxTokens: 163_840 }; } @@ -291,7 +296,7 @@ function buildFoundryThinkingLevelMap( } export function isFoundryProviderApi(value?: string | null): value is FoundryProviderApi { - return value === DEFAULT_API || value === DEFAULT_GPT5_API; + return value === DEFAULT_API || value === DEFAULT_GPT5_API || value === ANTHROPIC_MESSAGES_API; } export function normalizeFoundryEndpoint(endpoint: string): string { @@ -303,11 +308,13 @@ export function normalizeFoundryEndpoint(endpoint: string): string { const parsed = new URL(trimmed); parsed.search = ""; parsed.hash = ""; - const normalizedPath = parsed.pathname.replace(/\/openai(?:$|\/).*/i, "").replace(/\/+$/, ""); + const normalizedPath = parsed.pathname + .replace(/\/(?:openai|anthropic)(?:$|\/).*/i, "") + .replace(/\/+$/, ""); return `${parsed.origin}${normalizedPath && normalizedPath !== "/" ? normalizedPath : ""}`; } catch { const withoutQuery = trimmed.replace(/[?#].*$/, "").replace(/\/+$/, ""); - return withoutQuery.replace(/\/openai(?:$|\/).*/i, ""); + return withoutQuery.replace(/\/(?:openai|anthropic)(?:$|\/).*/i, ""); } } @@ -316,6 +323,11 @@ function buildFoundryV1BaseUrl(endpoint: string): string { return base.endsWith("/openai/v1") ? base : `${base}/openai/v1`; } +function buildFoundryAnthropicBaseUrl(endpoint: string): string { + const base = normalizeFoundryEndpoint(endpoint); + return base.endsWith("/anthropic") ? base : `${base}/anthropic`; +} + export function resolveFoundryApi( modelId: string, modelNameHint?: string | null, @@ -325,16 +337,22 @@ export function resolveFoundryApi( return configuredApi; } const configuredModelName = resolveConfiguredModelNameHint(modelId, modelNameHint); + if (isAnthropicFoundryDeployment(configuredModelName)) { + return ANTHROPIC_MESSAGES_API; + } return usesFoundryResponsesByDefault(configuredModelName) ? DEFAULT_GPT5_API : DEFAULT_API; } export function buildFoundryProviderBaseUrl( endpoint: string, - _modelId: string, - _modelNameHint?: string | null, - _configuredApi?: ModelApi | null, + modelId: string, + modelNameHint?: string | null, + configuredApi?: ModelApi | null, ): string { - return buildFoundryV1BaseUrl(endpoint); + const resolvedApi = resolveFoundryApi(modelId, modelNameHint, configuredApi); + return resolvedApi === ANTHROPIC_MESSAGES_API + ? buildFoundryAnthropicBaseUrl(endpoint) + : buildFoundryV1BaseUrl(endpoint); } export function extractFoundryEndpoint(baseUrl: string | null | undefined): string | undefined { @@ -354,6 +372,9 @@ function buildFoundryModelCompat( configuredApi?: ModelApi | null, ): FoundryModelCompat | undefined { const resolvedApi = resolveFoundryApi(modelId, modelNameHint, configuredApi); + if (resolvedApi === ANTHROPIC_MESSAGES_API) { + return undefined; + } const configuredModelName = resolveConfiguredModelNameHint(modelId, modelNameHint); const needsMaxCompletionTokens = requiresFoundryMaxCompletionTokens(configuredModelName); const supportsReasoningEffort = supportsFoundryReasoningEffort(configuredModelName); @@ -382,15 +403,22 @@ export function resolveFoundryModelCapabilities( const api = resolveFoundryApi(modelId, modelName, configuredApi); const normalizedInput = normalizeModelInput(existingInput); const supportedReasoningEfforts = resolveFoundryReasoningEfforts(modelName); + const isAnthropic = api === ANTHROPIC_MESSAGES_API || isAnthropicFoundryDeployment(modelName); + const supportsClaudeThinking = isAnthropic && supportsClaudeAdaptiveThinking(modelName); + const supportsClaudeXhighThinking = isAnthropic && supportsClaudeNativeXhighEffort(modelName); const tokenLimits = resolveFoundryModelTokenLimits(modelName); return { modelName, api, reasoning: - supportsFoundryReasoningEffort(modelName) || supportsFoundryReasoningContent(modelName), - ...(supportedReasoningEfforts - ? { thinkingLevelMap: buildFoundryThinkingLevelMap(supportedReasoningEfforts) } - : {}), + supportsClaudeThinking || + supportsFoundryReasoningEffort(modelName) || + supportsFoundryReasoningContent(modelName), + ...(supportsClaudeXhighThinking + ? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } } + : supportedReasoningEfforts + ? { thinkingLevelMap: buildFoundryThinkingLevelMap(supportedReasoningEfforts) } + : {}), input: normalizedInput.includes("image") || supportsFoundryImageInput(modelName) ? ["text", "image"] @@ -426,6 +454,7 @@ function buildFoundryProviderConfig( ): ModelProviderConfig { 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 @@ -440,7 +469,9 @@ function buildFoundryProviderConfig( ? { apiKey: runtimeApiKey, headers: { "api-key": runtimeApiKey } } : {}), } - : {}), + : isEntraIdAuth + ? { authHeader: true } + : {}), models: deployments.map((deployment) => { const capabilities = resolveFoundryModelCapabilities( deployment.name,