fix(foundry): align Claude onboarding contracts

This commit is contained in:
Vincent Koc
2026-06-10 14:34:29 +09:00
parent 378ae44aad
commit 0a8dfc21be
3 changed files with 350 additions and 115 deletions

View File

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

View File

@@ -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://<resource>.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<ManualFoundryModelFamilyChoice> {
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<string> {
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<FoundrySelection> {
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),
},

View File

@@ -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<T extends { name: string; modelName?: string }>(
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,