UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

580 lines (579 loc) 22.5 kB
import { w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js"; import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { m as resolveAgentWorkspaceDir, u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import { n as resolveDefaultAgentWorkspaceDir } from "./workspace-default-DPT1Dhad.js"; import { c as normalizeProviderId } from "./model-ref-shared-Dz7QU0Lx.js"; import { d as modelCatalogLogicalKey, i as buildModelAliasIndex, m as resolveBareModelDefaultProvider, o as dedupeModelCatalogEntries, y as resolveModelRefFromString } from "./model-selection-shared-BlLyx1r2.js"; import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js"; import { t as resolveDefaultModelForAgent } from "./model-selection-config-BrdmmqKD.js"; import { a as listOpenAIAuthProfileProvidersForAgentRuntime } from "./openai-routing-Ex_3E9TF.js"; import { n as openAIModelCatalogRoutePolicy } from "./openai-model-routes-Bte2KVl5.js"; import { t as resolveAgentHarnessPolicy } from "./policy-D9i1QMuw.js"; import { i as getPreparedModelRuntimeAuthStore } from "./prepared-model-runtime-auth-CnrySjUa.js"; import { t as PreparedModelCatalogConfigReplacedError } from "./prepared-model-catalog.errors-DftAtHN9.js"; import { t as getChannelPlugin } from "./registry-Cz6cv4VC.js"; import "./plugins-Cozj1Enf.js"; import { n as listCliRuntimeModelBackendBindings } from "./cli-backends-Bs_a4MW0.js"; import { o as isRetiredModelPickerProvider } from "./model-runtime-aliases-BJ2qM8I_.js"; import "./workspace-ConDEamr.js"; import { n as PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors-DeG6Ut3_.js"; import { f as withPreparedModelCatalogOwner, u as loadPublishedPreparedModelCatalogOwnerSnapshot } from "./prepared-model-catalog-Bt4dYhnY.js"; import { n as createProviderAuthChecker } from "./model-provider-auth-jVhbzrw7.js"; import "./model-selection-di2kjKCB.js"; import { n as createModelVisibilityPolicy } from "./model-visibility-policy-mYoB7nkk.js"; import { s as rejectUnauthorizedCommand } from "./command-gates-B4tlStPI.js"; import { t as resolveAgentRuntimeLabel } from "./agent-runtime-label-BA4eUY6k.js"; import { o as resolveRuntimeNormalization } from "./model-runtime-normalization-CVjCcJkR.js"; import { n as awaitWithinDeadline, t as ABSOLUTE_DEADLINE_EXPIRED } from "./absolute-deadline-D0jNXqHr.js"; import { t as resolveModelAuthLabel } from "./model-auth-label-BJ8ne6bw.js"; import { r as modelCatalogBrowseRequiresFullDiscovery } from "./model-catalog-browse-DzKrbDYm.js"; import { n as resolveLogicalModelCatalogEntryState, r as resolveLogicalVisibleModelCatalog } from "./model-catalog-visibility-D9ic3uaR.js"; //#region src/auto-reply/reply/commands-models.ts const PAGE_SIZE_DEFAULT = 20; const PAGE_SIZE_MAX = 100; const MODELS_ADD_DEPRECATED_TEXT = "⚠️ /models add is deprecated. Use /models to browse providers and /model to switch models."; function isModelsBrowseVisibleProvider(provider) { return !isRetiredModelPickerProvider(provider); } function usesUnfilteredCatalogModels(provider, cliRuntimeProviders) { return cliRuntimeProviders.has(normalizeProviderId(provider)); } function normalizeRuntimeChoiceId(runtime) { const normalized = normalizeLowercaseStringOrEmpty(runtime); if (!normalized || normalized === "auto" || normalized === "default") return "openclaw"; return normalized; } function buildRuntimeChoice(params) { const id = normalizeRuntimeChoiceId(params.runtime); const label = resolveAgentRuntimeLabel({ config: params.cfg, resolvedHarness: id }); return { id, label, description: id === "openclaw" ? "Use the built-in OpenClaw runtime." : params.cli ? `Run ${params.provider} models through ${label}.` : `Use the ${label} runtime selected by the effective harness policy.` }; } function buildDefaultRuntimeChoice(params) { const harnessPolicy = resolveAgentHarnessPolicy({ config: params.cfg, provider: params.provider, modelId: params.modelId, agentId: params.agentId }); return buildRuntimeChoice({ cfg: params.cfg, provider: params.provider, runtime: harnessPolicy.runtime }); } function addRuntimeChoice(choices, choice) { if (!choices.some((existing) => existing.id === choice.id)) choices.push(choice); return choices; } function buildPreparedModelsProviderData(cfg, agentId, options = {}) { return buildPreparedModelsProviderDataWithContext(cfg, agentId, options); } async function buildPreparedModelsProviderDataWithContext(cfg, agentId, options, agentDir) { const deadlineMs = options.view === "all" ? void 0 : Date.now() + 750; let currentAgentDir = agentDir; let currentConfig = cfg; const buildCurrentData = (control) => buildPreparedDataForConfig(currentConfig, agentId, { ...options, agentDir: currentAgentDir }, control); for (;;) { if (deadlineMs !== void 0 && Date.now() >= deadlineMs) return buildCurrentData({ catalogFallback: true, deadlineMs }); try { return await buildCurrentData({ deadlineMs }); } catch (error) { if (!isPreparedModelCatalogOwnerReplacement(error)) throw error; } const owner = await loadPublishedModelsOwner({ agentId, deadlineMs, workspaceDir: options.workspaceDir }); if (!owner) return buildCurrentData({ catalogFallback: true, deadlineMs }); currentConfig = owner.config; currentAgentDir = owner.agentDir; } } function isPreparedModelCatalogOwnerReplacement(error) { return error instanceof PreparedModelCatalogConfigReplacedError || error instanceof PreparedModelRuntimePublicationSupersededError; } async function loadPublishedModelsOwner(params) { for (;;) try { const owner = await awaitWithinDeadline(() => loadPublishedPreparedModelCatalogOwnerSnapshot({ readOnly: true, agentId: params.agentId, workspaceDir: params.workspaceDir }), params.deadlineMs); if (owner === ABSOLUTE_DEADLINE_EXPIRED) return; return owner; } catch (error) { if (!isPreparedModelCatalogOwnerReplacement(error)) throw error; } } async function buildPreparedDataForConfig(cfg, agentId, options, control) { const project = (owner) => projectPreparedModelsProviderData(cfg, agentId, options, owner); if (control.catalogFallback) return project(); const agentDir = options.agentDir ?? (agentId ? resolveAgentDir(cfg, agentId) : void 0); const result = await awaitWithinDeadline(() => withPreparedModelCatalogOwner({ config: cfg, readOnly: !modelCatalogBrowseRequiresFullDiscovery({ cfg, agentId, view: options.view }), refreshFullCatalog: "stale", ...agentId ? { agentId } : {}, ...agentDir ? { agentDir } : {}, ...options.workspaceDir ? { workspaceDir: options.workspaceDir } : {} }, project), control.deadlineMs); return result === ABSOLUTE_DEADLINE_EXPIRED ? project() : result; } async function projectPreparedModelsProviderData(cfg, agentId, options, owner) { const runtimeNormalization = resolveRuntimeNormalization(cfg); const resolvedDefault = resolveDefaultModelForAgent({ cfg, agentId, ...runtimeNormalization }); const workspaceDir = options.workspaceDir ?? (agentId ? resolveAgentWorkspaceDir(cfg, agentId) : void 0) ?? resolveDefaultAgentWorkspaceDir(); const cliRuntimeProviders = new Set(listCliRuntimeModelBackendBindings().map((binding) => normalizeProviderId(binding.runtime))); const snapshot = owner?.modelCatalog ?? { entries: [], routeVariants: [] }; const authStore = owner && getPreparedModelRuntimeAuthStore(owner); const catalog = snapshot.entries; const visibilityPolicy = createModelVisibilityPolicy({ cfg, catalog, defaultProvider: resolvedDefault.provider, defaultModel: resolvedDefault.model, agentId, ...runtimeNormalization }); const authChecker = createProviderAuthChecker({ cfg, workspaceDir, agentId, allowPluginSyntheticAuth: false, discoverExternalCliAuth: false, allowPreparedRuntimeAuth: true, ...authStore && owner ? { preparedAuth: { authStore, authModes: owner.authModes }, metadataSnapshot: owner.metadataSnapshot } : {} }); const logicalModelKey = (entry) => openAIModelCatalogRoutePolicy.resolveIdentity(entry)?.key ?? modelCatalogLogicalKey(entry); const incompatibleModelKeys = /* @__PURE__ */ new Set(); const hasAuth = options.view === "all" ? async () => true : authChecker; const visibleCatalog = await resolveLogicalVisibleModelCatalog({ cfg, catalog, defaultProvider: resolvedDefault.provider, defaultModel: resolvedDefault.model, agentId, workspaceDir, view: options.view, policy: visibilityPolicy, routePolicy: openAIModelCatalogRoutePolicy, routeVariants: snapshot.routeVariants, evaluateEntry: async (entry, routeVariants) => { const identity = openAIModelCatalogRoutePolicy.resolveIdentity(entry); const evaluation = await authChecker.evaluateModelAuth(entry.provider, { modelId: identity?.id ?? entry.id, observedRoutes: routeVariants.map((variant) => ({ api: variant.api, baseUrl: variant.baseUrl })) }); if (evaluation.routeResolution?.kind === "incompatible") incompatibleModelKeys.add(logicalModelKey(entry)); return resolveLogicalModelCatalogEntryState({ evaluation, authBacked: options.view === "all" || evaluation.availability === true, routePolicy: openAIModelCatalogRoutePolicy }); } }); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: resolvedDefault.provider, agentId, ...runtimeNormalization }); const restrictToProviderWildcards = options.view !== "all" && visibilityPolicy.hasProviderWildcards; const byProvider = /* @__PURE__ */ new Map(); const add = (p, m) => { const key = normalizeProviderId(p); if (!isModelsBrowseVisibleProvider(key)) return; if (restrictToProviderWildcards && !usesUnfilteredCatalogModels(key, cliRuntimeProviders) && !visibilityPolicy.allows({ provider: key, model: m })) return; const set = byProvider.get(key) ?? /* @__PURE__ */ new Set(); set.add(m); byProvider.set(key, set); }; const addRawModelRef = (raw) => { const trimmed = normalizeOptionalString(raw); if (!trimmed) return; const defaultProvider = !trimmed.includes("/") ? resolveBareModelDefaultProvider({ cfg, catalog, model: trimmed, defaultProvider: resolvedDefault.provider, agentId, manifestPlugins: runtimeNormalization.manifestPlugins }) : resolvedDefault.provider; const resolved = resolveModelRefFromString({ cfg, agentId, raw: trimmed, defaultProvider, aliasIndex, ...runtimeNormalization }); if (!resolved) return; if (incompatibleModelKeys.has(logicalModelKey({ provider: resolved.ref.provider, id: resolved.ref.model }))) return; add(resolved.ref.provider, resolved.ref.model); }; const addModelConfigEntries = () => { const modelConfig = cfg.agents?.defaults?.model; if (typeof modelConfig === "string") addRawModelRef(modelConfig); else if (modelConfig && typeof modelConfig === "object") { addRawModelRef(modelConfig.primary); for (const fallback of modelConfig.fallbacks ?? []) addRawModelRef(fallback); } const imageConfig = cfg.agents?.defaults?.imageModel; if (typeof imageConfig === "string") addRawModelRef(imageConfig); else if (imageConfig && typeof imageConfig === "object") { addRawModelRef(imageConfig.primary); for (const fallback of imageConfig.fallbacks ?? []) addRawModelRef(fallback); } }; for (const entry of visibleCatalog) { if (incompatibleModelKeys.has(logicalModelKey(entry))) continue; add(entry.provider, entry.id); } for (const entry of catalog) if (usesUnfilteredCatalogModels(entry.provider, cliRuntimeProviders) && await hasAuth(entry.provider, { modelId: entry.id, api: entry.api, baseUrl: entry.baseUrl })) add(entry.provider, entry.id); for (const raw of visibilityPolicy.exactModelRefs) addRawModelRef(raw); if (!incompatibleModelKeys.has(logicalModelKey({ provider: resolvedDefault.provider, id: resolvedDefault.model }))) add(resolvedDefault.provider, resolvedDefault.model); addModelConfigEntries(); const providers = [...byProvider.keys()].toSorted(); const modelNames = /* @__PURE__ */ new Map(); for (const entry of [...catalog, ...visibleCatalog]) if (entry.name && entry.name !== entry.id) modelNames.set(`${normalizeProviderId(entry.provider)}/${entry.id}`, entry.name); const runtimeChoicesByProvider = /* @__PURE__ */ new Map(); const runtimeBindings = [{ provider: "openai", runtime: "codex", cli: false }, ...listCliRuntimeModelBackendBindings().map((binding) => ({ provider: binding.provider, runtime: binding.runtime, cli: true }))]; for (const binding of runtimeBindings) { const provider = normalizeProviderId(binding.provider); const defaultModelId = provider === normalizeProviderId(resolvedDefault.provider) ? resolvedDefault.model : void 0; const choices = runtimeChoicesByProvider.get(provider) ?? [buildDefaultRuntimeChoice({ cfg, agentId, provider, modelId: defaultModelId })]; addRuntimeChoice(choices, buildRuntimeChoice({ cfg, provider, runtime: "openclaw" })); addRuntimeChoice(choices, buildRuntimeChoice({ cfg, provider, runtime: binding.runtime, cli: binding.cli })); runtimeChoicesByProvider.set(provider, choices); } if (owner && !owner.isCurrent()) throw new PreparedModelRuntimePublicationSupersededError("model browse owner was superseded"); return { byProvider, providers, resolvedDefault, modelNames, modelCatalog: dedupeModelCatalogEntries([...visibleCatalog, ...catalog]), runtimeChoicesByProvider }; } function formatProviderLine(params) { return `- ${params.provider} (${params.count})`; } function parseListArgs(tokens) { const provider = normalizeOptionalString(tokens[0]); let page = 1; let all = false; for (const token of tokens.slice(1)) { const lower = normalizeLowercaseStringOrEmpty(token); if (lower === "all" || lower === "--all") { all = true; continue; } if (lower.startsWith("page=")) { const value = parseStrictPositiveInteger(lower.slice(5)); if (value !== void 0) page = value; continue; } const pageToken = parseStrictPositiveInteger(lower); if (pageToken !== void 0) page = pageToken; } let pageSize = PAGE_SIZE_DEFAULT; for (const token of tokens) { const lower = normalizeLowercaseStringOrEmpty(token); if (lower.startsWith("limit=") || lower.startsWith("size=")) { const rawValue = lower.slice(lower.indexOf("=") + 1); const value = parseStrictPositiveInteger(rawValue); if (value !== void 0) pageSize = Math.min(PAGE_SIZE_MAX, value); } } return { action: "list", provider: provider ? normalizeProviderId(provider) : void 0, page, pageSize, all }; } function parseModelsArgs(raw) { const trimmed = raw.trim(); if (!trimmed) return { action: "providers" }; const tokens = trimmed.split(/\s+/g).filter(Boolean); switch (normalizeLowercaseStringOrEmpty(tokens[0])) { case "providers": return { action: "providers" }; case "list": return parseListArgs(tokens.slice(1)); case "add": return { action: "add", provider: normalizeOptionalString(tokens[1]), modelId: normalizeOptionalString(tokens.slice(2).join(" ")) }; default: return parseListArgs(tokens); } } function resolveProviderLabel(params) { const harnessPolicy = resolveAgentHarnessPolicy({ config: params.cfg, provider: params.provider, agentId: params.agentId }); const acceptedProviderIds = listOpenAIAuthProfileProvidersForAgentRuntime({ provider: params.provider, harnessRuntime: harnessPolicy.runtime, config: params.cfg }); const authLabel = resolveModelAuthLabel({ provider: params.provider, acceptedProviderIds, cfg: params.cfg, sessionEntry: params.sessionEntry, agentDir: params.agentDir, workspaceDir: params.workspaceDir }); if (!authLabel || authLabel === "unknown") return params.provider; return `${params.provider} · 🔑 ${authLabel}`; } function formatModelsAvailableHeader(params) { return `Models (${resolveProviderLabel({ provider: params.provider, cfg: params.cfg, agentId: params.agentId, agentDir: params.agentDir, workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry })}) — ${params.total} available`; } function buildModelsMenuText(params) { return [ "Providers:", ...params.providers.map((provider) => formatProviderLine({ provider, count: params.byProvider.get(provider)?.size ?? 0 })), "", "Use: /models <provider>", "Switch: /model <provider/model>" ].join("\n"); } function buildProviderInfos(params) { return params.providers.map((provider) => ({ id: provider, count: params.byProvider.get(provider)?.size ?? 0 })); } async function resolveModelsCommandReply(params) { const body = params.commandBodyNormalized.trim(); if (!body.startsWith("/models")) return null; const parsed = parseModelsArgs(body.replace(/^\/models\b/i, "").trim()); const { byProvider, providers, modelNames } = await buildPreparedModelsProviderDataWithContext(params.cfg, params.agentId, { ...parsed.action === "list" && parsed.all ? { view: "all" } : {}, workspaceDir: params.workspaceDir }, params.agentDir); const commandPlugin = params.surface ? getChannelPlugin(params.surface) : null; const providerInfos = buildProviderInfos({ providers, byProvider }); if (parsed.action === "providers") { const channelData = commandPlugin?.commands?.buildModelsMenuChannelData?.({ providers: providerInfos }) ?? commandPlugin?.commands?.buildModelsProviderChannelData?.({ providers: providerInfos }); if (channelData) return { text: "Select a provider:", channelData }; return { text: buildModelsMenuText({ providers, byProvider }) }; } if (parsed.action === "add") return { text: MODELS_ADD_DEPRECATED_TEXT }; const { provider, page, pageSize, all } = parsed; if (!provider) { const channelData = commandPlugin?.commands?.buildModelsProviderChannelData?.({ providers: providerInfos }); if (channelData) return { text: "Select a provider:", channelData }; return { text: buildModelsMenuText({ providers, byProvider }) }; } if (!byProvider.has(provider)) return { text: [ `Unknown provider: ${provider}`, "", "Available providers:", ...providers.map((entry) => `- ${entry}`), "", "Use: /models <provider>" ].join("\n") }; const models = [...byProvider.get(provider) ?? /* @__PURE__ */ new Set()].toSorted(); const total = models.length; if (total === 0) return { text: [ `Models (${resolveProviderLabel({ provider, cfg: params.cfg, agentId: params.agentId, agentDir: params.agentDir, workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry })}) — none`, "", "Browse: /models", "Switch: /model <provider/model>" ].join("\n") }; const interactivePageSize = 8; const interactiveTotalPages = Math.max(1, Math.ceil(total / interactivePageSize)); const interactivePage = Math.max(1, Math.min(page, interactiveTotalPages)); const interactiveChannelData = commandPlugin?.commands?.buildModelsListChannelData?.({ provider, models, currentModel: params.currentModel, currentPage: interactivePage, totalPages: interactiveTotalPages, pageSize: interactivePageSize, modelNames }); if (interactiveChannelData) return { text: formatModelsAvailableHeader({ provider, total, cfg: params.cfg, agentId: params.agentId, agentDir: params.agentDir, workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry }), channelData: interactiveChannelData }; const effectivePageSize = all ? total : pageSize; const pageCount = effectivePageSize > 0 ? Math.ceil(total / effectivePageSize) : 1; const safePage = all ? 1 : Math.max(1, Math.min(page, pageCount)); if (!all && page !== safePage) return { text: [ `Page out of range: ${page} (valid: 1-${pageCount})`, "", `Try: /models list ${provider} ${safePage}`, `All: /models list ${provider} all` ].join("\n") }; const startIndex = (safePage - 1) * effectivePageSize; const endIndexExclusive = Math.min(total, startIndex + effectivePageSize); const pageModels = models.slice(startIndex, endIndexExclusive); const lines = [`Models (${resolveProviderLabel({ provider, cfg: params.cfg, agentId: params.agentId, agentDir: params.agentDir, workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry })}) — showing ${startIndex + 1}-${endIndexExclusive} of ${total} (page ${safePage}/${pageCount})`]; for (const id of pageModels) lines.push(`- ${provider}/${id}`); lines.push("", "Switch: /model <provider/model>"); if (!all && safePage < pageCount) lines.push(`More: /models list ${provider} ${safePage + 1}`); if (!all) lines.push(`All: /models list ${provider} all`); return { text: lines.join("\n") }; } const handleModelsCommand = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const commandBodyNormalized = params.command.commandBodyNormalized.trim(); if (!commandBodyNormalized.startsWith("/models")) return null; const parsed = parseModelsArgs(commandBodyNormalized.replace(/^\/models\b/i, "").trim()); const unauthorized = rejectUnauthorizedCommand(params, "/models"); if (unauthorized) return unauthorized; if (parsed.action === "add") return { shouldContinue: false, reply: { text: MODELS_ADD_DEPRECATED_TEXT } }; const modelsAgentId = params.sessionKey ? resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg }) : params.agentId ?? "main"; const currentAgentId = params.agentId ?? "main"; const modelsAgentDir = modelsAgentId === currentAgentId && params.agentDir ? params.agentDir : resolveAgentDir(params.cfg, modelsAgentId); const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry; const reply = await resolveModelsCommandReply({ cfg: params.cfg, commandBodyNormalized, surface: params.ctx.Surface, currentModel: params.model ? `${params.provider}/${params.model}` : void 0, agentId: modelsAgentId, agentDir: modelsAgentDir, workspaceDir: targetSessionEntry?.spawnedWorkspaceDir ?? (modelsAgentId === currentAgentId ? params.workspaceDir : void 0), sessionEntry: targetSessionEntry }); if (!reply) return null; return { reply, shouldContinue: false }; }; //#endregion export { resolveModelsCommandReply as i, formatModelsAvailableHeader as n, handleModelsCommand as r, buildPreparedModelsProviderData as t };