Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
// Microsoft Foundry plugin module implements auth behavior.
import type {
ProviderAuthContext,
ProviderAuthMethod,
ProviderAuthResult,
} from "openclaw/plugin-sdk/core";
import {
ensureApiKeyFromOptionEnvOrPrompt,
ensureAuthProfileStore,
normalizeApiKeyInput,
normalizeOptionalSecretInput,
type SecretInput,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth";
import { getLoggedInAccount, isAzCliInstalled } from "./cli.js";
import {
loginWithTenantFallback,
listResourceDeployments,
promptApiKeyEndpointAndModel,
promptEndpointAndModelManually,
promptTenantId,
selectFoundryDeployment,
selectFoundryResource,
listSubscriptions,
testFoundryConnection,
} from "./onboard.js";
import {
buildFoundryAuthResult,
formatFoundryApiLabel,
type FoundryProviderApi,
isFoundryMaiImageModel,
listConfiguredFoundryProfileIds,
PROVIDER_ID,
resolveConfiguredModelNameHint,
resolveFoundryApi,
} from "./shared.js";
export function shouldTestFoundryTextConnection(params: {
modelId: string;
modelNameHint?: string | null;
}): boolean {
return !isFoundryMaiImageModel(
resolveConfiguredModelNameHint(params.modelId, params.modelNameHint),
);
}
export const entraIdAuthMethod: ProviderAuthMethod = {
id: "entra-id",
label: "Entra ID (az login)",
hint: "Use your Azure login — no API key needed",
kind: "custom",
wizard: {
choiceId: "microsoft-foundry-entra",
choiceLabel: "Microsoft Foundry (Entra ID / az login)",
choiceHint: "Use your Azure login — no API key needed",
onboardingScopes: ["text-inference", "image-generation"],
groupId: "microsoft-foundry",
groupLabel: "Microsoft Foundry",
groupHint: "Entra ID + API key",
},
run: async (ctx: ProviderAuthContext): Promise<ProviderAuthResult> => {
if (!isAzCliInstalled()) {
throw new Error(
"Azure CLI (az) is not installed.\nInstall it from https://learn.microsoft.com/cli/azure/install-azure-cli",
);
}
const account = getLoggedInAccount();
let tenantId = account?.tenantId;
if (account) {
const useExisting = await ctx.prompter.confirm({
message: `Already logged in as ${account.user?.name ?? "unknown"} (${account.name}). Use this account?`,
initialValue: true,
});
if (!useExisting) {
const loginResult = await loginWithTenantFallback(ctx);
tenantId = loginResult.tenantId ?? loginResult.account?.tenantId;
}
} else {
await ctx.prompter.note(
"You need to log in to Azure. A device code will be displayed - follow the instructions.",
"Azure Login",
);
const loginResult = await loginWithTenantFallback(ctx);
tenantId = loginResult.tenantId ?? loginResult.account?.tenantId;
}
const subs = listSubscriptions();
let selectedSub = null;
if (subs.length === 0) {
tenantId ??= await promptTenantId(ctx, {
required: true,
reason:
"No enabled Azure subscriptions were found. Continue with tenant-scoped Entra ID auth instead.",
});
await ctx.prompter.note(`Continuing with tenant-scoped auth (${tenantId}).`, "Azure Tenant");
} else if (subs.length === 1) {
selectedSub = subs[0]!;
tenantId ??= selectedSub.tenantId;
await ctx.prompter.note(
`Using subscription: ${selectedSub.name} (${selectedSub.id})`,
"Subscription",
);
} else {
const selectedId = await ctx.prompter.select({
message: "Select Azure subscription",
options: subs.map((sub) => ({
value: sub.id,
label: `${sub.name} (${sub.id})`,
})),
});
const match = subs.find((sub) => sub.id === selectedId);
if (!match) {
throw new Error(`Selected subscription not found: ${selectedId}`);
}
selectedSub = match;
tenantId ??= selectedSub.tenantId;
}
let endpoint: string;
let modelId: string;
let modelNameHint: string | undefined;
let api: FoundryProviderApi;
let discoveredDeployments:
| Array<{
name: string;
modelName?: string;
api?: FoundryProviderApi;
}>
| undefined;
if (selectedSub) {
const useDiscoveredResource = await ctx.prompter.confirm({
message: "Discover Microsoft Foundry resources from this subscription?",
initialValue: true,
});
if (useDiscoveredResource) {
const selectedResource = await selectFoundryResource(ctx, selectedSub);
const resourceDeployments = listResourceDeployments(selectedResource, selectedSub.id);
const { selected: selectedDeployment, supported: supportedDeployments } =
await selectFoundryDeployment(ctx, selectedResource, resourceDeployments);
discoveredDeployments = supportedDeployments.map((deployment) =>
Object.assign(
{ name: deployment.name },
deployment.modelName ? { modelName: deployment.modelName } : {},
{ api: resolveFoundryApi(deployment.name, deployment.modelName) },
),
);
endpoint = selectedResource.endpoint;
modelId = selectedDeployment.name;
modelNameHint = resolveConfiguredModelNameHint(modelId, selectedDeployment.modelName);
api = resolveFoundryApi(modelId, modelNameHint);
await ctx.prompter.note(
[
`Resource: ${selectedResource.accountName}`,
`Endpoint: ${endpoint}`,
`Deployment: ${modelId}`,
selectedDeployment.modelName ? `Model: ${selectedDeployment.modelName}` : undefined,
`API: ${formatFoundryApiLabel(api)}`,
]
.filter(Boolean)
.join("\n"),
"Microsoft Foundry",
);
} else {
({ endpoint, modelId, modelNameHint, api } = await promptEndpointAndModelManually(ctx));
}
} else {
({ endpoint, modelId, modelNameHint, api } = await promptEndpointAndModelManually(ctx));
}
if (shouldTestFoundryTextConnection({ modelId, modelNameHint })) {
await testFoundryConnection({
ctx,
endpoint,
modelId,
modelNameHint,
api,
subscriptionId: selectedSub?.id,
tenantId,
});
}
return buildFoundryAuthResult({
profileId: `${PROVIDER_ID}:entra`,
apiKey: "__entra_id_dynamic__",
endpoint,
modelId,
modelNameHint,
api,
authMethod: "entra-id",
...(selectedSub?.id ? { subscriptionId: selectedSub.id } : {}),
...(selectedSub?.name ? { subscriptionName: selectedSub.name } : {}),
...(tenantId ? { tenantId } : {}),
currentProviderProfileIds: listConfiguredFoundryProfileIds(ctx.config),
currentPluginsAllow: ctx.config.plugins?.allow,
...(discoveredDeployments ? { deployments: discoveredDeployments } : {}),
notes: [
...(selectedSub?.name ? [`Subscription: ${selectedSub.name}`] : []),
...(tenantId ? [`Tenant: ${tenantId}`] : []),
`Endpoint: ${endpoint}`,
`Model: ${modelId}`,
"Token is refreshed automatically via az CLI - keep az login active.",
],
});
},
};
export const apiKeyAuthMethod: ProviderAuthMethod = {
id: "api-key",
label: "Azure OpenAI API key",
hint: "Direct Azure OpenAI API key",
kind: "api_key",
wizard: {
choiceId: "microsoft-foundry-apikey",
choiceLabel: "Microsoft Foundry (API key)",
onboardingScopes: ["text-inference", "image-generation"],
groupId: "microsoft-foundry",
groupLabel: "Microsoft Foundry",
groupHint: "Entra ID + API key",
},
run: async (ctx) => {
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const existing = authStore.profiles[`${PROVIDER_ID}:default`];
const existingMetadata = existing?.type === "api_key" ? existing.metadata : undefined;
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
await ensureApiKeyFromOptionEnvOrPrompt({
token: normalizeOptionalSecretInput(ctx.opts?.azureOpenaiApiKey),
tokenProvider: PROVIDER_ID,
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID],
provider: PROVIDER_ID,
envLabel: "AZURE_OPENAI_API_KEY",
promptMessage: "Enter Azure OpenAI API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (apiKey, mode) => {
capturedSecretInput = apiKey;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedCredential) {
throw new Error("Missing Azure OpenAI API key.");
}
const selection = await promptApiKeyEndpointAndModel(ctx);
const existingModelNameHint =
existingMetadata?.modelId === selection.modelId
? (existingMetadata.modelName ?? existingMetadata.modelId)
: undefined;
return buildFoundryAuthResult({
profileId: `${PROVIDER_ID}:default`,
apiKey: capturedSecretInput ?? "",
...(capturedMode ? { secretInputMode: capturedMode } : {}),
endpoint: selection.endpoint,
modelId: selection.modelId,
modelNameHint: selection.modelNameHint ?? existingModelNameHint,
api: selection.api,
authMethod: "api-key",
currentProviderProfileIds: listConfiguredFoundryProfileIds(ctx.config),
currentPluginsAllow: ctx.config.plugins?.allow,
notes: [`Endpoint: ${selection.endpoint}`, `Model: ${selection.modelId}`],
});
},
};

View File

@@ -0,0 +1,212 @@
// Microsoft Foundry plugin module implements cli behavior.
import { execFile, execFileSync, spawn } from "node:child_process";
import {
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { AzAccessToken, AzAccount } from "./shared.js";
import { COGNITIVE_SERVICES_RESOURCE } from "./shared.js";
function summarizeAzErrorMessage(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return "";
}
const normalized = trimmed.replace(/\s+/g, " ");
if (/not recognized|enoent|spawn .* az/i.test(normalized)) {
return "Azure CLI (az) is not installed or not on PATH.";
}
if (/az login/i.test(normalized) || /please run 'az login'/i.test(normalized)) {
return "Azure CLI is not logged in. Run `az login --use-device-code`.";
}
if (
/subscription/i.test(normalized) &&
/could not be found|does not exist|no subscriptions/i.test(normalized)
) {
return "Azure CLI could not find an accessible subscription. Check the selected subscription or tenant access.";
}
if (
/tenant/i.test(normalized) &&
/not found|invalid|doesn't exist|does not exist/i.test(normalized)
) {
return "Azure CLI could not use that tenant. Verify the tenant ID or tenant domain and try `az login --tenant <tenant>`.";
}
if (/aadsts\d+/i.test(normalized)) {
return "Azure login failed for the selected tenant. Re-run `az login --use-device-code` and confirm the tenant is correct.";
}
return normalized.slice(0, 300);
}
function buildAzCommandError(error: Error, stderr: string, stdout: string): Error {
const details = summarizeAzErrorMessage(`${stderr ?? ""} ${stdout ?? ""}`);
return new Error(details ? `${error.message}: ${details}` : error.message);
}
export function execAz(args: string[]): string {
return (
normalizeOptionalString(
execFileSync("az", args, {
encoding: "utf-8",
timeout: 30_000,
shell: process.platform === "win32",
}),
) ?? ""
);
}
async function execAzAsync(args: string[]): Promise<string> {
return await new Promise<string>((resolve, reject) => {
execFile(
"az",
args,
{
encoding: "utf-8",
timeout: 30_000,
shell: process.platform === "win32",
},
(error, stdout, stderr) => {
if (error) {
reject(buildAzCommandError(error, stderr ?? "", stdout ?? ""));
return;
}
resolve(normalizeStringifiedOptionalString(stdout) ?? "");
},
);
});
}
export function isAzCliInstalled(): boolean {
try {
execAz(["version", "--output", "none"]);
return true;
} catch {
return false;
}
}
export function getLoggedInAccount(): AzAccount | null {
try {
return parseAzJson(execAz(["account", "show", "--output", "json"]), "account") as AzAccount;
} catch {
return null;
}
}
export function listSubscriptions(): AzAccount[] {
try {
const subs = parseAzJson(
execAz(["account", "list", "--output", "json", "--all"]),
"subscriptions",
) as AzAccount[];
return subs.filter((sub) => sub.state === "Enabled");
} catch {
return [];
}
}
function parseAzJson(raw: string, label: string): unknown {
try {
return JSON.parse(raw) as unknown;
} catch {
throw new Error(`Azure CLI returned malformed ${label} JSON.`);
}
}
type AccessTokenParams = {
scope?: string;
subscriptionId?: string;
tenantId?: string;
};
function buildAccessTokenArgs(params?: AccessTokenParams): string[] {
const args = ["account", "get-access-token"];
if (params?.scope) {
args.push("--scope", params.scope);
} else {
args.push("--resource", COGNITIVE_SERVICES_RESOURCE);
}
args.push("--output", "json");
if (params?.subscriptionId) {
args.push("--subscription", params.subscriptionId);
} else if (params?.tenantId) {
args.push("--tenant", params.tenantId);
}
return args;
}
export function getAccessTokenResult(params?: AccessTokenParams): AzAccessToken {
return parseAzJson(execAz(buildAccessTokenArgs(params)), "access token") as AzAccessToken;
}
export async function getAccessTokenResultAsync(
params?: AccessTokenParams,
): Promise<AzAccessToken> {
return parseAzJson(
await execAzAsync(buildAccessTokenArgs(params)),
"access token",
) as AzAccessToken;
}
export async function azLoginDeviceCode(): Promise<void> {
return azLoginDeviceCodeWithOptions({});
}
export async function azLoginDeviceCodeWithOptions(params: {
tenantId?: string;
allowNoSubscriptions?: boolean;
}): Promise<void> {
return new Promise<void>((resolve, reject) => {
const maxCapturedLoginOutputChars = 8_000;
const args = [
"login",
"--use-device-code",
...(params.tenantId ? ["--tenant", params.tenantId] : []),
...(params.allowNoSubscriptions ? ["--allow-no-subscriptions"] : []),
];
const child = spawn("az", args, {
stdio: ["inherit", "pipe", "pipe"],
shell: process.platform === "win32",
});
const stdoutChunks: string[] = [];
const stderrChunks: string[] = [];
let stdoutLen = 0;
let stderrLen = 0;
const appendBoundedChunk = (chunks: string[], text: string, len: number): number => {
if (!text) {
return len;
}
chunks.push(text);
let total = len + text.length;
while (total > maxCapturedLoginOutputChars && chunks.length > 0) {
const removed = chunks.shift();
total -= removed?.length ?? 0;
}
return total;
};
child.stdout?.on("data", (chunk) => {
const text = String(chunk);
stdoutLen = appendBoundedChunk(stdoutChunks, text, stdoutLen);
process.stdout.write(text);
});
child.stderr?.on("data", (chunk) => {
const text = String(chunk);
stderrLen = appendBoundedChunk(stderrChunks, text, stderrLen);
process.stderr.write(text);
});
child.on("close", (code) => {
if (code === 0) {
resolve();
return;
}
const output = normalizeOptionalString([...stderrChunks, ...stdoutChunks].join("")) ?? "";
reject(
new Error(
output
? `az login exited with code ${code}: ${output}`
: `az login exited with code ${code}`,
),
);
});
child.on("error", reject);
});
}

View File

@@ -0,0 +1,551 @@
// Microsoft Foundry image provider tests cover MAI request construction.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildMicrosoftFoundryImageGenerationProvider } from "./image-generation-provider.js";
import { PROVIDER_ID } from "./shared.js";
const {
assertOkOrThrowHttpErrorMock,
createProviderOperationDeadlineMock,
isProviderApiKeyConfiguredMock,
postJsonRequestMock,
postMultipartRequestMock,
prepareFoundryRuntimeAuthMock,
resolveApiKeyForProviderMock,
resolveProviderHttpRequestConfigMock,
resolveProviderOperationTimeoutMsMock,
sanitizeConfiguredModelProviderRequestMock,
} = vi.hoisted(() => ({
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
createProviderOperationDeadlineMock: vi.fn((params: Record<string, unknown>) => params),
isProviderApiKeyConfiguredMock: vi.fn(() => true),
postJsonRequestMock: vi.fn(),
postMultipartRequestMock: vi.fn(),
prepareFoundryRuntimeAuthMock: vi.fn(),
resolveApiKeyForProviderMock: vi.fn(async () => ({
apiKey: "foundry-key",
mode: "api-key" as const,
profileId: undefined as string | undefined,
source: "test",
})),
resolveProviderHttpRequestConfigMock: vi.fn((params: Record<string, unknown>) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
allowPrivateNetwork: false,
headers: new Headers(params.defaultHeaders as HeadersInit | undefined),
dispatcherPolicy: undefined,
})),
resolveProviderOperationTimeoutMsMock: vi.fn(
(params: Record<string, unknown>) =>
(params.deadline as { timeoutMs?: number }).timeoutMs ?? params.defaultTimeoutMs,
),
sanitizeConfiguredModelProviderRequestMock: vi.fn((request) => request),
}));
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
isProviderApiKeyConfigured: isProviderApiKeyConfiguredMock,
}));
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-http", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-http")>(
"openclaw/plugin-sdk/provider-http",
);
return {
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
createProviderOperationDeadline: createProviderOperationDeadlineMock,
postJsonRequest: postJsonRequestMock,
postMultipartRequest: postMultipartRequestMock,
readProviderJsonResponse: actual.readProviderJsonResponse,
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
resolveProviderOperationTimeoutMs: resolveProviderOperationTimeoutMsMock,
sanitizeConfiguredModelProviderRequest: sanitizeConfiguredModelProviderRequestMock,
};
});
vi.mock("./runtime.js", () => ({
prepareFoundryRuntimeAuth: prepareFoundryRuntimeAuthMock,
}));
function buildConfig(
params: {
modelId?: string;
modelName?: string;
baseUrl?: string;
includeModel?: boolean;
mediaMaxMb?: number;
} = {},
): OpenClawConfig {
const baseUrl = params.baseUrl ?? "https://example.services.ai.azure.com/openai/v1";
const modelId = params.modelId ?? "image-deployment";
const modelName = params.modelName ?? "MAI-Image-2.5";
return {
...(params.mediaMaxMb !== undefined
? { agents: { defaults: { mediaMaxMb: params.mediaMaxMb } } }
: {}),
models: {
providers: {
[PROVIDER_ID]: {
baseUrl,
api: "openai-completions",
models:
params.includeModel === false
? []
: [
{
id: modelId,
name: modelName,
api: "openai-completions",
baseUrl,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_000,
maxTokens: 0,
},
],
},
},
},
};
}
function releasedJson(payload: unknown) {
return {
response: Response.json(payload),
release: vi.fn(async () => {}),
};
}
function requirePostJsonRequest(): Record<string, unknown> {
const request = postJsonRequestMock.mock.calls[0]?.[0];
if (!request || typeof request !== "object") {
throw new Error("expected Microsoft Foundry JSON image request");
}
return request as Record<string, unknown>;
}
function requirePostMultipartRequest(): Record<string, unknown> {
const request = postMultipartRequestMock.mock.calls[0]?.[0];
if (!request || typeof request !== "object") {
throw new Error("expected Microsoft Foundry multipart image request");
}
return request as Record<string, unknown>;
}
function requireHeaders(value: unknown): Headers {
expect(value).toBeInstanceOf(Headers);
if (!(value instanceof Headers)) {
throw new Error("expected request headers");
}
return value;
}
describe("microsoft foundry image generation provider", () => {
afterEach(() => {
assertOkOrThrowHttpErrorMock.mockClear();
createProviderOperationDeadlineMock.mockClear();
isProviderApiKeyConfiguredMock.mockClear();
postJsonRequestMock.mockReset();
postMultipartRequestMock.mockReset();
prepareFoundryRuntimeAuthMock.mockReset();
resolveApiKeyForProviderMock.mockClear();
resolveProviderHttpRequestConfigMock.mockClear();
resolveProviderOperationTimeoutMsMock.mockClear();
sanitizeConfiguredModelProviderRequestMock.mockClear();
vi.unstubAllEnvs();
});
it("exposes MAI image provider metadata and capabilities", () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
expect(provider.id).toBe(PROVIDER_ID);
expect(provider.defaultModel).toBeUndefined();
expect(provider.models).toEqual([]);
expect(provider.capabilities.generate.maxCount).toBe(1);
expect(provider.capabilities.edit.enabled).toBe(true);
expect(provider.capabilities.edit.maxInputImages).toBe(1);
expect(provider.capabilities.geometry?.sizes).toBeUndefined();
expect(provider.capabilities.output?.formats).toEqual(["png"]);
expect(provider.isConfigured?.({ agentDir: "/agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: PROVIDER_ID,
agentDir: "/agent",
});
});
it("sends MAI image generation requests to the Foundry MAI endpoint with API-key auth", async () => {
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("png").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
const result = await provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw a clean product render",
cfg: buildConfig(),
size: "768x1365",
timeoutMs: 12_345,
ssrfPolicy: { allowPrivateNetwork: true },
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith({
provider: PROVIDER_ID,
cfg: buildConfig(),
agentDir: undefined,
store: undefined,
});
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith({
baseUrl: "https://example.services.ai.azure.com/mai/v1",
defaultBaseUrl: "https://example.services.ai.azure.com/mai/v1",
allowPrivateNetwork: false,
defaultHeaders: { "api-key": "foundry-key" },
request: undefined,
provider: PROVIDER_ID,
capability: "image",
transport: "http",
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
expect(createProviderOperationDeadlineMock).toHaveBeenCalledWith({
timeoutMs: 12_345,
label: "Microsoft Foundry MAI image generation",
});
expect(resolveProviderOperationTimeoutMsMock).toHaveBeenCalledWith({
deadline: { timeoutMs: 12_345, label: "Microsoft Foundry MAI image generation" },
defaultTimeoutMs: 600_000,
});
const request = requirePostJsonRequest();
expect(request.url).toBe("https://example.services.ai.azure.com/mai/v1/images/generations");
expect(request.body).toEqual({
model: "image-deployment",
prompt: "draw a clean product render",
width: 768,
height: 1365,
});
expect(Object.fromEntries(requireHeaders(request.headers).entries())).toEqual({
"api-key": "foundry-key",
"content-type": "application/json",
});
expect(request.timeoutMs).toBe(12_345);
expect(request.ssrfPolicy).toEqual({ allowPrivateNetwork: true });
expect(result.model).toBe("image-deployment");
expect(result.images[0]?.buffer.toString()).toBe("png");
expect(result.images[0]?.mimeType).toBe("image/png");
});
it("accepts a valid max-size MAI image JSON response", async () => {
const imageBytes = Buffer.alloc(6 * 1024 * 1024, 1);
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: imageBytes.toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
const result = await provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw it",
cfg: buildConfig(),
});
expect(result.images).toHaveLength(1);
expect(result.images[0]?.buffer.byteLength).toBe(imageBytes.byteLength);
});
it("honors configured generated media caps above the default image limit", async () => {
const imageBytes = Buffer.alloc(7 * 1024 * 1024, 1);
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: imageBytes.toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
const result = await provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw it",
cfg: buildConfig({ mediaMaxMb: 8 }),
});
expect(result.images).toHaveLength(1);
expect(result.images[0]?.buffer.byteLength).toBe(imageBytes.byteLength);
});
it("rejects oversized MAI image JSON responses", async () => {
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: "x".repeat(10 * 1024 * 1024) }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw it",
cfg: buildConfig(),
}),
).rejects.toThrow("microsoft-foundry.image-generation: JSON response exceeds");
});
it("uses AZURE_OPENAI_ENDPOINT when env API-key auth has no configured base URL", async () => {
vi.stubEnv("AZURE_OPENAI_ENDPOINT", "https://env.services.ai.azure.com");
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("png").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
await provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw from env endpoint",
cfg: buildConfig({ baseUrl: "" }),
});
expect(requirePostJsonRequest().url).toBe(
"https://env.services.ai.azure.com/mai/v1/images/generations",
);
});
it("refreshes Entra ID auth and sends MAI image edits as multipart form data", async () => {
resolveApiKeyForProviderMock.mockResolvedValueOnce({
apiKey: "__entra_id_dynamic__",
mode: "api-key",
profileId: "microsoft-foundry:entra",
source: "profile:microsoft-foundry:entra",
});
prepareFoundryRuntimeAuthMock.mockResolvedValueOnce({
apiKey: "entra-token",
baseUrl: "https://example.services.ai.azure.com/openai/v1",
expiresAt: Date.now() + 60_000,
});
resolveProviderHttpRequestConfigMock.mockImplementationOnce(
(params: Record<string, unknown>) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
allowPrivateNetwork: false,
headers: new Headers({
...(params.defaultHeaders as Record<string, string>),
"Content-Type": "application/json",
}),
dispatcherPolicy: undefined,
}),
);
postMultipartRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("edited").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
const result = await provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "make it brighter",
cfg: buildConfig(),
agentDir: "/agent",
inputImages: [
{
buffer: Buffer.from("input"),
mimeType: "image/png",
fileName: "input.png",
},
],
});
expect(prepareFoundryRuntimeAuthMock).toHaveBeenCalledWith(
expect.objectContaining({
agentDir: "/agent",
provider: PROVIDER_ID,
modelId: "image-deployment",
apiKey: "__entra_id_dynamic__",
authMode: "api-key",
profileId: "microsoft-foundry:entra",
}),
);
expect(postMultipartRequestMock).toHaveBeenCalledOnce();
const request = requirePostMultipartRequest();
expect(request.url).toBe("https://example.services.ai.azure.com/mai/v1/images/edits");
expect(Object.fromEntries(requireHeaders(request.headers).entries())).toEqual({
authorization: "Bearer entra-token",
});
const form = request.body as FormData;
expect(form.get("model")).toBe("image-deployment");
expect(form.get("prompt")).toBe("make it brighter");
const image = form.get("image");
expect(image).toBeInstanceOf(Blob);
expect((image as File).name).toBe("input.png");
expect((image as File).type).toBe("image/png");
expect(result.images[0]?.buffer.toString()).toBe("edited");
});
it("rejects image edits for MAI text-to-image-only deployments", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "edit it",
cfg: buildConfig({ modelName: "MAI-Image-2e" }),
inputImages: [{ buffer: Buffer.from("input"), mimeType: "image/png" }],
}),
).rejects.toThrow("MAI-Image-2e does not support Microsoft Foundry MAI image edits.");
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalled();
expect(postMultipartRequestMock).not.toHaveBeenCalled();
});
it("requires an explicit deployment name before making requests", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "",
prompt: "draw it",
cfg: buildConfig(),
}),
).rejects.toThrow("requires a deployment name");
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalled();
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("allows custom MAI deployment names for generation when model metadata is absent", async () => {
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("png").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
await provider.generateImage({
provider: PROVIDER_ID,
model: "prod-image",
prompt: "draw it",
cfg: buildConfig({ includeModel: false }),
size: "800x1000",
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
expect(requirePostJsonRequest().body).toEqual({
model: "prod-image",
prompt: "draw it",
width: 800,
height: 1000,
});
});
it("allows custom mai-image deployment names for generation without model metadata", async () => {
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("png").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
await provider.generateImage({
provider: PROVIDER_ID,
model: "mai-image-2-live",
prompt: "draw it",
cfg: buildConfig({ modelId: "mai-image-2-live", includeModel: false }),
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
expect(requirePostJsonRequest().body).toMatchObject({
model: "mai-image-2-live",
});
});
it("allows manual custom deployment names when configured name only repeats the id", async () => {
postJsonRequestMock.mockResolvedValue(
releasedJson({
data: [{ b64_json: Buffer.from("png").toString("base64") }],
}),
);
const provider = buildMicrosoftFoundryImageGenerationProvider();
await provider.generateImage({
provider: PROVIDER_ID,
model: "prod-image",
prompt: "draw it",
cfg: buildConfig({ modelId: "prod-image", modelName: "prod-image" }),
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
expect(requirePostJsonRequest().body).toEqual({
model: "prod-image",
prompt: "draw it",
width: 1024,
height: 1024,
});
});
it("requires MAI-Image-2.5 metadata before editing custom deployment names", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "prod-image",
prompt: "edit it",
cfg: buildConfig({ includeModel: false }),
inputImages: [{ buffer: Buffer.from("input"), mimeType: "image/png" }],
}),
).rejects.toThrow("edits require MAI-Image-2.5 model metadata");
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalled();
expect(postMultipartRequestMock).not.toHaveBeenCalled();
});
it("rejects non-MAI image deployments before making requests", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "gpt-deployment",
prompt: "draw it",
cfg: buildConfig({ modelId: "gpt-deployment", modelName: "gpt-5.4" }),
}),
).rejects.toThrow('supports MAI image deployments only, got "gpt-5.4"');
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalled();
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("rejects literal non-image MAI model names before making requests", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "MAI-DS-R1",
prompt: "draw it",
cfg: buildConfig({ includeModel: false }),
}),
).rejects.toThrow('supports MAI image deployments only, got "MAI-DS-R1"');
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalled();
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("rejects MAI image sizes outside Microsoft Foundry limits", async () => {
const provider = buildMicrosoftFoundryImageGenerationProvider();
await expect(
provider.generateImage({
provider: PROVIDER_ID,
model: "image-deployment",
prompt: "draw it",
cfg: buildConfig(),
size: "512x512",
}),
).rejects.toThrow("at least 768x768");
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,404 @@
// Microsoft Foundry image provider routes MAI image deployments to the MAI API.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { ProviderRuntimeModel } from "openclaw/plugin-sdk/core";
import type {
ImageGenerationProvider,
ImageGenerationRequest,
ImageGenerationResult,
ImageGenerationSourceImage,
} from "openclaw/plugin-sdk/image-generation";
import {
imageSourceUploadFileName,
parseOpenAiCompatibleImageResponse,
resolveInlineImageJsonResponseMaxBytes,
} from "openclaw/plugin-sdk/image-generation";
import { MAX_IMAGE_BYTES } from "openclaw/plugin-sdk/media-runtime";
import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
assertOkOrThrowHttpError,
createProviderOperationDeadline,
postJsonRequest,
postMultipartRequest,
readProviderJsonResponse,
resolveProviderHttpRequestConfig,
resolveProviderOperationTimeoutMs,
sanitizeConfiguredModelProviderRequest,
} from "openclaw/plugin-sdk/provider-http";
import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { prepareFoundryRuntimeAuth } from "./runtime.js";
import { extractFoundryEndpoint } from "./shared-runtime.js";
import {
DEFAULT_API,
isFoundryMaiImageModel,
isFoundryProviderApi,
PROVIDER_ID,
} from "./shared.js";
const DEFAULT_TIMEOUT_MS = 600_000;
const DEFAULT_IMAGE_SIZE = { width: 1024, height: 1024 };
const MAI_MIN_IMAGE_SIDE_PX = 768;
const MAI_MAX_IMAGE_PIXELS = 1_048_576;
const MAI_IMAGE_BASE_PATH = "/mai/v1";
const MAI_IMAGE_MAX_RESULTS = 1;
const MAI_IMAGE_OUTPUT_MIME = "image/png";
const MB = 1024 * 1024;
const MAI_IMAGE_UPLOAD_MIME_TYPES = new Set(["image/jpeg", "image/jpg", "image/png"]);
type ModelProviderConfig = NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]>[string];
function readProviderConfig(req: ImageGenerationRequest): ModelProviderConfig | undefined {
return req.cfg.models?.providers?.[PROVIDER_ID];
}
function resolveConfiguredModelName(
providerConfig: ModelProviderConfig | undefined,
model: string,
): { modelName: string; hasMetadata: boolean } {
const configuredName = providerConfig?.models.find((candidate) => candidate.id === model)?.name;
const hasDistinctModelMetadata =
normalizeOptionalLowercaseString(configuredName) !== normalizeOptionalLowercaseString(model);
return configuredName
? { modelName: configuredName, hasMetadata: hasDistinctModelMetadata }
: { modelName: model, hasMetadata: false };
}
function ensureMaiImageModel(
providerConfig: ModelProviderConfig | undefined,
model: string,
): { modelName: string; hasMetadata: boolean } {
const resolved = resolveConfiguredModelName(providerConfig, model);
const normalizedModel = normalizeOptionalLowercaseString(model);
if (
!isFoundryMaiImageModel(resolved.modelName) &&
(resolved.hasMetadata ||
(normalizedModel?.startsWith("mai-") && !normalizedModel.startsWith("mai-image-")))
) {
throw new Error(
`Microsoft Foundry image generation supports MAI image deployments only, got "${resolved.modelName}".`,
);
}
return resolved;
}
function isMaiImageEditModel(modelName: string): boolean {
const normalized = normalizeOptionalLowercaseString(modelName);
return normalized === "mai-image-2.5" || normalized === "mai-image-2.5-flash";
}
function resolveMaiImageSize(size: string | undefined): { width: number; height: number } {
if (!size) {
return DEFAULT_IMAGE_SIZE;
}
const match = size.match(/^(\d{1,5})x(\d{1,5})$/u);
if (!match) {
throw new Error(`Microsoft Foundry MAI image size must use WIDTHxHEIGHT, got "${size}".`);
}
const width = Number(match[1]);
const height = Number(match[2]);
if (
!Number.isInteger(width) ||
!Number.isInteger(height) ||
width < MAI_MIN_IMAGE_SIDE_PX ||
height < MAI_MIN_IMAGE_SIDE_PX ||
width * height > MAI_MAX_IMAGE_PIXELS
) {
throw new Error(
`Microsoft Foundry MAI image size must be at least 768x768 and at most 1,048,576 total pixels, got "${size}".`,
);
}
return { width, height };
}
function resolveGeneratedImageMaxBytes(req: {
cfg: { agents?: { defaults?: { mediaMaxMb?: number } } };
}): number {
const configured = req.cfg.agents?.defaults?.mediaMaxMb;
if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) {
return Math.floor(configured * MB);
}
return MAX_IMAGE_BYTES;
}
function assertSingleImageCount(count: number | undefined): void {
if (count === undefined || count === 1) {
return;
}
throw new Error("Microsoft Foundry MAI image models return one image per request.");
}
function resolveConfiguredEndpoint(params: {
providerConfig: ModelProviderConfig | undefined;
preparedBaseUrl?: string;
}): string {
const endpoint =
extractFoundryEndpoint(params.preparedBaseUrl) ??
extractFoundryEndpoint(params.providerConfig?.baseUrl) ??
extractFoundryEndpoint(process.env.AZURE_OPENAI_ENDPOINT);
if (!endpoint) {
throw new Error("Microsoft Foundry endpoint missing for MAI image generation.");
}
return endpoint;
}
function buildMaiImageUrl(baseUrl: string, mode: "generations" | "edits"): string {
return `${baseUrl.replace(/\/+$/u, "")}/images/${mode}`;
}
function buildRuntimeModel(params: {
providerConfig: ModelProviderConfig | undefined;
model: string;
modelName: string;
}): ProviderRuntimeModel {
const api = isFoundryProviderApi(params.providerConfig?.api)
? params.providerConfig.api
: DEFAULT_API;
return {
id: params.model,
name: params.modelName,
api,
provider: PROVIDER_ID,
baseUrl: params.providerConfig?.baseUrl ?? "",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 32_000,
maxTokens: 0,
};
}
async function resolveMaiImageAuth(params: {
req: ImageGenerationRequest;
providerConfig: ModelProviderConfig | undefined;
model: string;
modelName: string;
}): Promise<{ headers: Record<string, string>; baseUrl?: string }> {
const auth = await resolveApiKeyForProvider({
provider: PROVIDER_ID,
cfg: params.req.cfg,
agentDir: params.req.agentDir,
store: params.req.authStore,
});
if (!auth.apiKey) {
throw new Error("Microsoft Foundry API key missing");
}
if (auth.apiKey !== "__entra_id_dynamic__") {
return {
headers: {
"api-key": auth.apiKey,
},
};
}
const prepared = await prepareFoundryRuntimeAuth({
config: params.req.cfg,
agentDir: params.req.agentDir,
env: process.env,
provider: PROVIDER_ID,
modelId: params.model,
model: buildRuntimeModel({
providerConfig: params.providerConfig,
model: params.model,
modelName: params.modelName,
}),
apiKey: auth.apiKey,
authMode: auth.mode,
...(auth.profileId ? { profileId: auth.profileId } : {}),
});
if (!prepared?.apiKey) {
throw new Error("Microsoft Foundry Entra ID token missing after runtime auth refresh.");
}
return {
headers: {
Authorization: `Bearer ${prepared.apiKey}`,
},
...(prepared.baseUrl ? { baseUrl: prepared.baseUrl } : {}),
};
}
function buildEditFormData(params: {
req: ImageGenerationRequest;
image: ImageGenerationSourceImage;
model: string;
}): FormData {
const mimeType = normalizeOptionalLowercaseString(params.image.mimeType) ?? MAI_IMAGE_OUTPUT_MIME;
if (!MAI_IMAGE_UPLOAD_MIME_TYPES.has(mimeType)) {
throw new Error("Microsoft Foundry MAI image edits require a PNG or JPEG input image.");
}
const form = new FormData();
form.set("model", params.model);
form.set("prompt", params.req.prompt);
form.set(
"image",
new Blob([new Uint8Array(params.image.buffer)], {
type: mimeType === "image/jpg" ? "image/jpeg" : mimeType,
}),
imageSourceUploadFileName({
image: params.image,
index: 0,
fileNamePrefix: "microsoft-foundry-input",
}),
);
return form;
}
function parseMaiImageResponse(payload: unknown, label: string) {
const images = parseOpenAiCompatibleImageResponse(payload, {
defaultMimeType: MAI_IMAGE_OUTPUT_MIME,
fileNamePrefix: "microsoft-foundry-image",
malformedResponseError: `${label} response malformed`,
sniffMimeType: true,
});
if (images.length === 0) {
throw new Error(`${label} response missing image data`);
}
return images;
}
export function buildMicrosoftFoundryImageGenerationProvider(): ImageGenerationProvider {
return {
id: PROVIDER_ID,
label: "Microsoft Foundry",
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
models: [],
isConfigured: ({ agentDir }) =>
isProviderApiKeyConfigured({
provider: PROVIDER_ID,
agentDir,
}),
capabilities: {
generate: {
maxCount: MAI_IMAGE_MAX_RESULTS,
supportsSize: true,
},
edit: {
enabled: true,
maxCount: MAI_IMAGE_MAX_RESULTS,
maxInputImages: 1,
supportsSize: false,
},
output: {
formats: ["png"],
},
},
async generateImage(req): Promise<ImageGenerationResult> {
const providerConfig = readProviderConfig(req);
const model = normalizeOptionalString(req.model);
if (!model) {
throw new Error("Microsoft Foundry MAI image generation requires a deployment name.");
}
const { modelName, hasMetadata } = ensureMaiImageModel(providerConfig, model);
const inputImages = req.inputImages ?? [];
const mode = inputImages.length > 0 ? "edits" : "generations";
assertSingleImageCount(req.count);
if (inputImages.length > 1) {
throw new Error("Microsoft Foundry MAI image edits support one input image.");
}
if (
mode === "edits" &&
(hasMetadata || isFoundryMaiImageModel(model)) &&
!isMaiImageEditModel(modelName)
) {
throw new Error(`${modelName} does not support Microsoft Foundry MAI image edits.`);
}
if (mode === "edits" && !hasMetadata && !isFoundryMaiImageModel(model)) {
throw new Error(
"Microsoft Foundry MAI image edits require MAI-Image-2.5 model metadata for custom deployment names.",
);
}
const auth = await resolveMaiImageAuth({ req, providerConfig, model, modelName });
const endpoint = resolveConfiguredEndpoint({
providerConfig,
preparedBaseUrl: auth.baseUrl,
});
const resolvedBaseUrl = `${endpoint}${MAI_IMAGE_BASE_PATH}`;
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolvedBaseUrl,
defaultBaseUrl: resolvedBaseUrl,
allowPrivateNetwork: false,
defaultHeaders: auth.headers,
request: sanitizeConfiguredModelProviderRequest(providerConfig?.request),
provider: PROVIDER_ID,
capability: "image",
transport: "http",
});
const label =
mode === "edits"
? "Microsoft Foundry MAI image edit"
: "Microsoft Foundry MAI image generation";
const deadline = createProviderOperationDeadline({
timeoutMs: req.timeoutMs,
label,
});
const timeoutMs = resolveProviderOperationTimeoutMs({
deadline,
defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
});
const request =
mode === "edits"
? postMultipartRequest({
url: buildMaiImageUrl(baseUrl, mode),
headers: (() => {
const multipartHeaders = new Headers(headers);
multipartHeaders.delete("Content-Type");
return multipartHeaders;
})(),
body: buildEditFormData({
req,
image: inputImages[0],
model,
}),
timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
ssrfPolicy: req.ssrfPolicy,
dispatcherPolicy,
})
: postJsonRequest({
url: buildMaiImageUrl(baseUrl, mode),
headers: (() => {
const jsonHeaders = new Headers(headers);
jsonHeaders.set("Content-Type", "application/json");
return jsonHeaders;
})(),
body: {
model,
prompt: req.prompt,
...resolveMaiImageSize(req.size),
},
timeoutMs,
fetchFn: fetch,
allowPrivateNetwork,
ssrfPolicy: req.ssrfPolicy,
dispatcherPolicy,
});
const { response, release } = await request;
try {
await assertOkOrThrowHttpError(response, `${label} failed`);
const payload = await readProviderJsonResponse(
response,
"microsoft-foundry.image-generation",
{
maxBytes: resolveInlineImageJsonResponseMaxBytes(
MAI_IMAGE_MAX_RESULTS,
resolveGeneratedImageMaxBytes(req),
),
},
);
return {
images: parseMaiImageResponse(payload, label),
model,
};
} finally {
await release();
}
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
// Microsoft Foundry plugin entrypoint registers its OpenClaw integration.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildMicrosoftFoundryImageGenerationProvider } from "./image-generation-provider.js";
import { buildMicrosoftFoundryProvider } from "./provider.js";
export default definePluginEntry({
id: "microsoft-foundry",
name: "Microsoft Foundry Provider",
description: "Microsoft Foundry provider with Entra ID and API key auth",
register(api) {
api.registerProvider(buildMicrosoftFoundryProvider());
api.registerImageGenerationProvider(buildMicrosoftFoundryImageGenerationProvider());
},
});

View File

@@ -0,0 +1,73 @@
// Microsoft Foundry tests cover bounded connection-test error reads.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as cli from "./cli.js";
import { testFoundryConnection } from "./onboard.js";
import { DEFAULT_API } from "./shared.js";
const hoisted = vi.hoisted(() => ({
fetchWithSsrFGuard: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
fetchWithSsrFGuard: hoisted.fetchWithSsrFGuard,
}));
function cancelTrackedResponse(
text: string,
init: ResponseInit,
): {
response: Response;
wasCanceled: () => boolean;
} {
let canceled = false;
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode(text));
},
cancel() {
canceled = true;
},
});
return {
response: new Response(stream, init),
wasCanceled: () => canceled,
};
}
describe("testFoundryConnection", () => {
beforeEach(() => {
vi.spyOn(cli, "getAccessTokenResult").mockReturnValue({ accessToken: "token" });
});
afterEach(() => {
vi.restoreAllMocks();
hoisted.fetchWithSsrFGuard.mockReset();
});
it("bounds connection-test error bodies without using response.text()", async () => {
const note = vi.fn();
const tracked = cancelTrackedResponse(`${"foundry failure ".repeat(1024)}tail`, {
status: 503,
headers: { "content-type": "text/plain" },
});
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
hoisted.fetchWithSsrFGuard.mockResolvedValue({
response: tracked.response,
release: async () => {},
});
await testFoundryConnection({
ctx: { prompter: { note } } as never,
endpoint: "https://example.openai.azure.com",
modelId: "gpt-4o",
api: DEFAULT_API,
});
expect(textSpy).not.toHaveBeenCalled();
expect(tracked.wasCanceled()).toBe(true);
expect(note).toHaveBeenCalledWith(
expect.stringContaining("Warning: test request returned 503"),
"Connection Test",
);
});
});

