UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

538 lines (537 loc) 22.3 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { t as createLazyImportLoader } from "./lazy-promise-BONnzNfb.js"; import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js"; import { i as normalizeProviderId } from "./provider-id-Dq06Bcx6.js"; import "./agent-scope-MrLta7Pq.js"; import { s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js"; import { r as getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot-CfA_n2Nc.js"; import { a as resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot-Ctk9Oarq.js"; import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js"; import { t as planManifestModelCatalogRows } from "./manifest-planner-BRQyTcMi.js"; import { s as resolveClaudeFable5ModelIdentity } from "./src-M7TBQdDX.js"; import "./config-C9RxTsn1.js"; import { i as ensureAuthProfileStoreWithoutExternalProfiles } from "./store-C8spD0DG.js"; import { n as isManifestPluginAvailableForControlPlane, s as loadManifestMetadataSnapshot } from "./manifest-contract-eligibility-DEeoLRlM.js"; import { t as augmentModelCatalogWithProviderPlugins } from "./provider-runtime.runtime.js"; import "./auth-profiles-84rzaGag.js"; import { S as modelSupportsInput, r as buildConfiguredModelCatalog, s as hasConfiguredProviderModelRows } from "./model-selection-shared-b32cUYts.js"; import { n as readCachedAgentModelCatalog, r as writeCachedAgentModelCatalog, t as buildAgentModelCatalogCacheKey } from "./model-catalog-state-cache-BMVIG9NR.js"; import { n as resolveModelWorkspaceDir } from "./model-discovery-context-DmaLjfYx.js"; import { n as modelKey, r as normalizeConfiguredProviderCatalogModelId } from "./model-ref-shared-CqcindZs.js"; import { i as prepareOpenClawModelsJsonSource, t as buildModelsJsonSourceFingerprint } from "./models-config-SUxEzcTF.js"; import { i as filterGeneratedPluginModelCatalogProviders, s as listPluginModelCatalogFiles } from "./plugin-model-catalog-C26wDCJp.js"; import { join } from "node:path"; import { readFile } from "node:fs/promises"; //#region src/agents/model-catalog.ts /** * Loads bundled, manifest, and discovered model catalog entries. */ const log = createSubsystemLogger("model-catalog"); const AGENT_CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW = 128e3; let modelCatalogPromise = null; let hasLoggedModelCatalogError = false; let hasLoggedReadOnlyStaticCatalogError = false; let manifestModelCatalogCache = /* @__PURE__ */ new WeakMap(); function buildLoadModelCatalogStateCacheKey(params) { return buildAgentModelCatalogCacheKey({ agentDir: params.agentDir, cacheScope: { source: "load-model-catalog", sourceFingerprint: params.sourceFingerprint }, config: params.config, metadataSnapshot: params.metadataSnapshot, workspaceDir: params.workspaceDir }); } const defaultImportAgentDiscovery = () => import("./agent-model-discovery-C50rqKxs.js"); let importAgentDiscovery = defaultImportAgentDiscovery; const modelSuppressionLoader = createLazyImportLoader(() => import("./model-suppression.runtime.js")); const providerApiKeyResolverLoader = createLazyImportLoader(() => import("./models-config.providers.secrets-rK53Ec7x.js")); function shouldLogModelCatalogTiming() { return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1"; } function loadModelSuppression() { return modelSuppressionLoader.load(); } function loadProviderApiKeyResolver() { return providerApiKeyResolverLoader.load(); } function resetModelCatalogCache() { modelCatalogPromise = null; manifestModelCatalogCache = /* @__PURE__ */ new WeakMap(); hasLoggedModelCatalogError = false; hasLoggedReadOnlyStaticCatalogError = false; } function resetModelCatalogCacheForTest() { resetModelCatalogCache(); importAgentDiscovery = defaultImportAgentDiscovery; } function setModelCatalogImportForTest(loader) { importAgentDiscovery = loader ?? defaultImportAgentDiscovery; } function catalogEntryDedupeKey(provider, id) { return normalizeLowercaseStringOrEmpty(modelKey(normalizeProviderId(provider), id)); } function mergeCatalogCompat(base, override) { if (!base) return override; if (!override) return base; return { ...base, ...override }; } function mergeCatalogParams(base, override) { if (!base) return override; if (!override) return base; return { ...base, ...override }; } function overlayCatalogMetadata(base, overlay) { const params = mergeCatalogParams(base.params, overlay.params); return { ...base, ...overlay.api !== void 0 ? { api: overlay.api } : {}, ...overlay.contextWindow !== void 0 ? { contextWindow: overlay.contextWindow } : {}, ...overlay.contextTokens !== void 0 ? { contextTokens: overlay.contextTokens } : {}, ...overlay.reasoning !== void 0 ? { reasoning: overlay.reasoning } : {}, ...overlay.input !== void 0 ? { input: overlay.input } : {}, ...params ? { params } : {}, compat: mergeCatalogCompat(base.compat, overlay.compat) }; } function normalizeCatalogEntryContract(entry) { if (entry.api === "anthropic-messages" && resolveClaudeFable5ModelIdentity({ id: entry.id, params: entry.params })) return { ...entry, reasoning: true }; return entry; } function mergeCatalogEntries(models, entries) { const indexByKey = new Map(models.map((entry, index) => [catalogEntryDedupeKey(entry.provider, entry.id), index])); for (const entry of entries) { const key = catalogEntryDedupeKey(entry.provider, entry.id); const existingIndex = indexByKey.get(key); if (existingIndex === void 0) { models.push(entry); indexByKey.set(key, models.length - 1); continue; } models[existingIndex] = overlayCatalogMetadata(models[existingIndex], entry); } } function loadManifestModelCatalog(params) { const resolvedSnapshot = params.metadataSnapshot ?? (params.fallbackToMetadataScan === false ? getCurrentPluginMetadataSnapshot({ config: params.config, env: params.env, ...params.workspaceDir !== void 0 ? { workspaceDir: params.workspaceDir } : {}, ...params.workspaceDir === void 0 ? { allowWorkspaceScopedSnapshot: true } : {} }) : resolvePluginMetadataSnapshot({ config: params.config, ...params.workspaceDir !== void 0 ? { workspaceDir: params.workspaceDir } : {}, env: params.env ?? process.env, allowWorkspaceScopedCurrent: params.workspaceDir === void 0 })); if (!resolvedSnapshot) return []; const cached = manifestModelCatalogCache.get(params.config); if (cached?.snapshot === resolvedSnapshot) return cached.rows; const rows = planManifestModelCatalogRows({ registry: { plugins: resolvedSnapshot.plugins.filter((plugin) => plugin.modelCatalog && isManifestPluginAvailableForControlPlane({ snapshot: resolvedSnapshot, plugin, config: params.config })) } }).rows.map((row) => { const entry = { id: row.id, name: row.name, provider: row.provider, api: row.api }; const contextWindow = row.contextWindow ?? row.contextTokens; if (contextWindow) entry.contextWindow = contextWindow; if (row.contextTokens) entry.contextTokens = row.contextTokens; if (typeof row.reasoning === "boolean") entry.reasoning = row.reasoning; if (row.input?.length) entry.input = [...row.input]; if (row.compat) entry.compat = row.compat; return entry; }); manifestModelCatalogCache.set(params.config, { snapshot: resolvedSnapshot, rows }); return rows; } function sortModelCatalogEntries(entries) { return entries.map(normalizeCatalogEntryContract).toSorted((a, b) => { const p = a.provider.localeCompare(b.provider); if (p !== 0) return p; return a.name.localeCompare(b.name); }); } function normalizePersistedModelCatalogEntry(providerRaw, entry, defaults, options = {}) { const rawId = normalizeOptionalString(entry.id) ?? ""; if (!rawId) return; const provider = normalizeProviderId(providerRaw); if (!provider) return; const id = normalizeConfiguredProviderCatalogModelId(provider, rawId, options); const name = normalizeOptionalString(entry.name ?? id) || id; const contextWindow = typeof entry?.contextWindow === "number" && entry.contextWindow > 0 ? entry.contextWindow : defaults?.contextWindow !== void 0 ? defaults.contextWindow : AGENT_CUSTOM_MODEL_DEFAULT_CONTEXT_WINDOW; const contextTokens = typeof entry?.contextTokens === "number" && entry.contextTokens > 0 ? entry.contextTokens : defaults?.contextTokens !== void 0 ? defaults.contextTokens : void 0; const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : false; const api = typeof entry?.api === "string" ? entry.api : defaults?.api; const parsedInput = Array.isArray(entry?.input) ? entry.input.filter((value) => [ "text", "image", "audio", "video", "document" ].includes(String(value))) : void 0; const input = parsedInput?.length ? parsedInput : ["text"]; const compat = entry?.compat && typeof entry.compat === "object" ? entry.compat : void 0; const modelParams = entry?.params && typeof entry.params === "object" ? entry.params : void 0; return { id, name, provider, ...api ? { api } : {}, contextWindow, ...contextTokens !== void 0 ? { contextTokens } : {}, reasoning, input, ...modelParams ? { params: modelParams } : {}, compat }; } function readProviderCatalogRows(parsed) { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; const providers = parsed.providers; return providers && typeof providers === "object" && !Array.isArray(providers) ? providers : {}; } async function loadReadOnlyPersistedProviderRows(agentDir, getPluginMetadataSnapshot) { const raw = await readFile(join(agentDir, "models.json"), "utf8"); const providers = { ...readProviderCatalogRows(JSON.parse(raw)) }; for (const catalogFile of listPluginModelCatalogFiles(agentDir)) { const catalogRaw = await readFile(catalogFile.path, "utf8").catch(() => void 0); if (!catalogRaw) continue; let parsed; try { parsed = JSON.parse(catalogRaw); } catch { continue; } Object.assign(providers, filterGeneratedPluginModelCatalogProviders({ catalogPluginId: catalogFile.pluginId, parsedCatalog: parsed, pluginMetadataSnapshot: getPluginMetadataSnapshot(), providers: readProviderCatalogRows(parsed) })); } return providers; } async function loadReadOnlyPersistedModelCatalog(params) { const cfg = params?.config ?? getRuntimeConfig(); const agentDir = resolveDefaultAgentDir(cfg); const workspaceDir = resolveModelWorkspaceDir(cfg, void 0); const models = []; const { buildShouldSuppressBuiltInModel } = await loadModelSuppression(); const shouldSuppressBuiltInModel = buildShouldSuppressBuiltInModel({ config: cfg }); let metadataSnapshot = params?.metadataSnapshot; const getMetadataSnapshot = () => { metadataSnapshot ??= loadManifestMetadataSnapshot({ config: cfg, env: process.env, workspaceDir }); return metadataSnapshot; }; let manifestPlugins; const getManifestPlugins = () => { manifestPlugins ??= getMetadataSnapshot().plugins; return manifestPlugins; }; const providers = await loadReadOnlyPersistedProviderRows(agentDir, getMetadataSnapshot); for (const [providerRaw, providerConfig] of Object.entries(providers)) { if (!Array.isArray(providerConfig?.models)) continue; const providerContextWindow = typeof providerConfig?.contextWindow === "number" && providerConfig.contextWindow > 0 ? providerConfig.contextWindow : void 0; const providerContextTokens = typeof providerConfig?.contextTokens === "number" && providerConfig.contextTokens > 0 ? providerConfig.contextTokens : void 0; const providerApi = typeof providerConfig?.api === "string" ? providerConfig.api : void 0; for (const entry of providerConfig.models) { const normalized = normalizePersistedModelCatalogEntry(providerRaw, entry, { api: providerApi, contextWindow: providerContextWindow, contextTokens: providerContextTokens }, { manifestPlugins: getManifestPlugins() }); if (normalized && !shouldSuppressBuiltInModel(normalized)) models.push(normalized); } } if (models.length === 0) throw new Error("persisted model catalog has no usable model rows"); try { mergeCatalogEntries(models, loadManifestModelCatalog({ config: cfg, env: process.env, fallbackToMetadataScan: false, metadataSnapshot: getMetadataSnapshot() })); } catch {} const configuredModels = buildConfiguredModelCatalog({ cfg, manifestPlugins: hasConfiguredProviderModelRows(cfg) ? getManifestPlugins() : void 0 }); if (configuredModels.length > 0) mergeCatalogEntries(models, configuredModels); return sortModelCatalogEntries(models); } function hasConfiguredProviderRowsNeedingManifestLookup(cfg) { const providers = cfg.models?.providers; if (!providers || typeof providers !== "object") return false; return Object.entries(providers).some(([providerRaw, provider]) => Array.isArray(provider?.models) && normalizeProviderId(providerRaw) !== "openai"); } function loadReadOnlyStaticModelCatalog(params) { const cfg = params?.config ?? getRuntimeConfig(); const models = []; try { mergeCatalogEntries(models, loadManifestModelCatalog({ config: cfg, env: process.env, fallbackToMetadataScan: false, metadataSnapshot: params?.metadataSnapshot })); } catch (error) { if (!hasLoggedReadOnlyStaticCatalogError) { hasLoggedReadOnlyStaticCatalogError = true; log.warn(`Failed to load read-only manifest model catalog: ${String(error)}`); } } const configuredModels = buildConfiguredModelCatalog({ cfg, manifestPlugins: hasConfiguredProviderRowsNeedingManifestLookup(cfg) ? params?.metadataSnapshot?.plugins ?? resolvePluginMetadataSnapshot({ config: cfg, env: process.env, allowWorkspaceScopedCurrent: true }).plugins : [] }); if (configuredModels.length > 0) mergeCatalogEntries(models, configuredModels); return sortModelCatalogEntries(models); } async function loadModelCatalog(params) { const readOnly = params?.readOnly === true; if (readOnly) try { return await loadReadOnlyPersistedModelCatalog(params); } catch { return loadReadOnlyStaticModelCatalog(params); } if (!readOnly && params?.useCache === false) modelCatalogPromise = null; const useSharedCache = !readOnly && !params?.metadataSnapshot; if (useSharedCache && modelCatalogPromise) return modelCatalogPromise; const loadCatalog = async () => { const models = []; const timingEnabled = shouldLogModelCatalogTiming(); const startMs = timingEnabled ? Date.now() : 0; const logStage = (stage, extra) => { if (!timingEnabled) return; const suffix = extra ? ` ${extra}` : ""; log.info(`model-catalog stage=${stage} elapsedMs=${Date.now() - startMs}${suffix}`); }; const sortModels = sortModelCatalogEntries; try { const cfg = params?.config ?? getRuntimeConfig(); const workspaceDir = resolveModelWorkspaceDir(cfg, void 0); let manifestMetadataSnapshot; let manifestPlugins; const getManifestMetadataSnapshot = () => { manifestMetadataSnapshot ??= params?.metadataSnapshot ?? loadManifestMetadataSnapshot({ config: cfg, env: process.env, workspaceDir }); return manifestMetadataSnapshot; }; const getManifestPlugins = () => { manifestPlugins ??= getManifestMetadataSnapshot().plugins; return manifestPlugins; }; const agentDir = resolveDefaultAgentDir(cfg); const sourceFingerprint = await buildModelsJsonSourceFingerprint(cfg, agentDir, { pluginMetadataSnapshot: params?.metadataSnapshot, workspaceDir }); let catalogKey = buildLoadModelCatalogStateCacheKey({ agentDir, config: cfg, metadataSnapshot: params?.metadataSnapshot, sourceFingerprint: sourceFingerprint.fingerprint, workspaceDir }); if (!readOnly && params?.useCache !== false) { const cached = readCachedAgentModelCatalog({ agentDir, catalogKey }); if (cached?.length) { logStage("state-cache-hit", `entries=${cached.length}`); return cached; } } if (!readOnly) { const preparedSource = await prepareOpenClawModelsJsonSource(cfg, agentDir, { pluginMetadataSnapshot: params?.metadataSnapshot, workspaceDir }); const preparedCatalogKey = buildLoadModelCatalogStateCacheKey({ agentDir, config: cfg, metadataSnapshot: params?.metadataSnapshot, sourceFingerprint: preparedSource.fingerprint, workspaceDir: preparedSource.workspaceDir ?? workspaceDir }); logStage("models-json-ready"); if (preparedCatalogKey !== catalogKey) { catalogKey = preparedCatalogKey; if (params?.useCache !== false) { const cached = readCachedAgentModelCatalog({ agentDir, catalogKey }); if (cached?.length) { logStage("state-cache-hit", `entries=${cached.length}`); return cached; } } } } const agentDiscovery = await importAgentDiscovery(); logStage("agent-discovery-imported"); const { buildShouldSuppressBuiltInModel } = await loadModelSuppression(); logStage("catalog-deps-ready"); const authStorage = agentDiscovery.discoverAuthStorage(agentDir, readOnly ? { readOnly: true } : void 0); logStage("auth-storage-ready"); const registry = agentDiscovery.discoverModels(authStorage, agentDir, { pluginMetadataSnapshot: getManifestMetadataSnapshot(), workspaceDir }); logStage("registry-ready"); const entries = registry.getAll(); logStage("registry-read", `entries=${entries.length}`); const shouldSuppressBuiltInModel = buildShouldSuppressBuiltInModel({ config: cfg }); logStage("suppress-resolver-ready"); for (const entry of entries) { const rawId = normalizeOptionalString(entry?.id) ?? ""; if (!rawId) continue; const provider = normalizeOptionalString(entry?.provider) ?? ""; if (!provider) continue; const id = normalizeConfiguredProviderCatalogModelId(provider, rawId, { manifestPlugins: getManifestPlugins() }); if (shouldSuppressBuiltInModel({ provider, id })) continue; const name = normalizeOptionalString(entry?.name ?? id) || id; const contextWindow = typeof entry?.contextWindow === "number" && entry.contextWindow > 0 ? entry.contextWindow : void 0; const contextTokens = typeof entry?.contextTokens === "number" && entry.contextTokens > 0 ? entry.contextTokens : void 0; const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : void 0; const api = typeof entry?.api === "string" ? entry.api : void 0; const input = Array.isArray(entry?.input) ? entry.input : void 0; const modelParams = entry?.params && typeof entry.params === "object" ? entry.params : void 0; const compat = entry?.compat && typeof entry.compat === "object" ? entry.compat : void 0; models.push({ id, name, provider, ...api ? { api } : {}, contextWindow, ...contextTokens !== void 0 ? { contextTokens } : {}, reasoning, input, ...modelParams ? { params: modelParams } : {}, compat }); } mergeCatalogEntries(models, loadManifestModelCatalog({ config: cfg, env: process.env, metadataSnapshot: getManifestMetadataSnapshot() })); logStage("manifest-models-merged", `entries=${models.length}`); const configuredModels = buildConfiguredModelCatalog({ cfg, manifestPlugins: hasConfiguredProviderModelRows(cfg) ? getManifestPlugins() : void 0 }); let augmentEntries; if (configuredModels.length > 0) { const entriesForAugment = [...models]; mergeCatalogEntries(entriesForAugment, configuredModels); augmentEntries = entriesForAugment; } logStage("configured-models-prepared", `entries=${models.length}`); if (!readOnly) { const { createProviderApiKeyResolver } = await loadProviderApiKeyResolver(); let authStore; const resolveProviderApiKeyForProvider = createProviderApiKeyResolver(process.env, () => authStore ??= ensureAuthProfileStoreWithoutExternalProfiles(agentDir, { allowKeychainPrompt: false }), cfg); const resolveProviderApiKey = (providerId) => providerId?.trim() ? resolveProviderApiKeyForProvider(providerId) : { apiKey: void 0, discoveryApiKey: void 0 }; const supplemental = await augmentModelCatalogWithProviderPlugins({ config: cfg, env: process.env, context: { config: cfg, agentDir, env: process.env, resolveProviderApiKey, entries: augmentEntries ?? [...models] } }); if (supplemental.length > 0) { const normalizedSupplemental = []; for (const entry of supplemental) normalizedSupplemental.push({ ...entry, id: normalizeConfiguredProviderCatalogModelId(entry.provider, entry.id, { manifestPlugins: getManifestPlugins() }) }); mergeCatalogEntries(models, normalizedSupplemental); } } logStage("plugin-models-merged", `entries=${models.length}`); if (configuredModels.length > 0) mergeCatalogEntries(models, configuredModels); logStage("configured-models-finalized", `entries=${models.length}`); if (models.length === 0) { if (useSharedCache) modelCatalogPromise = null; } const sorted = sortModels(models); if (!readOnly) writeCachedAgentModelCatalog({ agentDir, catalogKey, entries: sorted }); logStage("complete", `entries=${sorted.length}`); return sorted; } catch (error) { if (!hasLoggedModelCatalogError) { hasLoggedModelCatalogError = true; log.warn(`Failed to load model catalog: ${String(error)}`); } if (useSharedCache) modelCatalogPromise = null; if (models.length > 0) return sortModels(models); return []; } }; if (readOnly || params?.metadataSnapshot) return loadCatalog(); modelCatalogPromise = loadCatalog(); return modelCatalogPromise; } /** * Check if a model supports image input based on its catalog entry. */ function modelSupportsVision(entry) { return modelSupportsInput(entry, "image"); } /** * Check if a model supports native document/PDF input based on its catalog entry. */ function modelSupportsDocument(entry) { return modelSupportsInput(entry, "document"); } //#endregion export { resetModelCatalogCache as a, modelSupportsVision as i, loadModelCatalog as n, resetModelCatalogCacheForTest as o, modelSupportsDocument as r, setModelCatalogImportForTest as s, loadManifestModelCatalog as t };