import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js"; import { getActivePluginRegistryWorkspaceDirFromState } from "../plugins/runtime-state.js"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "../shared/string-coerce.js"; import { sanitizeForLog, stripAnsi } from "../terminal/ansi.js"; import { resolveConfiguredProviderFallback } from "./configured-provider-fallback.js"; import { DEFAULT_PROVIDER } from "./defaults.js"; import { findModelCatalogEntry } from "./model-catalog-lookup.js"; import type { ModelCatalogEntry } from "./model-catalog.types.js"; import { splitTrailingAuthProfile } from "./model-ref-profile.js"; import { normalizeConfiguredProviderCatalogModelId, normalizeStaticProviderModelId, } from "./model-ref-shared.js"; import { type ModelManifestNormalizationContext, type ModelRef, findNormalizedProviderValue, modelKey, normalizeModelRef, normalizeProviderId, parseModelRef, } from "./model-selection-normalize.js"; let log: ReturnType | null = null; function getLog(): ReturnType { log ??= createSubsystemLogger("model-selection"); return log; } const OPENROUTER_COMPAT_FREE_ALIAS = "openrouter:free"; type ModelManifestPlugins = ModelManifestNormalizationContext["manifestPlugins"]; export type ModelAliasIndex = { byAlias: Map; byKey: Map; }; type ModelManifestPluginContext = { peek: () => ModelManifestPlugins; get: () => ModelManifestPlugins; }; type ModelAliasCandidate = { keyRaw: string; alias: string; }; function resolveManifestPluginsForModelIdNormalization(params: { cfg: OpenClawConfig; workspaceDir?: string; manifestPlugins?: ModelManifestPlugins; allowManifestNormalization?: boolean; }): ModelManifestPlugins { if (params.allowManifestNormalization === false || params.manifestPlugins !== undefined) { return params.manifestPlugins; } const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); if (!workspaceDir) { const currentManifestPlugins = getCurrentPluginMetadataSnapshot({ config: params.cfg, env: process.env, })?.plugins; if (currentManifestPlugins) { return currentManifestPlugins; } return loadManifestMetadataSnapshot({ config: params.cfg, env: process.env, }).plugins; } return loadManifestMetadataSnapshot({ config: params.cfg, workspaceDir, env: process.env, }).plugins; } function createModelManifestPluginContext(params: { cfg: OpenClawConfig; workspaceDir?: string; manifestPlugins?: ModelManifestPlugins; allowManifestNormalization?: boolean; }): ModelManifestPluginContext { let manifestPlugins = params.manifestPlugins; let resolved = params.allowManifestNormalization === false || params.manifestPlugins !== undefined; return { peek: () => manifestPlugins, get: () => { if (!resolved) { manifestPlugins = resolveManifestPluginsForModelIdNormalization(params); resolved = true; } return manifestPlugins; }, }; } function listModelAliasCandidates(cfg: OpenClawConfig): ModelAliasCandidate[] { return Object.entries(cfg.agents?.defaults?.models ?? {}).flatMap(([keyRaw, entryRaw]) => { const trimmedKey = keyRaw.trim(); if (trimmedKey.endsWith("/*") && normalizeProviderId(trimmedKey.slice(0, -2))) { return []; } const alias = normalizeOptionalString((entryRaw as { alias?: string } | undefined)?.alias) ?? ""; return alias ? [{ keyRaw, alias }] : []; }); } function findModelAliasCandidate( cfg: OpenClawConfig, raw: string, ): ModelAliasCandidate | undefined { const aliasKey = normalizeLowercaseStringOrEmpty(raw); let match: ModelAliasCandidate | undefined; for (const candidate of listModelAliasCandidates(cfg)) { if (normalizeLowercaseStringOrEmpty(candidate.alias) === aliasKey) { match = candidate; } } return match; } function sanitizeModelWarningValue(value: string): string { const stripped = value ? stripAnsi(value) : ""; let controlBoundary = -1; for (let index = 0; index < stripped.length; index += 1) { const code = stripped.charCodeAt(index); if (code <= 0x1f || code === 0x7f) { controlBoundary = index; break; } } if (controlBoundary === -1) { return sanitizeForLog(stripped); } return sanitizeForLog(stripped.slice(0, controlBoundary)); } function mergeModelCatalogEntries(params: { primary: readonly ModelCatalogEntry[]; secondary: readonly ModelCatalogEntry[]; }): ModelCatalogEntry[] { const merged = [...params.primary]; const seen = new Set(merged.map((entry) => modelKey(entry.provider, entry.id))); for (const entry of params.secondary) { const key = modelKey(entry.provider, entry.id); if (seen.has(key)) { continue; } merged.push(entry); seen.add(key); } return merged; } export function inferUniqueProviderFromConfiguredModels( params: { cfg: OpenClawConfig; model: string; allowManifestNormalization?: boolean; } & ModelManifestNormalizationContext, ): string | undefined { const model = params.model.trim(); if (!model) { return undefined; } const normalized = normalizeLowercaseStringOrEmpty(model); const providers = new Set(); const addProvider = (provider: string) => { const normalizedProvider = normalizeProviderId(provider); if (!normalizedProvider) { return; } providers.add(normalizedProvider); }; const configuredModels = params.cfg.agents?.defaults?.models; if (configuredModels) { for (const key of Object.keys(configuredModels)) { const ref = key.trim(); if (!ref || !ref.includes("/") || ref.endsWith("/*")) { continue; } const parsed = parseModelRef(ref, DEFAULT_PROVIDER, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: false, manifestPlugins: params.manifestPlugins, }); if (!parsed) { continue; } if (parsed.model === model || normalizeLowercaseStringOrEmpty(parsed.model) === normalized) { addProvider(parsed.provider); if (providers.size > 1) { return undefined; } } } } const configuredProviders = params.cfg.models?.providers; if (configuredProviders) { for (const [providerId, providerConfig] of Object.entries(configuredProviders)) { const models = providerConfig?.models; if (!Array.isArray(models)) { continue; } for (const entry of models) { const modelId = entry?.id?.trim(); if (!modelId) { continue; } const normalizedModelId = normalizeConfiguredProviderCatalogModelId(providerId, modelId, { allowManifestNormalization: params.allowManifestNormalization, manifestPlugins: params.manifestPlugins, }); if ( modelId === model || normalizeLowercaseStringOrEmpty(modelId) === normalized || normalizedModelId === model || normalizeLowercaseStringOrEmpty(normalizedModelId) === normalized ) { addProvider(providerId); } } if (providers.size > 1) { return undefined; } } } if (providers.size !== 1) { return undefined; } return providers.values().next().value; } export function inferUniqueProviderFromCatalog(params: { catalog: readonly ModelCatalogEntry[]; model: string; }): string | undefined { const model = params.model.trim(); if (!model) { return undefined; } const normalized = normalizeLowercaseStringOrEmpty(model); const providers = new Set(); for (const entry of params.catalog) { const entryId = entry.id.trim(); if (!entryId) { continue; } if (entryId !== model && normalizeLowercaseStringOrEmpty(entryId) !== normalized) { continue; } const provider = normalizeProviderId(entry.provider); if (provider) { providers.add(provider); } if (providers.size > 1) { return undefined; } } return providers.size === 1 ? providers.values().next().value : undefined; } export function resolveBareModelDefaultProvider( params: { cfg: OpenClawConfig; catalog: readonly ModelCatalogEntry[]; model: string; defaultProvider: string; } & ModelManifestNormalizationContext, ): string { return ( inferUniqueProviderFromConfiguredModels({ cfg: params.cfg, model: params.model, manifestPlugins: params.manifestPlugins, }) ?? inferUniqueProviderFromCatalog({ catalog: params.catalog, model: params.model }) ?? params.defaultProvider ); } function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean { return ref.provider === "openrouter" && ref.model.includes("/") && ref.model.endsWith(":free"); } function resolveConfiguredOpenRouterCompatFreeRef( params: { cfg: OpenClawConfig; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef | null { const configuredModels = params.cfg.agents?.defaults?.models ?? {}; for (const raw of Object.keys(configuredModels)) { if (!raw.includes("/")) { continue; } const parsed = parseModelRef(raw, params.defaultProvider, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (parsed && isConcreteOpenRouterFreeModelRef(parsed)) { return parsed; } } const openrouterProviderConfig = findNormalizedProviderValue( params.cfg.models?.providers, "openrouter", ); for (const entry of openrouterProviderConfig?.models ?? []) { const modelId = entry?.id?.trim(); if (!modelId || !modelId.includes("/") || !modelId.endsWith(":free")) { continue; } return normalizeModelRef("openrouter", modelId, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); } return null; } export function resolveConfiguredOpenRouterCompatAlias( params: { cfg?: OpenClawConfig; raw: string; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef | null { const normalized = normalizeLowercaseStringOrEmpty(params.raw); if (normalized === "openrouter:auto") { return normalizeModelRef("openrouter", "auto", { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); } if (normalized !== OPENROUTER_COMPAT_FREE_ALIAS || !params.cfg) { return null; } return resolveConfiguredOpenRouterCompatFreeRef({ cfg: params.cfg, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); } function parseModelRefWithCompatAlias( params: { cfg?: OpenClawConfig; raw: string; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef | null { return ( resolveConfiguredOpenRouterCompatAlias(params) ?? resolveExactConfiguredProviderRef(params) ?? parseModelRef(params.raw, params.defaultProvider, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }) ); } function resolveExactConfiguredProviderRef( params: { cfg?: OpenClawConfig; raw: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef | null { const slash = params.raw.indexOf("/"); if (slash <= 0 || !params.cfg?.models?.providers) { return null; } const providerRaw = params.raw.slice(0, slash).trim(); const modelRaw = params.raw.slice(slash + 1).trim(); if (!providerRaw || !modelRaw) { return null; } const providerKey = normalizeLowercaseStringOrEmpty(providerRaw); const exactConfigured = Object.entries(params.cfg.models.providers).find( ([key]) => normalizeLowercaseStringOrEmpty(key) === providerKey, ); if (!exactConfigured) { return null; } const [configuredProvider, providerConfig] = exactConfigured; const normalizedConfiguredProvider = normalizeProviderId(configuredProvider); const apiOwner = typeof providerConfig?.api === "string" ? normalizeProviderId(providerConfig.api) : ""; if (!apiOwner || apiOwner === normalizedConfiguredProvider) { return null; } const provider = normalizeLowercaseStringOrEmpty(configuredProvider); return { provider, model: normalizeConfiguredProviderCatalogModelId( provider, normalizeStaticProviderModelId(provider, modelRaw.trim(), { allowManifestNormalization: params.allowManifestNormalization, manifestPlugins: params.manifestPlugins, }), { allowManifestNormalization: params.allowManifestNormalization, manifestPlugins: params.manifestPlugins, }, ), }; } export function resolveAllowlistModelKey( params: { cfg?: OpenClawConfig; raw: string; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): string | null { const parsed = parseModelRefWithCompatAlias({ cfg: params.cfg, raw: params.raw, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (!parsed) { return null; } return modelKey(parsed.provider, parsed.model); } export function buildConfiguredAllowlistKeys( params: { cfg: OpenClawConfig | undefined; defaultProvider: string; } & ModelManifestNormalizationContext, ): Set | null { const visibility = parseConfiguredModelVisibilityEntries({ cfg: params.cfg }); if (visibility.exactModelRefs.length === 0) { return null; } const keys = new Set(); for (const raw of visibility.exactModelRefs) { const key = resolveAllowlistModelKey({ cfg: params.cfg, raw, defaultProvider: params.defaultProvider, manifestPlugins: params.manifestPlugins, }); if (key) { keys.add(key); } } return keys.size > 0 ? keys : null; } type BuildModelAliasIndexParams = { cfg: OpenClawConfig; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext; function buildModelAliasIndexWithManifestContext( params: Omit & { manifestPluginContext: ModelManifestPluginContext; }, ): ModelAliasIndex { const byAlias = new Map(); const byKey = new Map(); const aliasCandidates = listModelAliasCandidates(params.cfg); if (aliasCandidates.length === 0) { return { byAlias, byKey }; } const manifestPlugins = params.manifestPluginContext.get(); for (const { keyRaw, alias } of aliasCandidates) { const parsed = parseModelRefWithCompatAlias({ cfg: params.cfg, raw: keyRaw, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins, }); if (!parsed) { continue; } const aliasKey = normalizeLowercaseStringOrEmpty(alias); byAlias.set(aliasKey, { alias, ref: parsed }); const key = modelKey(parsed.provider, parsed.model); const existing = byKey.get(key) ?? []; existing.push(alias); byKey.set(key, existing); } return { byAlias, byKey }; } export function buildModelAliasIndex(params: BuildModelAliasIndexParams): ModelAliasIndex { return buildModelAliasIndexWithManifestContext({ cfg: params.cfg, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPluginContext: createModelManifestPluginContext(params), }); } type ModelCatalogMetadata = { configuredByKey: Map; aliasByKey: Map; }; function buildModelCatalogMetadata( params: { cfg: OpenClawConfig; defaultProvider: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelCatalogMetadata { const configuredByKey = new Map(); for (const entry of buildConfiguredModelCatalog({ cfg: params.cfg, manifestPlugins: params.manifestPlugins, })) { configuredByKey.set(modelKey(entry.provider, entry.id), entry); } const aliasByKey = new Map(); const configuredModels = params.cfg.agents?.defaults?.models ?? {}; for (const [rawKey, entryRaw] of Object.entries(configuredModels)) { if (rawKey.trim().endsWith("/*")) { continue; } const key = resolveAllowlistModelKey({ cfg: params.cfg, raw: rawKey, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (!key) { continue; } const alias = ((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); if (alias) { aliasByKey.set(key, alias); } } return { configuredByKey, aliasByKey }; } function applyModelCatalogMetadata(params: { entry: ModelCatalogEntry; metadata: ModelCatalogMetadata; }): ModelCatalogEntry { const key = modelKey(params.entry.provider, params.entry.id); const configuredEntry = params.metadata.configuredByKey.get(key); const alias = params.metadata.aliasByKey.get(key); if (!configuredEntry && !alias) { return params.entry; } const nextAlias = alias ?? params.entry.alias; const nextContextWindow = configuredEntry?.contextWindow ?? params.entry.contextWindow; const nextContextTokens = configuredEntry?.contextTokens ?? params.entry.contextTokens; const nextReasoning = configuredEntry?.reasoning ?? params.entry.reasoning; const nextInput = configuredEntry?.input ?? params.entry.input; const nextCompat = params.entry.compat || configuredEntry?.compat ? { ...params.entry.compat, ...configuredEntry?.compat } : undefined; return { ...params.entry, name: configuredEntry?.name ?? params.entry.name, ...(nextAlias ? { alias: nextAlias } : {}), ...(nextContextWindow !== undefined ? { contextWindow: nextContextWindow } : {}), ...(nextContextTokens !== undefined ? { contextTokens: nextContextTokens } : {}), ...(nextReasoning !== undefined ? { reasoning: nextReasoning } : {}), ...(nextInput ? { input: nextInput } : {}), ...(nextCompat ? { compat: nextCompat } : {}), }; } function buildSyntheticAllowedCatalogEntry(params: { parsed: ModelRef; metadata: ModelCatalogMetadata; }): ModelCatalogEntry { const key = modelKey(params.parsed.provider, params.parsed.model); const configuredEntry = params.metadata.configuredByKey.get(key); const alias = params.metadata.aliasByKey.get(key); const nextContextWindow = configuredEntry?.contextWindow; const nextContextTokens = configuredEntry?.contextTokens; const nextReasoning = configuredEntry?.reasoning; const nextInput = configuredEntry?.input; const nextCompat = configuredEntry?.compat; return { id: params.parsed.model, name: configuredEntry?.name ?? params.parsed.model, provider: params.parsed.provider, ...(alias ? { alias } : {}), ...(nextContextWindow !== undefined ? { contextWindow: nextContextWindow } : {}), ...(nextContextTokens !== undefined ? { contextTokens: nextContextTokens } : {}), ...(nextReasoning !== undefined ? { reasoning: nextReasoning } : {}), ...(nextInput ? { input: nextInput } : {}), ...(nextCompat ? { compat: nextCompat } : {}), }; } export function resolveModelRefFromString( params: { cfg?: OpenClawConfig; raw: string; defaultProvider: string; aliasIndex?: ModelAliasIndex; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): { ref: ModelRef; alias?: string } | null { const { model } = splitTrailingAuthProfile(params.raw); if (!model) { return null; } const aliasKey = normalizeLowercaseStringOrEmpty(model); const aliasMatch = params.aliasIndex?.byAlias.get(aliasKey); if (aliasMatch) { return { ref: aliasMatch.ref, alias: aliasMatch.alias }; } const parsed = parseModelRefWithCompatAlias({ cfg: params.cfg, raw: model, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (!parsed) { return null; } return { ref: parsed }; } export function resolveConfiguredModelRef( params: { cfg: OpenClawConfig; defaultProvider: string; defaultModel: string; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef { const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? ""; if (rawModel) { const trimmed = rawModel.trim(); const { model: modelWithoutProfile } = splitTrailingAuthProfile(trimmed); const manifestPluginContext = createModelManifestPluginContext(params); const aliasCandidate = findModelAliasCandidate(params.cfg, trimmed) ?? (modelWithoutProfile && modelWithoutProfile !== trimmed ? findModelAliasCandidate(params.cfg, modelWithoutProfile) : undefined); const manifestPlugins = manifestPluginContext.peek(); if (aliasCandidate) { const aliasRef = parseModelRefWithCompatAlias({ cfg: params.cfg, raw: aliasCandidate.keyRaw, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: manifestPluginContext.get(), }); if (aliasRef) { return aliasRef; } } if (!trimmed.includes("/")) { const normalizedTrimmed = normalizeLowercaseStringOrEmpty(trimmed); const needsOpenRouterCompatManifestPlugins = normalizedTrimmed === "openrouter:auto" || normalizedTrimmed === OPENROUTER_COMPAT_FREE_ALIAS; const openrouterCompatRef = resolveConfiguredOpenRouterCompatAlias({ cfg: params.cfg, raw: trimmed, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: needsOpenRouterCompatManifestPlugins ? manifestPluginContext.get() : manifestPlugins, }); if (openrouterCompatRef) { return openrouterCompatRef; } let inferredProvider = inferUniqueProviderFromConfiguredModels({ cfg: params.cfg, model: trimmed, allowManifestNormalization: false, manifestPlugins, }); let inferredProviderManifestPlugins = manifestPlugins; if ( (!inferredProvider || inferredProvider !== "openai") && hasConfiguredRowsNeedingManifestLookup(params.cfg, params.defaultProvider) ) { inferredProviderManifestPlugins = manifestPluginContext.get(); inferredProvider = inferUniqueProviderFromConfiguredModels({ cfg: params.cfg, model: trimmed, allowManifestNormalization: params.allowManifestNormalization, manifestPlugins: inferredProviderManifestPlugins, }) ?? inferredProvider; } if (inferredProvider) { return normalizeModelRef(inferredProvider, trimmed, { allowManifestNormalization: inferredProviderManifestPlugins ? params.allowManifestNormalization : false, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: inferredProviderManifestPlugins, }); } const safeTrimmed = sanitizeModelWarningValue(trimmed); const safeResolved = sanitizeForLog(`${params.defaultProvider}/${safeTrimmed}`); getLog().warn( `Model "${safeTrimmed}" specified without provider. Falling back to "${safeResolved}". Please use "${safeResolved}" in your config.`, ); return { provider: params.defaultProvider, model: trimmed }; } const resolved = resolveModelRefFromString({ cfg: params.cfg, raw: trimmed, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: manifestPluginContext.get(), }); if (resolved) { return resolved.ref; } const safe = sanitizeForLog(trimmed); const safeFallback = sanitizeForLog(`${params.defaultProvider}/${params.defaultModel}`); getLog().warn( `Model "${safe}" could not be resolved. Falling back to default "${safeFallback}".`, ); } const fallbackProvider = resolveConfiguredProviderFallback({ cfg: params.cfg, defaultProvider: params.defaultProvider, }); if (fallbackProvider) { return fallbackProvider; } return { provider: params.defaultProvider, model: params.defaultModel }; } export function buildAllowedModelSetWithFallbacks( params: { cfg: OpenClawConfig; catalog: ModelCatalogEntry[]; defaultProvider: string; defaultModel?: string; fallbackModels: readonly string[]; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): { allowAny: boolean; allowedCatalog: ModelCatalogEntry[]; allowedKeys: Set; } { const metadata = buildModelCatalogMetadata({ cfg: params.cfg, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); const configuredCatalog = buildConfiguredModelCatalog({ cfg: params.cfg, manifestPlugins: params.manifestPlugins, }); const catalog = mergeModelCatalogEntries({ primary: params.catalog, secondary: configuredCatalog, }).map((entry) => applyModelCatalogMetadata({ entry, metadata })); const visibility = parseConfiguredModelVisibilityEntries({ cfg: params.cfg }); const allowAny = !visibility.hasEntries; const defaultModelNormalization = allowAny ? { allowManifestNormalization: false, allowPluginNormalization: false, manifestPlugins: params.manifestPlugins, } : { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }; const defaultModel = params.defaultModel?.trim(); const defaultRef = defaultModel && params.defaultProvider ? parseModelRefWithCompatAlias({ cfg: params.cfg, raw: defaultModel, defaultProvider: params.defaultProvider, ...defaultModelNormalization, }) : null; const defaultKey = defaultRef ? modelKey(defaultRef.provider, defaultRef.model) : undefined; const catalogKeys = new Set(); for (const entry of catalog) { catalogKeys.add(modelKey(entry.provider, entry.id)); } if (allowAny) { if (defaultKey) { catalogKeys.add(defaultKey); } return { allowAny: true, allowedCatalog: catalog, allowedKeys: catalogKeys, }; } const allowedKeys = new Set(); const allowedRefs: ModelRef[] = []; const syntheticCatalogEntries = new Map(); for (const provider of visibility.providerWildcards) { allowedKeys.add(providerWildcardModelKey(provider)); } const addAllowedCatalogRef = (ref: ModelRef) => { if ( !allowedRefs.some( (existing) => modelKey(existing.provider, existing.model) === modelKey(ref.provider, ref.model), ) ) { allowedRefs.push(ref); } }; for (const entry of catalog) { if (!visibility.providerWildcards.has(normalizeProviderId(entry.provider))) { continue; } allowedKeys.add(modelKey(entry.provider, entry.id)); addAllowedCatalogRef({ provider: entry.provider, model: entry.id }); } const addAllowedModelRef = (raw: string) => { const trimmed = raw.trim(); const defaultProvider = !trimmed.includes("/") ? resolveBareModelDefaultProvider({ cfg: params.cfg, catalog, model: trimmed, defaultProvider: params.defaultProvider, manifestPlugins: params.manifestPlugins, }) : params.defaultProvider; const parsed = parseModelRefWithCompatAlias({ cfg: params.cfg, raw, defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (!parsed) { return; } const key = modelKey(parsed.provider, parsed.model); allowedKeys.add(key); addAllowedCatalogRef(parsed); if ( !findModelCatalogEntry(catalog, { provider: parsed.provider, modelId: parsed.model }) && !syntheticCatalogEntries.has(key) ) { syntheticCatalogEntries.set(key, buildSyntheticAllowedCatalogEntry({ parsed, metadata })); } }; for (const raw of visibility.exactModelRefs) { addAllowedModelRef(raw); } if (visibility.exactModelRefs.length > 0) { for (const fallback of params.fallbackModels) { addAllowedModelRef(fallback); } } if ( defaultKey && ((visibility.exactModelRefs.length > 0 && visibility.providerWildcards.size === 0) || (defaultRef && visibility.providerWildcards.has(normalizeProviderId(defaultRef.provider)))) ) { allowedKeys.add(defaultKey); if (defaultRef) { addAllowedCatalogRef(defaultRef); } } const allowedCatalog = [ ...catalog.filter((entry) => allowedRefs.some( (ref) => findModelCatalogEntry([entry], { provider: ref.provider, modelId: ref.model }) === entry, ), ), ...syntheticCatalogEntries.values(), ]; if ( allowedCatalog.length === 0 && allowedKeys.size === 0 && visibility.providerWildcards.size === 0 ) { if (defaultKey) { catalogKeys.add(defaultKey); } return { allowAny: true, allowedCatalog: catalog, allowedKeys: catalogKeys, }; } return { allowAny: false, allowedCatalog, allowedKeys }; } export type ModelRefStatus = { key: string; inCatalog: boolean; allowAny: boolean; allowed: boolean; }; export type ResolveAllowedModelRefResult = | { ref: ModelRef; key: string } | { error: string; }; function getModelRefStatusFromAllowedSet(params: { catalog: ModelCatalogEntry[]; ref: ModelRef; allowed: { allowAny: boolean; allowedKeys: Set; }; }): ModelRefStatus { const key = modelKey(params.ref.provider, params.ref.model); return { key, inCatalog: Boolean( findModelCatalogEntry(params.catalog, { provider: params.ref.provider, modelId: params.ref.model, }), ), allowAny: params.allowed.allowAny, allowed: params.allowed.allowAny || isModelKeyAllowedBySet(params.allowed.allowedKeys, key), }; } export function getModelRefStatusWithFallbackModels( params: { cfg: OpenClawConfig; catalog: ModelCatalogEntry[]; ref: ModelRef; defaultProvider: string; defaultModel?: string; fallbackModels: readonly string[]; } & ModelManifestNormalizationContext, ): ModelRefStatus { const allowed = buildAllowedModelSetWithFallbacks({ cfg: params.cfg, catalog: params.catalog, defaultProvider: params.defaultProvider, defaultModel: params.defaultModel, fallbackModels: params.fallbackModels, manifestPlugins: params.manifestPlugins, }); return getModelRefStatusFromAllowedSet({ catalog: params.catalog, ref: params.ref, allowed, }); } export function resolveAllowedModelRefFromAliasIndex( params: { cfg: OpenClawConfig; raw: string; defaultProvider: string; aliasIndex: ModelAliasIndex; getStatus: (ref: ModelRef) => ModelRefStatus; } & ModelManifestNormalizationContext, ): ResolveAllowedModelRefResult { const trimmed = params.raw.trim(); if (!trimmed) { return { error: "invalid model: empty" }; } const effectiveDefaultProvider = !trimmed.includes("/") ? (inferUniqueProviderFromConfiguredModels({ cfg: params.cfg, model: trimmed, manifestPlugins: params.manifestPlugins, }) ?? params.defaultProvider) : params.defaultProvider; const resolved = resolveModelRefFromString({ cfg: params.cfg, raw: trimmed, defaultProvider: effectiveDefaultProvider, aliasIndex: params.aliasIndex, manifestPlugins: params.manifestPlugins, }); if (!resolved) { return { error: `invalid model: ${trimmed}` }; } const status = params.getStatus(resolved.ref); if (!status.allowed) { return { error: `model not allowed: ${status.key}` }; } return { ref: resolved.ref, key: status.key }; } export function hasConfiguredProviderModelRows(cfg: OpenClawConfig): boolean { const providers = cfg.models?.providers; if (!providers || typeof providers !== "object") { return false; } return Object.values(providers).some((provider) => Array.isArray(provider?.models)); } function hasConfiguredProviderRowsNeedingManifestLookup(cfg: OpenClawConfig): boolean { 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 hasConfiguredModelRefsNeedingManifestLookup( cfg: OpenClawConfig, defaultProvider: string, ): boolean { const configuredModels = cfg.agents?.defaults?.models; if (!configuredModels || typeof configuredModels !== "object") { return false; } const normalizedDefaultProvider = normalizeProviderId(defaultProvider); return Object.keys(configuredModels).some((keyRaw) => { const key = keyRaw.trim(); if (!key || key.endsWith("/*")) { return false; } const slashIndex = key.indexOf("/"); if (slashIndex <= 0) { return false; } const provider = normalizeProviderId(key.slice(0, slashIndex)); return Boolean(provider && provider !== normalizedDefaultProvider); }); } function hasConfiguredRowsNeedingManifestLookup( cfg: OpenClawConfig, defaultProvider: string, ): boolean { return ( hasConfiguredProviderRowsNeedingManifestLookup(cfg) || hasConfiguredModelRefsNeedingManifestLookup(cfg, defaultProvider) ); } function resolveConfiguredModelManifestPlugins(params: { cfg: OpenClawConfig; workspaceDir?: string; manifestPlugins?: ModelManifestPlugins; }): ModelManifestPlugins { if (params.manifestPlugins) { return params.manifestPlugins; } if (!hasConfiguredProviderModelRows(params.cfg)) { return undefined; } const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); if (!workspaceDir) { return ( getCurrentPluginMetadataSnapshot({ config: params.cfg, env: process.env, })?.plugins ?? [] ); } return loadManifestMetadataSnapshot({ config: params.cfg, workspaceDir, env: process.env, }).plugins; } export function buildConfiguredModelCatalog(params: { cfg: OpenClawConfig; workspaceDir?: string; manifestPlugins?: ModelManifestPlugins; }): ModelCatalogEntry[] { const providers = params.cfg.models?.providers; if (!providers || typeof providers !== "object") { return []; } const manifestPlugins = resolveConfiguredModelManifestPlugins(params); const catalog: ModelCatalogEntry[] = []; for (const [providerRaw, provider] of Object.entries(providers)) { const providerId = normalizeProviderId(providerRaw); if (!providerId || !Array.isArray(provider?.models)) { continue; } for (const model of provider.models) { const rawId = normalizeOptionalString(model?.id) ?? ""; const id = rawId ? normalizeConfiguredProviderCatalogModelId(providerId, rawId, { manifestPlugins }) : ""; if (!id) { continue; } const name = normalizeOptionalString(model?.name) || id; const contextWindow = typeof model?.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined; const contextTokens = typeof model?.contextTokens === "number" && model.contextTokens > 0 ? model.contextTokens : undefined; const input = Array.isArray(model?.input) ? model.input : undefined; const compat = model?.compat && typeof model.compat === "object" ? model.compat : undefined; const reasoning = typeof model?.reasoning === "boolean" ? model.reasoning : isVllmQwenThinkingCompat(providerId, compat) ? true : undefined; catalog.push({ provider: providerId, id, name, contextWindow, contextTokens, reasoning, input, compat, }); } } return catalog; } function isVllmQwenThinkingCompat( providerId: string, compat?: { thinkingFormat?: unknown } | null, ): boolean { return ( providerId === "vllm" && (compat?.thinkingFormat === "qwen" || compat?.thinkingFormat === "qwen-chat-template") ); } export function resolveHooksGmailModel( params: { cfg: OpenClawConfig; defaultProvider: string; } & ModelManifestNormalizationContext, ): ModelRef | null { const hooksModel = params.cfg.hooks?.gmail?.model; if (!hooksModel?.trim()) { return null; } const aliasIndex = buildModelAliasIndex({ cfg: params.cfg, defaultProvider: params.defaultProvider, manifestPlugins: params.manifestPlugins, }); const resolved = resolveModelRefFromString({ cfg: params.cfg, raw: hooksModel, defaultProvider: params.defaultProvider, aliasIndex, manifestPlugins: params.manifestPlugins, }); return resolved?.ref ?? null; } export function normalizeModelSelection(value: unknown): string | undefined { if (typeof value === "string") { const trimmed = value.trim(); return trimmed || undefined; } if (!value || typeof value !== "object") { return undefined; } const primary = (value as { primary?: unknown }).primary; if (typeof primary === "string" && primary.trim()) { return primary.trim(); } return undefined; } export function parseConfiguredModelVisibilityEntries(params: { cfg?: OpenClawConfig }): { exactModelRefs: string[]; providerWildcards: Set; hasEntries: boolean; } { const rawModels = Object.keys(params.cfg?.agents?.defaults?.models ?? {}); const exactModelRefs: string[] = []; const providerWildcards = new Set(); for (const raw of rawModels) { const trimmed = raw.trim(); if (!trimmed) { continue; } if (trimmed.endsWith("/*")) { const provider = normalizeProviderId(trimmed.slice(0, -2)); if (provider) { providerWildcards.add(provider); continue; } } exactModelRefs.push(raw); } return { exactModelRefs, providerWildcards, hasEntries: rawModels.length > 0, }; } export function providerWildcardModelKey(provider: string): string { return modelKey(normalizeProviderId(provider), "*"); } export function isModelKeyAllowedBySet(allowedKeys: ReadonlySet, key: string): boolean { if (allowedKeys.has(key)) { return true; } const separator = key.indexOf("/"); if (separator <= 0) { return false; } return allowedKeys.has(providerWildcardModelKey(key.slice(0, separator))); } export function resolveAllowedModelSelection( params: { provider: string; model: string; allowAny: boolean; allowedKeys: ReadonlySet; allowedCatalog: readonly ModelCatalogEntry[]; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelRef | null { const current = normalizeModelRef(params.provider, params.model, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if ( params.allowAny || isModelKeyAllowedBySet(params.allowedKeys, modelKey(current.provider, current.model)) ) { return current; } const fallback = params.allowedCatalog[0]; if (!fallback) { return null; } return normalizeModelRef(fallback.provider, fallback.id, { allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); } export type ModelVisibilityPolicy = { allowAny: boolean; allowedCatalog: ModelCatalogEntry[]; allowedKeys: Set; exactModelRefs: readonly string[]; providerWildcards: ReadonlySet; hasConfiguredEntries: boolean; hasProviderWildcards: boolean; allowsKey: (key: string) => boolean; allows: (ref: { provider: string; model: string }) => boolean; resolveSelection: (ref: { provider: string; model: string }) => ModelRef | null; visibleCatalog: (params: { catalog: readonly ModelCatalogEntry[]; defaultVisibleCatalog: readonly ModelCatalogEntry[]; view?: "default" | "configured" | "all"; }) => ModelCatalogEntry[]; }; function dedupeModelCatalogEntries(entries: readonly ModelCatalogEntry[]): ModelCatalogEntry[] { const seen = new Set(); const next: ModelCatalogEntry[] = []; for (const entry of entries) { const key = modelKey(entry.provider, entry.id); if (seen.has(key)) { continue; } seen.add(key); next.push(entry); } return next; } export function createModelVisibilityPolicyWithFallbacks( params: { cfg: OpenClawConfig; catalog: ModelCatalogEntry[]; defaultProvider: string; defaultModel?: string; fallbackModels: readonly string[]; allowManifestNormalization?: boolean; allowPluginNormalization?: boolean; } & ModelManifestNormalizationContext, ): ModelVisibilityPolicy { const visibility = parseConfiguredModelVisibilityEntries({ cfg: params.cfg }); const allowed = buildAllowedModelSetWithFallbacks(params); const allowsKey = (key: string): boolean => allowed.allowAny || isModelKeyAllowedBySet(allowed.allowedKeys, key); const exactConfiguredKeys = new Set(); for (const raw of visibility.exactModelRefs) { const key = resolveAllowlistModelKey({ cfg: params.cfg, raw, defaultProvider: params.defaultProvider, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }); if (key) { exactConfiguredKeys.add(key); } } const policy: ModelVisibilityPolicy = { allowAny: allowed.allowAny, allowedCatalog: allowed.allowedCatalog, allowedKeys: allowed.allowedKeys, exactModelRefs: visibility.exactModelRefs, providerWildcards: visibility.providerWildcards, hasConfiguredEntries: visibility.hasEntries, hasProviderWildcards: visibility.providerWildcards.size > 0, allowsKey, allows: (ref) => allowsKey(modelKey(ref.provider, ref.model)), resolveSelection: (ref) => resolveAllowedModelSelection({ provider: ref.provider, model: ref.model, allowAny: allowed.allowAny, allowedKeys: allowed.allowedKeys, allowedCatalog: allowed.allowedCatalog, allowManifestNormalization: params.allowManifestNormalization, allowPluginNormalization: params.allowPluginNormalization, manifestPlugins: params.manifestPlugins, }), visibleCatalog: ({ catalog, defaultVisibleCatalog, view }) => { if (view === "all") { return [...catalog]; } if (allowed.allowAny) { return [...defaultVisibleCatalog]; } if (visibility.providerWildcards.size === 0) { return [...allowed.allowedCatalog]; } return dedupeModelCatalogEntries([ ...defaultVisibleCatalog.filter((entry) => visibility.providerWildcards.has(normalizeProviderId(entry.provider)), ), ...allowed.allowedCatalog.filter( (entry) => !visibility.providerWildcards.has(normalizeProviderId(entry.provider)) || exactConfiguredKeys.has(modelKey(entry.provider, entry.id)), ), ]); }, }; return policy; }