View File

@@ -0,0 +1,640 @@
// Microsoft Foundry setup module handles plugin onboarding behavior.
import type { ProviderAuthContext } from "openclaw/plugin-sdk/core";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import {
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
azLoginDeviceCode,
azLoginDeviceCodeWithOptions,
execAz,
getAccessTokenResult,
getLoggedInAccount,
} from "./cli.js";
import {
type AzAccount,
type AzCognitiveAccount,
type AzDeploymentSummary,
type FoundryProviderApi,
type FoundryResourceOption,
type FoundrySelection,
buildFoundryProviderBaseUrl,
extractFoundryEndpoint,
requiresFoundryMaxCompletionTokens,
requiresFoundryEntraIdClaudeAuth,
requiresFoundryMandatoryAdaptiveClaudeThinking,
ANTHROPIC_MESSAGES_API,
DEFAULT_API,
DEFAULT_GPT5_API,
FOUNDRY_ANTHROPIC_SCOPE,
usesFoundryResponsesByDefault,
} from "./shared.js";
const FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
export { listSubscriptions } from "./cli.js";
function listFoundryResources(subscriptionId?: string): FoundryResourceOption[] {
try {
const accounts = JSON.parse(
execAz([
"cognitiveservices",
"account",
"list",
...(subscriptionId ? ["--subscription", subscriptionId] : []),
"--query",
"[].{id:id,name:name,kind:kind,location:location,resourceGroup:resourceGroup,endpoint:properties.endpoint,customSubdomain:properties.customSubDomainName,projects:properties.associatedProjects}",
"--output",
"json",
]),
) as AzCognitiveAccount[];
const resources: FoundryResourceOption[] = [];
for (const account of accounts) {
if (!account.resourceGroup) {
continue;
}
if (account.kind === "OpenAI") {
const endpoint = extractFoundryEndpoint(account.endpoint);
if (!endpoint) {
continue;
}
resources.push({
id: account.id,
accountName: account.name,
kind: "OpenAI",
location: account.location,
resourceGroup: account.resourceGroup,
endpoint,
projects: [],
});
continue;
}
if (account.kind !== "AIServices") {
continue;
}
const customSubdomain = normalizeOptionalString(account.customSubdomain);
const endpoint = customSubdomain
? `https://${customSubdomain}.services.ai.azure.com`
: undefined;
if (!endpoint) {
continue;
}
resources.push({
id: account.id,
accountName: account.name,
kind: "AIServices",
location: account.location,
resourceGroup: account.resourceGroup,
endpoint,
projects: Array.isArray(account.projects)
? account.projects.filter((project): project is string => typeof project === "string")
: [],
});
}
return resources;
} catch {
return [];
}
}
export function listResourceDeployments(
resource: FoundryResourceOption,
subscriptionId?: string,
): AzDeploymentSummary[] {
try {
const deployments = JSON.parse(
execAz([
"cognitiveservices",
"account",
"deployment",
"list",
...(subscriptionId ? ["--subscription", subscriptionId] : []),
"-g",
resource.resourceGroup,
"-n",
resource.accountName,
"--query",
"[].{name:name,modelName:properties.model.name,modelVersion:properties.model.version,state:properties.provisioningState,sku:sku.name}",
"--output",
"json",
]),
) as AzDeploymentSummary[];
return deployments.filter((deployment) => deployment.state === "Succeeded");
} catch {
return [];
}
}
function buildCreateFoundryHint(selectedSub: AzAccount): string {
return [
`No Azure AI Foundry or Azure OpenAI resources were found in subscription ${selectedSub.name} (${selectedSub.id}).`,
"Create one in Azure AI Foundry or Azure Portal, then rerun onboard.",
"Azure AI Foundry: https://ai.azure.com",
"Azure OpenAI docs: https://learn.microsoft.com/azure/ai-foundry/openai/how-to/create-resource",
].join("\n");
}
export async function selectFoundryResource(
ctx: ProviderAuthContext,
selectedSub: AzAccount,
): Promise<FoundryResourceOption> {
const resources = listFoundryResources(selectedSub.id);
if (resources.length === 0) {
throw new Error(buildCreateFoundryHint(selectedSub));
}
if (resources.length === 1) {
const only = resources[0];
await ctx.prompter.note(
`Using ${only.kind === "AIServices" ? "Azure AI Foundry" : "Azure OpenAI"} resource: ${only.accountName}`,
"Foundry Resource",
);
return only;
}
const selectedResourceId = await ctx.prompter.select({
message: "Select Azure AI Foundry / Azure OpenAI resource",
options: resources.map((resource) => ({
value: resource.id,
label: `${resource.accountName} (${resource.kind === "AIServices" ? "Azure AI Foundry" : "Azure OpenAI"}${resource.location ? `, ${resource.location}` : ""})`,
hint: [
`RG: ${resource.resourceGroup}`,
resource.projects.length > 0 ? `${resource.projects.length} project(s)` : undefined,
]
.filter(Boolean)
.join(" | "),
})),
});
return resources.find((resource) => resource.id === selectedResourceId) ?? resources[0];
}
export async function selectFoundryDeployment(
ctx: ProviderAuthContext,
resource: FoundryResourceOption,
deployments: AzDeploymentSummary[],
): Promise<{ selected: AzDeploymentSummary; supported: AzDeploymentSummary[] }> {
const supported = deployments;
if (supported.length === 0) {
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];
await ctx.prompter.note(`Using deployment: ${only.name}`, "Model Deployment");
return { selected: only, supported };
}
const selectedDeploymentName = await ctx.prompter.select({
message: "Select model deployment",
options: supported.map((deployment) => ({
value: deployment.name,
label: deployment.name,
hint: [deployment.modelName, deployment.modelVersion, deployment.sku]
.filter(Boolean)
.join(" | "),
})),
});
const selected =
supported.find((deployment) => deployment.name === selectedDeploymentName) ?? supported[0];
return { selected, supported };
}
async function promptFoundryApi(
ctx: ProviderAuthContext,
initialApi: FoundryProviderApi,
): Promise<FoundryProviderApi> {
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",
hint: "Recommended for Azure OpenAI GPT, o-series, and Codex deployments",
},
{
value: "openai-completions",
label: "Chat Completions API",
hint: "Use for Foundry models that only expose chat/completions semantics",
},
],
initialValue: initialApi,
});
}
type ManualFoundryModelFamilyChoice = "claude" | "reasoning-family" | "mai-image" | "other-chat";
type ManualFoundryMaiImageModel =
| "MAI-Image-2.5-Flash"
| "MAI-Image-2.5"
| "MAI-Image-2e"
| "MAI-Image-2";
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",
hint: "Use for Azure OpenAI reasoning and Codex deployments",
},
{
value: "mai-image",
label: "MAI image model",
hint: "Use for Microsoft MAI image deployments",
},
{
value: "other-chat",
label: "Other chat model",
hint: "Use for other chat/completions style Foundry models",
},
],
initialValue,
});
}
async function promptFoundryMaiImageModel(
ctx: ProviderAuthContext,
): Promise<ManualFoundryMaiImageModel> {
return await ctx.prompter.select({
message: "MAI image base model",
options: [
{
value: "MAI-Image-2.5-Flash",
label: "MAI-Image-2.5-Flash",
hint: "Latest fast MAI image deployment",
},
{
value: "MAI-Image-2.5",
label: "MAI-Image-2.5",
hint: "Latest MAI image deployment",
},
{
value: "MAI-Image-2e",
label: "MAI-Image-2e",
hint: "Efficient MAI image deployment",
},
{
value: "MAI-Image-2",
label: "MAI-Image-2",
hint: "MAI image deployment",
},
],
initialValue: "MAI-Image-2.5-Flash",
});
}
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.services.ai.azure.com or https://xxx.openai.azure.com",
...(options?.endpointInitialValue ? { initialValue: options.endpointInitialValue } : {}),
validate: (v) => {
const val = normalizeStringifiedOptionalString(v) ?? "";
if (!val) {
return "Endpoint URL is required";
}
return URL.canParse(val) ? undefined : "Invalid URL";
},
})
).trim();
const modelId = (
await ctx.prompter.text({
message: "Default model/deployment name",
...(options?.modelInitialValue ? { initialValue: options.modelInitialValue } : {}),
placeholder: "claude-fable-5",
validate: (v) => {
const val = normalizeStringifiedOptionalString(v) ?? "";
if (!val) {
return "Model ID is required";
}
return undefined;
},
})
).trim();
const familyChoice = await promptFoundryModelFamily(
ctx,
options?.modelFamilyInitialValue ?? "claude",
);
if (familyChoice === "mai-image") {
return {
endpoint,
modelId,
modelNameHint: await promptFoundryMaiImageModel(ctx),
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)
? modelId
: "gpt-5"
: undefined;
const api = await promptFoundryApi(
ctx,
familyChoice === "reasoning-family" ? DEFAULT_GPT5_API : DEFAULT_API,
);
return {
endpoint,
modelId,
...(resolvedModelName ? { modelNameHint: resolvedModelName } : {}),
api,
};
}
export async function promptEndpointAndModelManually(
ctx: ProviderAuthContext,
): Promise<FoundrySelection> {
return promptEndpointAndModelBase(ctx);
}
export async function promptApiKeyEndpointAndModel(
ctx: ProviderAuthContext,
): Promise<FoundrySelection> {
return promptEndpointAndModelBase(ctx, {
endpointInitialValue: process.env.AZURE_OPENAI_ENDPOINT,
modelInitialValue: "gpt-4o",
modelFamilyInitialValue: "other-chat",
allowEntraOnlyClaudeModels: false,
});
}
export function buildFoundryConnectionTest(params: {
endpoint: string;
modelId: string;
modelNameHint?: string | null;
api: FoundryProviderApi;
}): { url: string; body: Record<string, unknown> } {
const baseUrl = buildFoundryProviderBaseUrl(
params.endpoint,
params.modelId,
params.modelNameHint,
params.api,
);
if (params.api === DEFAULT_GPT5_API) {
return {
url: `${baseUrl}/responses`,
body: {
model: params.modelId,
input: "hi",
max_output_tokens: 16,
},
};
}
if (params.api === ANTHROPIC_MESSAGES_API) {
return {
url: `${baseUrl}/v1/messages`,
body: {
model: params.modelId,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
...(requiresFoundryMandatoryAdaptiveClaudeThinking(params.modelNameHint ?? params.modelId)
? { thinking: { type: "adaptive" } }
: {}),
},
};
}
return {
url: `${baseUrl}/chat/completions`,
body: {
model: params.modelId,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
},
};
}
function extractTenantSuggestions(rawMessage: string): Array<{ id: string; label?: string }> {
const suggestions: Array<{ id: string; label?: string }> = [];
const seen = new Set<string>();
const regex = /([0-9a-fA-F-]{36})(?:\s+'([^'\r\n]+)')?/g;
for (const match of rawMessage.matchAll(regex)) {
const id = normalizeOptionalString(match[1]);
if (!id || seen.has(id)) {
continue;
}
seen.add(id);
suggestions.push({
id,
...(normalizeOptionalString(match[2]) ? { label: normalizeOptionalString(match[2]) } : {}),
});
}
return suggestions;
}
export function isValidTenantIdentifier(value: string): boolean {
const trimmed = normalizeOptionalString(value) ?? "";
if (!trimmed) {
return false;
}
const isTenantUuid =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(trimmed);
const isTenantDomain =
/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/.test(
trimmed,
);
return isTenantUuid || isTenantDomain;
}
export async function promptTenantId(
ctx: ProviderAuthContext,
params?: {
suggestions?: Array<{ id: string; label?: string }>;
required?: boolean;
reason?: string;
},
): Promise<string | undefined> {
const suggestionLines =
params?.suggestions && params.suggestions.length > 0
? params.suggestions.map((entry) => `- ${entry.id}${entry.label ? ` (${entry.label})` : ""}`)
: [];
if (params?.reason || suggestionLines.length > 0) {
await ctx.prompter.note(
[
params?.reason,
suggestionLines.length > 0 ? "Suggested tenants:" : undefined,
...suggestionLines,
]
.filter(Boolean)
.join("\n"),
"Azure Tenant",
);
}
const tenantId = (
await ctx.prompter.text({
message: params?.required ? "Azure tenant ID" : "Azure tenant ID (optional)",
placeholder: params?.suggestions?.[0]?.id ?? "00000000-0000-0000-0000-000000000000",
validate: (value) => {
const trimmed = normalizeStringifiedOptionalString(value) ?? "";
if (!trimmed) {
return params?.required ? "Tenant ID is required" : undefined;
}
return isValidTenantIdentifier(trimmed)
? undefined
: "Enter a valid tenant ID or tenant domain";
},
})
).trim();
return tenantId || undefined;
}
export async function loginWithTenantFallback(
ctx: ProviderAuthContext,
): Promise<{ account: AzAccount | null; tenantId?: string }> {
try {
await azLoginDeviceCode();
return { account: getLoggedInAccount() };
} catch (error) {
const message = formatErrorMessage(error);
const isAzureTenantError =
/AADSTS\d+/i.test(message) ||
/no subscriptions found/i.test(message) ||
/Please provide a valid tenant/i.test(message) ||
/tenant.*not found/i.test(message);
if (!isAzureTenantError) {
throw error;
}
const tenantId = await promptTenantId(ctx, {
suggestions: extractTenantSuggestions(message),
required: true,
reason:
"Azure login needs a tenant-scoped retry. This often happens when your tenant requires MFA or your account has no Azure subscriptions.",
});
await azLoginDeviceCodeWithOptions({
tenantId,
allowNoSubscriptions: true,
});
return {
account: getLoggedInAccount(),
tenantId,
};
}
}
export async function testFoundryConnection(params: {
ctx: ProviderAuthContext;
endpoint: string;
modelId: string;
modelNameHint?: string;
api: FoundryProviderApi;
subscriptionId?: string;
tenantId?: string;
}): Promise<void> {
try {
const { accessToken } = getAccessTokenResult({
scope: params.api === ANTHROPIC_MESSAGES_API ? FOUNDRY_ANTHROPIC_SCOPE : undefined,
subscriptionId: params.subscriptionId,
tenantId: params.tenantId,
});
const testRequest = buildFoundryConnectionTest({
endpoint: params.endpoint,
modelId: params.modelId,
modelNameHint: params.modelNameHint,
api: params.api,
});
const { response: res, release } = await fetchWithSsrFGuard({
url: testRequest.url,
init: {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
...(params.api === ANTHROPIC_MESSAGES_API ? { "anthropic-version": "2023-06-01" } : {}),
},
body: JSON.stringify(testRequest.body),
},
timeoutMs: 15_000,
});
try {
if (res.status === 400) {
const body = await readResponseTextLimited(
res,
FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES,
).catch(() => "");
await params.ctx.prompter.note(
`Endpoint is reachable but returned 400 Bad Request - check your deployment name and API version.\n${body.slice(0, 200)}`,
"Connection Test",
);
} else if (!res.ok) {
const body = await readResponseTextLimited(
res,
FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES,
).catch(() => "");
await params.ctx.prompter.note(
`Warning: test request returned ${res.status}. ${body.slice(0, 200)}\nProceeding anyway - you can fix the endpoint later.`,
"Connection Test",
);
} else {
await params.ctx.prompter.note("Connection test successful!", "✓");
}
} finally {
await release();
}
} catch (err) {
await params.ctx.prompter.note(
`Warning: connection test failed: ${String(err)}\nProceeding anyway.`,
"Connection Test",
);
}
}

