UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

414 lines (413 loc) 15.5 kB
import { c as normalizeOptionalString, f as normalizeStringifiedOptionalString } from "./string-coerce-mnp54Vah.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js"; import "./error-runtime-C8vbtAJt.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./ssrf-runtime-BOGN5pUi.js"; import { C as requiresFoundryEntraIdClaudeAuth, N as usesFoundryResponsesByDefault, T as requiresFoundryMaxCompletionTokens, a as FOUNDRY_ANTHROPIC_SCOPE, i as DEFAULT_GPT5_API, m as extractFoundryEndpoint, p as buildFoundryProviderBaseUrl, r as DEFAULT_API, t as ANTHROPIC_MESSAGES_API, w as requiresFoundryMandatoryAdaptiveClaudeThinking } from "./shared-DwLUr9iI.js"; import { i as getAccessTokenResult, n as azLoginDeviceCodeWithOptions, o as getLoggedInAccount, r as execAz, t as azLoginDeviceCode } from "./cli-a9ke0pC8.js"; //#region extensions/microsoft-foundry/onboard.ts function listFoundryResources(subscriptionId) { 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" ])); const resources = []; 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` : void 0; 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) => typeof project === "string") : [] }); } return resources; } catch { return []; } } function listResourceDeployments(resource, subscriptionId) { try { return 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" ])).filter((deployment) => deployment.state === "Succeeded"); } catch { return []; } } function buildCreateFoundryHint(selectedSub) { 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"); } async function selectFoundryResource(ctx, selectedSub) { 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)` : void 0].filter(Boolean).join(" | ") })) }); return resources.find((resource) => resource.id === selectedResourceId) ?? resources[0]; } async function selectFoundryDeployment(ctx, resource, deployments) { 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(" | ") })) }); return { selected: supported.find((deployment) => deployment.name === selectedDeploymentName) ?? supported[0], supported }; } async function promptFoundryApi(ctx, initialApi) { 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 }); } async function promptFoundryModelFamily(ctx, initialValue) { 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) { 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, options) { 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."; } })).trim(); } async function promptEndpointAndModelBase(ctx, options) { 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) ? void 0 : "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) => { if (!(normalizeStringifiedOptionalString(v) ?? "")) return "Model ID is required"; } })).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" : void 0; const api = await promptFoundryApi(ctx, familyChoice === "reasoning-family" ? DEFAULT_GPT5_API : DEFAULT_API); return { endpoint, modelId, ...resolvedModelName ? { modelNameHint: resolvedModelName } : {}, api }; } async function promptEndpointAndModelManually(ctx) { return promptEndpointAndModelBase(ctx); } async function promptApiKeyEndpointAndModel(ctx) { return promptEndpointAndModelBase(ctx, { endpointInitialValue: process.env.AZURE_OPENAI_ENDPOINT, modelInitialValue: "gpt-4o", modelFamilyInitialValue: "other-chat", allowEntraOnlyClaudeModels: false }); } function buildFoundryConnectionTest(params) { const baseUrl = buildFoundryProviderBaseUrl(params.endpoint, params.modelId, params.modelNameHint, params.api); if (params.api === "openai-responses") return { url: `${baseUrl}/responses`, body: { model: params.modelId, input: "hi", max_output_tokens: 16 } }; if (params.api === "anthropic-messages") 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) { const suggestions = []; const seen = /* @__PURE__ */ new Set(); for (const match of rawMessage.matchAll(/([0-9a-fA-F-]{36})(?:\s+'([^'\r\n]+)')?/g)) { 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; } function isValidTenantIdentifier(value) { 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; } async function promptTenantId(ctx, params) { 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:" : void 0, ...suggestionLines ].filter(Boolean).join("\n"), "Azure Tenant"); return (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" : void 0; return isValidTenantIdentifier(trimmed) ? void 0 : "Enter a valid tenant ID or tenant domain"; } })).trim() || void 0; } async function loginWithTenantFallback(ctx) { try { await azLoginDeviceCode(); return { account: getLoggedInAccount() }; } catch (error) { const message = formatErrorMessage(error); if (!(/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))) 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 }; } } async function testFoundryConnection(params) { try { const { accessToken } = getAccessTokenResult({ scope: params.api === "anthropic-messages" ? FOUNDRY_ANTHROPIC_SCOPE : void 0, 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" ? { "anthropic-version": "2023-06-01" } : {} }, body: JSON.stringify(testRequest.body) }, timeoutMs: 15e3 }); try { if (res.status === 400) { const body = await res.text().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 res.text().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"); } } //#endregion export { promptApiKeyEndpointAndModel as a, selectFoundryDeployment as c, loginWithTenantFallback as i, selectFoundryResource as l, isValidTenantIdentifier as n, promptEndpointAndModelManually as o, listResourceDeployments as r, promptTenantId as s, buildFoundryConnectionTest as t, testFoundryConnection as u };