View File

@@ -0,0 +1,57 @@
{
"id": "microsoft-foundry",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["microsoft-foundry"],
"setup": {
"providers": [
{
"id": "microsoft-foundry",
"envVars": ["AZURE_OPENAI_API_KEY"]
}
]
},
"contracts": {
"imageGenerationProviders": ["microsoft-foundry"]
},
"imageGenerationProviderMetadata": {
"microsoft-foundry": {
"authSignals": [
{
"provider": "microsoft-foundry"
}
]
}
},
"providerAuthChoices": [
{
"provider": "microsoft-foundry",
"method": "entra-id",
"choiceId": "microsoft-foundry-entra",
"choiceLabel": "Microsoft Foundry (Entra ID / az login)",
"choiceHint": "Use your Azure login — no API key needed",
"onboardingScopes": ["text-inference", "image-generation"],
"groupId": "microsoft-foundry",
"groupLabel": "Microsoft Foundry",
"groupHint": "Entra ID + API key"
},
{
"provider": "microsoft-foundry",
"method": "api-key",
"choiceId": "microsoft-foundry-apikey",
"choiceLabel": "Microsoft Foundry (API key)",
"choiceHint": "Use an Azure OpenAI API key directly",
"onboardingScopes": ["text-inference", "image-generation"],
"groupId": "microsoft-foundry",
"groupLabel": "Microsoft Foundry",
"groupHint": "Entra ID + API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "@openclaw/microsoft-foundry",
"version": "2026.6.11",
"private": true,
"description": "OpenClaw Microsoft Foundry provider plugin",
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -0,0 +1,263 @@
// Microsoft Foundry provider module implements model/runtime integration.
import type { ProviderNormalizeResolvedModelContext } from "openclaw/plugin-sdk/core";
import {
resolveClaudeThinkingProfile,
supportsClaudeNativeMaxEffort,
type ModelProviderConfig,
type ProviderPlugin,
} from "openclaw/plugin-sdk/provider-model-shared";
import { OPENAI_RESPONSES_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
import { apiKeyAuthMethod, entraIdAuthMethod } from "./auth.js";
import { prepareFoundryRuntimeAuth } from "./runtime.js";
import {
PROVIDER_ID,
applyFoundryProfileBinding,
applyFoundryProviderConfig,
buildFoundryProviderBaseUrl,
extractFoundryEndpoint,
isFoundryClaudeMythosPreview,
isFoundryProviderApi,
mergeFoundryCanonicalModelParams,
normalizeFoundryEndpoint,
resolveFoundryModelCapabilities,
resolveFoundryTargetProfileId,
} from "./shared.js";
type FoundryProviderHooks = Pick<ProviderPlugin, "wrapStreamFn">;
const wrapOpenAIResponsesStreamFn = OPENAI_RESPONSES_STREAM_HOOKS.wrapStreamFn;
const wrapMicrosoftFoundryStreamFn: NonNullable<FoundryProviderHooks["wrapStreamFn"]> = (ctx) => {
if (ctx.model?.api !== "openai-responses") {
return ctx.streamFn ?? null;
}
const baseStreamFn = ctx.streamFn;
if (!baseStreamFn) {
return wrapOpenAIResponsesStreamFn?.(ctx) ?? null;
}
const streamFnWithResponsesReplayIds: NonNullable<typeof ctx.streamFn> = (
model,
context,
options,
) =>
baseStreamFn(model, context, {
...options,
// Foundry validates encrypted reasoning replay against the original item id,
// even though its Responses endpoint does not support persisted `store`.
replayResponsesItemIds: true,
} as typeof options & { replayResponsesItemIds: true });
return (
wrapOpenAIResponsesStreamFn?.({
...ctx,
streamFn: streamFnWithResponsesReplayIds,
}) ?? streamFnWithResponsesReplayIds
);
};
export function buildMicrosoftFoundryProvider(): ProviderPlugin {
return {
id: PROVIDER_ID,
label: "Microsoft Foundry",
docsPath: "/providers/models",
envVars: ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"],
auth: [entraIdAuthMethod, apiKeyAuthMethod],
onModelSelected: async (ctx) => {
const providerConfig = ctx.config.models?.providers?.[PROVIDER_ID];
if (
!providerConfig ||
!providerConfig.baseUrl?.trim() ||
!Array.isArray(providerConfig.models) ||
!ctx.model.startsWith(`${PROVIDER_ID}/`)
) {
return;
}
const selectedModelId = ctx.model.slice(`${PROVIDER_ID}/`.length);
const configuredModels = providerConfig.models ?? [];
const existingModel = configuredModels.find(
(model: { id: string }) => model.id === selectedModelId,
);
const existingModelApi = isFoundryProviderApi(existingModel?.api)
? existingModel.api
: undefined;
const providerApiForExistingModel =
existingModel && isFoundryProviderApi(providerConfig.api) ? providerConfig.api : undefined;
const selectedModelCapabilities = resolveFoundryModelCapabilities(
selectedModelId,
existingModel?.name,
existingModelApi ?? providerApiForExistingModel,
existingModel?.input,
);
const providerEndpoint = normalizeFoundryEndpoint(providerConfig.baseUrl ?? "");
const selectedProviderEndpoint =
extractFoundryEndpoint(existingModel?.baseUrl) ?? providerEndpoint;
const nextModels = configuredModels.map((model) => {
if (model.id !== selectedModelId) {
return model;
}
const selectedModelEndpoint = extractFoundryEndpoint(model.baseUrl) ?? providerEndpoint;
const selectedModelBaseUrl = buildFoundryProviderBaseUrl(
selectedModelEndpoint,
selectedModelId,
selectedModelCapabilities.modelName,
selectedModelCapabilities.api,
);
const nextModel = Object.assign({}, model, {
name: selectedModelCapabilities.modelName,
api: selectedModelCapabilities.api,
baseUrl: selectedModelBaseUrl,
reasoning: selectedModelCapabilities.reasoning || model.reasoning,
thinkingLevelMap: selectedModelCapabilities.thinkingLevelMap ?? model.thinkingLevelMap,
params: mergeFoundryCanonicalModelParams(
model.params,
selectedModelCapabilities.modelName,
),
input: selectedModelCapabilities.input,
});
if (selectedModelCapabilities.compat) {
const explicitSupportsReasoningEffort =
typeof model.compat?.supportsReasoningEffort === "boolean"
? model.compat.supportsReasoningEffort
: undefined;
const preserveExplicitReasoningEffort =
!selectedModelCapabilities.reasoning &&
model.reasoning &&
explicitSupportsReasoningEffort !== false;
const explicitMaxTokensField =
typeof model.compat?.maxTokensField === "string"
? model.compat.maxTokensField
: preserveExplicitReasoningEffort
? "max_completion_tokens"
: undefined;
nextModel.compat = {
...model.compat,
...selectedModelCapabilities.compat,
...(explicitSupportsReasoningEffort !== undefined
? { supportsReasoningEffort: explicitSupportsReasoningEffort }
: preserveExplicitReasoningEffort
? { supportsReasoningEffort: true }
: undefined),
...(explicitMaxTokensField ? { maxTokensField: explicitMaxTokensField } : {}),
};
}
return nextModel;
});
if (!nextModels.some((model) => model.id === selectedModelId)) {
nextModels.push({
id: selectedModelId,
name: selectedModelCapabilities.modelName,
api: selectedModelCapabilities.api,
baseUrl: buildFoundryProviderBaseUrl(
providerEndpoint,
selectedModelId,
selectedModelCapabilities.modelName,
selectedModelCapabilities.api,
),
reasoning: selectedModelCapabilities.reasoning,
...(selectedModelCapabilities.thinkingLevelMap
? { thinkingLevelMap: selectedModelCapabilities.thinkingLevelMap }
: {}),
params: mergeFoundryCanonicalModelParams(undefined, selectedModelCapabilities.modelName),
input: selectedModelCapabilities.input,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: selectedModelCapabilities.contextWindow,
maxTokens: selectedModelCapabilities.maxTokens,
...(selectedModelCapabilities.compat ? { compat: selectedModelCapabilities.compat } : {}),
});
}
const nextProviderConfig: ModelProviderConfig = {
...providerConfig,
baseUrl: buildFoundryProviderBaseUrl(
selectedProviderEndpoint,
selectedModelId,
selectedModelCapabilities.modelName,
selectedModelCapabilities.api,
),
api: selectedModelCapabilities.api,
models: nextModels,
};
const targetProfileId = resolveFoundryTargetProfileId(ctx.config);
if (targetProfileId) {
applyFoundryProfileBinding(ctx.config, targetProfileId);
}
applyFoundryProviderConfig(ctx.config, nextProviderConfig);
},
resolveThinkingProfile: ({ modelId, params }) => {
const modelName =
typeof params?.canonicalModelId === "string" ? params.canonicalModelId : undefined;
const capabilities = resolveFoundryModelCapabilities(modelId, modelName);
if (!capabilities.reasoning || capabilities.api !== "anthropic-messages") {
return undefined;
}
const profile = resolveClaudeThinkingProfile(capabilities.modelName, undefined, {
includeNativeMax: supportsClaudeNativeMaxEffort({ id: capabilities.modelName }),
});
if (!isFoundryClaudeMythosPreview(capabilities.modelName)) {
return profile;
}
const levels = profile.levels.filter((level) => level.id !== "off");
return {
...profile,
defaultLevel: "adaptive",
levels: levels.some((level) => level.id === "adaptive")
? levels
: [...levels, { id: "adaptive" }],
};
},
normalizeResolvedModel: ({ modelId, model }: ProviderNormalizeResolvedModelContext) => {
const endpoint = extractFoundryEndpoint(model.baseUrl ?? "");
if (!endpoint) {
return model;
}
const capabilities = resolveFoundryModelCapabilities(
modelId,
model.name,
isFoundryProviderApi(model.api) ? model.api : undefined,
model.input,
);
const explicitSupportsReasoningEffort =
typeof model.compat?.supportsReasoningEffort === "boolean"
? model.compat.supportsReasoningEffort
: undefined;
const preserveExplicitReasoningEffort = !capabilities.reasoning && model.reasoning;
const explicitMaxTokensField =
typeof model.compat?.maxTokensField === "string"
? model.compat.maxTokensField
: preserveExplicitReasoningEffort
? "max_completion_tokens"
: undefined;
const compat = capabilities.compat
? {
...model.compat,
...capabilities.compat,
...(explicitSupportsReasoningEffort !== undefined
? { supportsReasoningEffort: explicitSupportsReasoningEffort }
: preserveExplicitReasoningEffort
? { supportsReasoningEffort: true }
: undefined),
...(explicitMaxTokensField ? { maxTokensField: explicitMaxTokensField } : {}),
}
: undefined;
return {
...model,
name: capabilities.modelName,
api: capabilities.api,
reasoning: capabilities.reasoning || model.reasoning,
thinkingLevelMap: capabilities.thinkingLevelMap ?? model.thinkingLevelMap,
params: mergeFoundryCanonicalModelParams(model.params, capabilities.modelName),
input: capabilities.input,
baseUrl: buildFoundryProviderBaseUrl(
endpoint,
modelId,
capabilities.modelName,
capabilities.api,
),
...(compat ? { compat } : {}),
};
},
wrapStreamFn: wrapMicrosoftFoundryStreamFn,
prepareRuntimeAuth: prepareFoundryRuntimeAuth,
};
}

View File

@@ -0,0 +1,149 @@
// Microsoft Foundry plugin module implements runtime behavior.
import type {
ProviderPreparedRuntimeAuth,
ProviderPrepareRuntimeAuthContext,
} from "openclaw/plugin-sdk/core";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import {
asDateTimestampMs,
resolveDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import { ensureAuthProfileStore } from "openclaw/plugin-sdk/provider-auth";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { getAccessTokenResultAsync } from "./cli.js";
import {
ANTHROPIC_MESSAGES_API,
type CachedTokenEntry,
FOUNDRY_ANTHROPIC_SCOPE,
TOKEN_REFRESH_MARGIN_MS,
buildFoundryProviderBaseUrl,
extractFoundryEndpoint,
getFoundryTokenCacheKey,
isFoundryProviderApi,
resolveConfiguredModelNameHint,
} from "./shared-runtime.js";
const cachedTokens = new Map<string, CachedTokenEntry>();
const refreshPromises = new Map<string, Promise<{ apiKey: string; expiresAt: number }>>();
const FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS = 55 * 60 * 1000;
export function resetFoundryRuntimeAuthCaches(): void {
cachedTokens.clear();
refreshPromises.clear();
}
async function refreshEntraToken(params?: {
scope?: string;
subscriptionId?: string;
tenantId?: string;
}): Promise<{ apiKey: string; expiresAt: number }> {
const result = await getAccessTokenResultAsync(params);
const rawExpiry = result.expiresOn ? new Date(result.expiresOn).getTime() : Number.NaN;
const now = resolveDateTimestampMs(Date.now());
const expiresAt =
asDateTimestampMs(rawExpiry) ??
resolveExpiresAtMsFromDurationMs(FOUNDRY_TOKEN_FALLBACK_LIFETIME_MS, { nowMs: now }) ??
now;
cachedTokens.set(getFoundryTokenCacheKey(params), {
token: result.accessToken,
expiresAt,
});
return { apiKey: result.accessToken, expiresAt };
}
export async function prepareFoundryRuntimeAuth(
ctx: ProviderPrepareRuntimeAuthContext,
): Promise<ProviderPreparedRuntimeAuth> {
if (ctx.apiKey !== "__entra_id_dynamic__") {
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, {
allowKeychainPrompt: false,
});
const credential = ctx.profileId ? authStore.profiles[ctx.profileId] : undefined;
const metadata = credential?.type === "api_key" ? credential.metadata : undefined;
const modelId =
normalizeOptionalString(ctx.modelId) ??
normalizeOptionalString(metadata?.modelId) ??
ctx.modelId;
const requestedModelId = normalizeOptionalString(ctx.modelId);
const metadataModelId = normalizeOptionalString(metadata?.modelId);
const activeModelUsesMetadata = !requestedModelId || requestedModelId === metadataModelId;
const activeModelNameHint = activeModelUsesMetadata ? metadata?.modelName : undefined;
const modelNameHint = resolveConfiguredModelNameHint(
modelId,
ctx.model.name ?? activeModelNameHint,
);
const configuredApi = isFoundryProviderApi(ctx.model.api)
? ctx.model.api
: activeModelUsesMetadata &&
typeof metadata?.api === "string" &&
isFoundryProviderApi(metadata.api)
? metadata.api
: undefined;
const endpoint =
extractFoundryEndpoint(ctx.model.baseUrl ?? "") ??
normalizeOptionalString(metadata?.endpoint);
const tokenScope =
configuredApi === ANTHROPIC_MESSAGES_API ? FOUNDRY_ANTHROPIC_SCOPE : undefined;
const baseUrl = endpoint
? buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, configuredApi)
: undefined;
const cacheKey = getFoundryTokenCacheKey({
scope: tokenScope,
subscriptionId: metadata?.subscriptionId,
tenantId: metadata?.tenantId,
});
const cachedToken = cachedTokens.get(cacheKey);
const rawNow = Date.now();
const hasValidClock = asDateTimestampMs(rawNow) !== undefined;
const now = resolveDateTimestampMs(rawNow);
const refreshAfterMs =
resolveExpiresAtMsFromDurationMs(TOKEN_REFRESH_MARGIN_MS, { nowMs: now }) ?? now;
if (cachedToken && hasValidClock && cachedToken.expiresAt > refreshAfterMs) {
return {
apiKey: cachedToken.token,
expiresAt: cachedToken.expiresAt,
...(baseUrl ? { baseUrl } : {}),
request: {
auth: { mode: "authorization-bearer" as const, token: cachedToken.token },
},
};
}
let refreshPromise = refreshPromises.get(cacheKey);
if (!refreshPromise) {
refreshPromise = refreshEntraToken({
scope: tokenScope,
subscriptionId: metadata?.subscriptionId,
tenantId: metadata?.tenantId,
}).finally(() => {
refreshPromises.delete(cacheKey);
});
refreshPromises.set(cacheKey, refreshPromise);
}
const token = await refreshPromise;
return {
...token,
...(baseUrl ? { baseUrl } : {}),
request: {
auth: { mode: "authorization-bearer" as const, token: token.apiKey },
},
};
} catch (err) {
const details = formatErrorMessage(err);
throw new Error(`Failed to refresh Azure Entra ID token via az CLI: ${details}`, {
cause: err,
});
}
}

View File

@@ -0,0 +1,19 @@
// Microsoft Foundry plugin module implements shared runtime behavior.
export {
TOKEN_REFRESH_MARGIN_MS,
buildFoundryProviderBaseUrl,
extractFoundryEndpoint,
FOUNDRY_ANTHROPIC_SCOPE,
isFoundryProviderApi,
resolveConfiguredModelNameHint,
ANTHROPIC_MESSAGES_API,
type CachedTokenEntry,
} from "./shared.js";
export function getFoundryTokenCacheKey(params?: {
scope?: string;
subscriptionId?: string;
tenantId?: string;
}): string {
return `${params?.scope ?? ""}:${params?.subscriptionId ?? ""}:${params?.tenantId ?? ""}`;
}

View File

@@ -0,0 +1,769 @@
// Microsoft Foundry plugin module implements shared behavior.
import type { AuthConfig } from "openclaw/plugin-sdk/config-contracts";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
type ProviderAuthResult,
type SecretInput,
} from "openclaw/plugin-sdk/provider-auth";
import {
resolveClaudeFable5ModelIdentity,
supportsClaudeAdaptiveThinking,
supportsClaudeNativeXhighEffort,
type ModelApi,
type ModelProviderConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
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;
export interface AzAccount {
name: string;
id: string;
tenantId?: string;
user?: { name?: string };
state?: string;
isDefault?: boolean;
}
export interface AzAccessToken {
accessToken: string;
expiresOn?: string;
}
export interface AzCognitiveAccount {
id: string;
name: string;
kind: string;
location?: string;
resourceGroup?: string;
endpoint?: string | null;
customSubdomain?: string | null;
projects?: string[] | null;
}
export interface FoundryResourceOption {
id: string;
accountName: string;
kind: "AIServices" | "OpenAI";
location?: string;
resourceGroup: string;
endpoint: string;
projects: string[];
}
export interface AzDeploymentSummary {
name: string;
modelName?: string;
modelVersion?: string;
state?: string;
sku?: string;
}
export type FoundrySelection = {
endpoint: string;
modelId: string;
modelNameHint?: string;
api: FoundryProviderApi;
};
export type CachedTokenEntry = {
token: string;
expiresAt: number;
};
export type FoundryProviderApi =
| typeof DEFAULT_API
| typeof DEFAULT_GPT5_API
| typeof ANTHROPIC_MESSAGES_API;
type FoundryDeploymentConfigInput = {
name: string;
modelName?: string;
api?: FoundryProviderApi;
};
type FoundryModelCapabilities = {
modelName: string;
api: FoundryProviderApi;
reasoning: boolean;
thinkingLevelMap?: Record<string, string | null>;
input: Array<"text" | "image">;
contextWindow: number;
maxTokens: number;
compat?: FoundryModelCompat;
};
type FoundryProviderConfigPatch = Omit<ModelProviderConfig, "apiKey" | "headers"> & {
apiKey?: SecretInput | undefined;
headers?: Record<string, SecretInput> | undefined;
};
function normalizeModelInput(input?: unknown): Array<"text" | "image"> {
const normalized = Array.isArray(input)
? input.filter((item): item is "text" | "image" => item === "text" || item === "image")
: [];
return normalized.length > 0 ? normalized : ["text"];
}
type FoundryModelCompat = {
supportsStore?: boolean;
supportsReasoningEffort?: boolean;
supportedReasoningEfforts?: string[];
maxTokensField: "max_completion_tokens" | "max_tokens";
};
type FoundryConfigShape = {
auth?: AuthConfig;
models?: {
providers?: Record<string, ModelProviderConfig>;
};
};
type FoundryImageDefaultPatch = {
agents?: {
defaults?: {
imageGenerationModel?: {
primary: string;
};
};
};
};
function normalizeFoundryModelName(value?: string | null): string | undefined {
const trimmed = normalizeLowercaseStringOrEmpty(value);
return trimmed || undefined;
}
export function isAnthropicFoundryDeployment(modelName?: string | null): boolean {
const normalized = normalizeFoundryModelName(modelName);
return normalized ? normalized.startsWith("claude") : false;
}
export function isFoundryClaudeMythosPreview(value?: string | null): boolean {
return normalizeFoundryModelName(value) === "claude-mythos-preview";
}
export function usesFoundryResponsesByDefault(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
if (!normalized) {
return false;
}
return (
normalized.startsWith("gpt-") ||
normalized.startsWith("o1") ||
normalized.startsWith("o3") ||
normalized.startsWith("o4") ||
normalized.startsWith("deepseek-v4") ||
normalized === "computer-use-preview"
);
}
export function isFoundryMaiImageModel(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
if (!normalized) {
return false;
}
return (
normalized === "mai-image-2.5-flash" ||
normalized === "mai-image-2.5" ||
normalized === "mai-image-2e" ||
normalized === "mai-image-2" ||
normalized === "mai-image-2-efficient"
);
}
export function supportsFoundryReasoningContent(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
return normalized === "mai-ds-r1" || normalized === "mai-thinking-1";
}
export function supportsFoundryImageInput(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
if (!normalized) {
return false;
}
return (
isAnthropicFoundryDeployment(normalized) ||
normalized.startsWith("gpt-") ||
normalized.startsWith("o1") ||
normalized.startsWith("o3") ||
normalized.startsWith("o4") ||
normalized === "computer-use-preview"
);
}
export function requiresFoundryEntraIdClaudeAuth(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
return normalized
? normalized === "claude-mythos-preview" || normalized.startsWith("claude-mythos-")
: false;
}
export function requiresFoundryMandatoryAdaptiveClaudeThinking(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
return normalized
? resolveClaudeFable5ModelIdentity({ id: normalized }) !== undefined ||
normalized === "claude-mythos-preview" ||
normalized.startsWith("claude-mythos-")
: 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;
} {
const normalized = normalizeFoundryModelName(value);
const normalizedVersion = normalized?.replace(/\./g, "-");
if (
normalized &&
(supportsClaudeAdaptiveThinking({ id: normalized }) ||
requiresFoundryMandatoryAdaptiveClaudeThinking(normalized))
) {
return { contextWindow: 1_000_000, maxTokens: 128_000 };
}
if (
normalizedVersion === "claude-opus-4-5" ||
normalizedVersion === "claude-sonnet-4-5" ||
normalizedVersion === "claude-haiku-4-5"
) {
return { contextWindow: 200_000, maxTokens: 64_000 };
}
if (normalizedVersion === "claude-opus-4-1") {
return { contextWindow: 200_000, maxTokens: 32_000 };
}
if (normalized === "mai-ds-r1") {
return { contextWindow: 163_840, maxTokens: 163_840 };
}
return { contextWindow: 128_000, maxTokens: 16_384 };
}
export function requiresFoundryMaxCompletionTokens(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
if (!normalized) {
return false;
}
return (
normalized.startsWith("gpt-5") ||
normalized.startsWith("o1") ||
normalized.startsWith("o3") ||
normalized.startsWith("o4")
);
}
export function supportsFoundryReasoningEffort(value?: string | null): boolean {
const normalized = normalizeFoundryModelName(value);
if (
!normalized ||
/^gpt-5-chat(?:-|$)/u.test(normalized) ||
/^o1-mini(?:-|$)/u.test(normalized)
) {
return false;
}
return (
normalized.startsWith("gpt-5") ||
normalized.startsWith("o1") ||
normalized.startsWith("o3") ||
normalized.startsWith("o4")
);
}
function resolveFoundryReasoningEfforts(value?: string | null): string[] | undefined {
const normalized = normalizeFoundryModelName(value);
if (!normalized || !supportsFoundryReasoningEffort(normalized)) {
return undefined;
}
if (normalized === "gpt-5.1-codex-max") {
return ["none", "medium", "high", "xhigh"];
}
if (normalized === "gpt-5-pro") {
return ["high"];
}
if (/^gpt-5\.[2-9](?:\.|-|$)/u.test(normalized)) {
return ["none", "low", "medium", "high"];
}
if (/^gpt-5\.1(?:-|$)/u.test(normalized)) {
return ["none", "low", "medium", "high"];
}
if (/^gpt-5-codex(?:-|$)/u.test(normalized)) {
return ["low", "medium", "high"];
}
if (/^gpt-5(?:-|$)/u.test(normalized)) {
return ["minimal", "low", "medium", "high"];
}
return ["low", "medium", "high"];
}
function buildFoundryThinkingLevelMap(
efforts: string[] | undefined,
): Record<string, string | null> | undefined {
if (!efforts) {
return undefined;
}
const supported = new Set(efforts);
return {
off: supported.has("none") ? "none" : null,
minimal: supported.has("minimal") ? "minimal" : null,
low: supported.has("low") ? "low" : null,
medium: supported.has("medium") ? "medium" : null,
high: supported.has("high") ? "high" : null,
xhigh: supported.has("xhigh") ? "xhigh" : null,
max: null,
};
}
export function isFoundryProviderApi(value?: string | null): value is FoundryProviderApi {
return value === DEFAULT_API || value === DEFAULT_GPT5_API || value === ANTHROPIC_MESSAGES_API;
}
export function formatFoundryApiLabel(api: FoundryProviderApi): string {
return api === DEFAULT_GPT5_API
? "Responses"
: api === ANTHROPIC_MESSAGES_API
? "Anthropic Messages"
: "Chat Completions";
}
export function normalizeFoundryEndpoint(endpoint: string): string {
const trimmed = normalizeOptionalString(endpoint) ?? "";
if (!trimmed) {
return trimmed;
}
try {
const parsed = new URL(trimmed);
parsed.search = "";
parsed.hash = "";
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|anthropic)(?:$|\/).*/i, "");
}
}
function buildFoundryV1BaseUrl(endpoint: string): string {
const base = normalizeFoundryEndpoint(endpoint);
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,
configuredApi?: ModelApi | null,
): FoundryProviderApi {
if (isFoundryProviderApi(configuredApi)) {
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,
): string {
const resolvedApi = resolveFoundryApi(modelId, modelNameHint, configuredApi);
return resolvedApi === ANTHROPIC_MESSAGES_API
? buildFoundryAnthropicBaseUrl(endpoint)
: buildFoundryV1BaseUrl(endpoint);
}
export function extractFoundryEndpoint(baseUrl: string | null | undefined): string | undefined {
if (!baseUrl) {
return undefined;
}
try {
return normalizeFoundryEndpoint(baseUrl);
} catch {
return undefined;
}
}
function buildFoundryModelCompat(
modelId: string,
modelNameHint?: string | null,
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);
const supportedReasoningEfforts = resolveFoundryReasoningEfforts(configuredModelName);
if (resolvedApi !== DEFAULT_GPT5_API) {
return {
supportsReasoningEffort,
...(supportedReasoningEfforts ? { supportedReasoningEfforts } : {}),
maxTokensField: needsMaxCompletionTokens ? "max_completion_tokens" : "max_tokens",
};
}
return {
...(resolvedApi === DEFAULT_GPT5_API ? { supportsStore: false } : {}),
...(supportsReasoningEffort ? { supportsReasoningEffort, supportedReasoningEfforts } : {}),
maxTokensField: needsMaxCompletionTokens ? "max_completion_tokens" : "max_tokens",
};
}
export function resolveFoundryModelCapabilities(
modelId: string,
modelNameHint?: string | null,
configuredApi?: ModelApi | null,
existingInput?: unknown,
): FoundryModelCapabilities {
const modelName = resolveConfiguredModelNameHint(modelId, modelNameHint) ?? modelId;
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({ id: modelName }) ||
supportsFoundryManualClaudeThinking(modelName) ||
requiresFoundryMandatoryAdaptiveClaudeThinking(modelName));
const supportsClaudeXhighThinking =
isAnthropic && supportsClaudeNativeXhighEffort({ id: modelName });
const tokenLimits = resolveFoundryModelTokenLimits(modelName);
return {
modelName,
api,
reasoning:
supportsClaudeThinking ||
supportsFoundryReasoningEffort(modelName) ||
supportsFoundryReasoningContent(modelName),
...(supportsClaudeXhighThinking
? { thinkingLevelMap: { xhigh: "xhigh", max: "max" } }
: supportedReasoningEfforts
? { thinkingLevelMap: buildFoundryThinkingLevelMap(supportedReasoningEfforts) }
: {}),
input:
normalizedInput.includes("image") || supportsFoundryImageInput(modelName)
? ["text", "image"]
: normalizedInput,
contextWindow: tokenLimits.contextWindow,
maxTokens: tokenLimits.maxTokens,
compat: buildFoundryModelCompat(modelId, modelName, api),
};
}
export function mergeFoundryCanonicalModelParams(
params: Record<string, unknown> | undefined,
modelName: string,
): Record<string, unknown> {
return {
...params,
canonicalModelId: modelName,
};
}
export function resolveConfiguredModelNameHint(
modelId: string,
modelNameHint?: string | null,
): string | undefined {
const trimmedName = normalizeOptionalString(modelNameHint) ?? "";
if (trimmedName) {
return trimmedName;
}
const trimmedId = normalizeOptionalString(modelId) ?? "";
return trimmedId ? trimmedId : undefined;
}
function buildFoundryProviderConfig(
endpoint: string,
modelId: string,
modelNameHint?: string | null,
options?: {
api?: FoundryProviderApi;
deployments?: FoundryDeploymentConfigInput[];
},
): FoundryProviderConfigPatch {
const resolvedApi = resolveFoundryApi(modelId, modelNameHint, options?.api);
const deployments = options?.deployments?.length
? options.deployments
: [{ name: modelId, modelName: modelNameHint ?? undefined, api: resolvedApi }];
return {
baseUrl: buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, resolvedApi),
api: resolvedApi,
authHeader: undefined,
apiKey: undefined,
headers: undefined,
models: deployments.map((deployment) => {
const capabilities = resolveFoundryModelCapabilities(
deployment.name,
deployment.modelName,
deployment.api ?? resolvedApi,
);
const modelBaseUrl = buildFoundryProviderBaseUrl(
endpoint,
deployment.name,
capabilities.modelName,
capabilities.api,
);
return Object.assign(
{
id: deployment.name,
name: capabilities.modelName,
api: capabilities.api,
baseUrl: modelBaseUrl,
reasoning: capabilities.reasoning,
...(capabilities.thinkingLevelMap
? { thinkingLevelMap: capabilities.thinkingLevelMap }
: {}),
params: mergeFoundryCanonicalModelParams(undefined, capabilities.modelName),
input: capabilities.input,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: capabilities.contextWindow,
maxTokens: capabilities.maxTokens,
},
capabilities.compat ? { compat: capabilities.compat } : {},
);
}),
};
}
function resolveSelectedDeploymentModelName(params: {
modelId: string;
modelNameHint?: string | null;
deployments?: FoundryDeploymentConfigInput[];
}): string | undefined {
const selectedDeployment = params.deployments?.find(
(deployment) => deployment.name === params.modelId,
);
return resolveConfiguredModelNameHint(
params.modelId,
selectedDeployment?.modelName ?? params.modelNameHint,
);
}
function isSelectedMaiImageDeployment(params: {
modelId: string;
modelNameHint?: string | null;
deployments?: FoundryDeploymentConfigInput[];
}): boolean {
return isFoundryMaiImageModel(resolveSelectedDeploymentModelName(params));
}
function buildFoundryImageDefaultPatch(params: {
modelId: string;
modelNameHint?: string | null;
deployments?: FoundryDeploymentConfigInput[];
}): FoundryImageDefaultPatch {
if (!isSelectedMaiImageDeployment(params)) {
return {};
}
return {
agents: {
defaults: {
imageGenerationModel: {
primary: `${PROVIDER_ID}/${params.modelId}`,
},
},
},
};
}
function buildFoundryCredentialMetadata(params: {
authMethod: "api-key" | "entra-id";
endpoint: string;
modelId: string;
modelNameHint?: string | null;
api?: FoundryProviderApi;
subscriptionId?: string;
subscriptionName?: string;
tenantId?: string;
}): Record<string, string> {
const resolvedApi = resolveFoundryApi(params.modelId, params.modelNameHint, params.api);
const metadata: Record<string, string> = {
authMethod: params.authMethod,
endpoint: params.endpoint,
modelId: params.modelId,
api: resolvedApi,
};
const modelName = resolveConfiguredModelNameHint(params.modelId, params.modelNameHint);
if (modelName) {
metadata.modelName = modelName;
}
if (params.subscriptionId) {
metadata.subscriptionId = params.subscriptionId;
}
if (params.subscriptionName) {
metadata.subscriptionName = params.subscriptionName;
}
if (params.tenantId) {
metadata.tenantId = params.tenantId;
}
return metadata;
}
/**
* Build the plugins.allow patch so the provider is allowlisted when the
* config already gates plugins via a non-empty allow array. Returns an
* empty object when no patch is needed (allowlist absent / already listed).
*/
function buildPluginsAllowPatch(
currentAllow: string[] | undefined,
): { plugins: { allow: string[] } } | Record<string, never> {
if (!Array.isArray(currentAllow) || currentAllow.length === 0) {
return {};
}
if (currentAllow.includes(PROVIDER_ID)) {
return {};
}
return { plugins: { allow: [...currentAllow, PROVIDER_ID] } };
}
function buildFoundryAuthOrderPatch(params: {
profileId: string;
currentProviderProfileIds?: string[];
}): { auth: { order: Record<string, string[]> } } {
const nextOrder = [
params.profileId,
...(params.currentProviderProfileIds ?? []).filter(
(profileId) => profileId !== params.profileId,
),
];
return {
auth: {
order: {
[PROVIDER_ID]: nextOrder,
},
},
};
}
export function listConfiguredFoundryProfileIds(config: FoundryConfigShape): string[] {
return Object.entries(config.auth?.profiles ?? {})
.filter(([, profile]) => profile.provider === PROVIDER_ID)
.map(([profileId]) => profileId);
}
export function buildFoundryAuthResult(params: {
profileId: string;
apiKey: SecretInput;
secretInputMode?: "plaintext" | "ref";
endpoint: string;
modelId: string;
modelNameHint?: string | null;
api: FoundryProviderApi;
authMethod: "api-key" | "entra-id";
subscriptionId?: string;
subscriptionName?: string;
tenantId?: string;
notes?: string[];
/** Current plugins.allow so the provider can self-allowlist during onboard. */
currentPluginsAllow?: string[];
currentProviderProfileIds?: string[];
deployments?: FoundryDeploymentConfigInput[];
}): ProviderAuthResult {
const imageDefaultPatch = buildFoundryImageDefaultPatch(params);
const defaultModel = isSelectedMaiImageDeployment(params)
? undefined
: `${PROVIDER_ID}/${params.modelId}`;
return {
profiles: [
{
profileId: params.profileId,
credential: buildApiKeyCredential(
PROVIDER_ID,
params.apiKey,
buildFoundryCredentialMetadata({
authMethod: params.authMethod,
endpoint: params.endpoint,
modelId: params.modelId,
modelNameHint: params.modelNameHint,
api: params.api,
subscriptionId: params.subscriptionId,
subscriptionName: params.subscriptionName,
tenantId: params.tenantId,
}),
params.secretInputMode ? { secretInputMode: params.secretInputMode } : undefined,
),
},
],
configPatch: {
...buildFoundryAuthOrderPatch({
profileId: params.profileId,
currentProviderProfileIds: params.currentProviderProfileIds,
}),
...imageDefaultPatch,
models: {
providers: {
[PROVIDER_ID]: buildFoundryProviderConfig(
params.endpoint,
params.modelId,
params.modelNameHint,
{
api: params.api,
deployments: params.deployments,
},
) as unknown as ModelProviderConfig,
},
},
...buildPluginsAllowPatch(params.currentPluginsAllow),
},
...(defaultModel ? { defaultModel } : {}),
notes: params.notes,
};
}
export function applyFoundryProfileBinding(config: FoundryConfigShape, profileId: string): void {
const next = applyAuthProfileConfig(config, {
profileId,
provider: PROVIDER_ID,
mode: "api_key",
});
config.auth = next.auth;
}
export function applyFoundryProviderConfig(
config: FoundryConfigShape,
providerConfig: ModelProviderConfig,
): void {
config.models ??= {};
config.models.providers ??= {};
config.models.providers[PROVIDER_ID] = providerConfig;
}
export function resolveFoundryTargetProfileId(config: FoundryConfigShape): string | undefined {
const configuredProfiles = config.auth?.profiles ?? {};
const configuredProfileEntries = Object.entries(configuredProfiles).filter(([, profile]) => {
return profile.provider === PROVIDER_ID;
});
if (configuredProfileEntries.length === 0) {
return undefined;
}
// Prefer the explicitly ordered profile; fall back to the sole entry when there is exactly one.
return (
config.auth?.order?.[PROVIDER_ID]?.find((profileId) => normalizeOptionalString(profileId)) ??
(configuredProfileEntries.length === 1 ? configuredProfileEntries[0]?.[0] : undefined)
);
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}