import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { resolveVisibleModelCatalog } from "../agents/model-catalog-visibility.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.js"; import { isModelPickerVisibleModelRef, isModelPickerVisibleProvider, } from "../agents/model-picker-visibility.js"; import { createProviderAuthChecker } from "../agents/model-provider-auth.js"; import { formatLiteralProviderPrefixedModelRef } from "../agents/model-ref-shared.js"; import { buildConfiguredModelCatalog, buildModelAliasIndex, type ModelAliasIndex, modelKey, normalizeModelRef, normalizeProviderId, resolveConfiguredModelRef, resolveModelRefFromString, } from "../agents/model-selection.js"; import { loadStaticManifestCatalogRowsForList } from "../commands/models/list.manifest-catalog.js"; import { formatTokenK } from "../commands/models/shared.js"; import { normalizeAgentModelRefForConfig, resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { applyPrimaryModel } from "../plugins/provider-model-primary.js"; import { resolveOwningPluginIdsForProvider } from "../plugins/providers.js"; import type { ProviderPlugin } from "../plugins/types.js"; import type { RuntimeEnv } from "../runtime.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js"; export { applyPrimaryModel } from "../plugins/provider-model-primary.js"; const KEEP_VALUE = "__keep__"; const MANUAL_VALUE = "__manual__"; const BROWSE_VALUE = "__browse__"; const PROVIDER_FILTER_THRESHOLD = 30; const EMPTY_LITERAL_PREFIX_PROVIDERS = new Set(); // Internal router models are valid defaults during auth/setup but not manual API targets. const HIDDEN_ROUTER_MODELS = new Set(["openrouter/auto"]); export type PromptDefaultModelParams = { config: OpenClawConfig; prompter: WizardPrompter; allowKeep?: boolean; includeManual?: boolean; includeProviderPluginSetups?: boolean; ignoreAllowlist?: boolean; loadCatalog?: boolean; browseCatalogOnDemand?: boolean; preferredProvider?: string; agentDir?: string; workspaceDir?: string; env?: NodeJS.ProcessEnv; runtime?: RuntimeEnv; message?: string; }; export type PromptDefaultModelResult = { model?: string; config?: OpenClawConfig }; export type PromptModelAllowlistResult = { models?: string[]; scopeKeys?: string[] }; async function loadModelPickerRuntime() { return import("../commands/model-picker.runtime.js"); } const loadResolvedModelPickerRuntime = createLazyRuntimeSurface( loadModelPickerRuntime, ({ modelPickerRuntime }) => modelPickerRuntime, ); function resolveConfiguredModelRaw(cfg: OpenClawConfig): string { return resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ?? ""; } function resolveConfiguredModelKeys(cfg: OpenClawConfig): string[] { const models = cfg.agents?.defaults?.models ?? {}; return Object.keys(models) .map((key) => key.trim()) .filter((key) => key.length > 0); } function toPickerCatalogEntry( row: ReturnType[number], ): ModelCatalogEntry { return { id: row.id, name: row.name, provider: row.provider, ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}), reasoning: row.reasoning, input: row.input, }; } function loadPickerModelCatalog( cfg: OpenClawConfig, opts: { preferredProvider?: string } = {}, ): ReturnType { if (cfg.models?.mode === "replace") { return Promise.resolve(buildConfiguredModelCatalog({ cfg })); } if (opts.preferredProvider) { const manifestRows = loadStaticManifestCatalogRowsForList({ cfg, providerFilter: opts.preferredProvider, }); if (manifestRows.length > 0) { return Promise.resolve(manifestRows.map(toPickerCatalogEntry)); } } return loadModelCatalog({ config: cfg, }); } function normalizeModelKeys(values: string[]): string[] { const seen = new Set(); const next: string[] = []; for (const raw of values) { const value = normalizeAgentModelRefForConfig(raw); if (!value || seen.has(value)) { continue; } seen.add(value); next.push(value); } return next; } function resolveFallbackModelKey(params: { cfg: OpenClawConfig; raw: string; defaultProvider: string; aliasIndex: ModelAliasIndex; }): string | undefined { const raw = normalizeOptionalString(params.raw); if (!raw) { return undefined; } const resolved = resolveModelRefFromString({ cfg: params.cfg, raw, defaultProvider: params.defaultProvider, aliasIndex: params.aliasIndex, }); if (!resolved) { return undefined; } return modelKey(resolved.ref.provider, resolved.ref.model); } function resolveFallbackModelKeys(params: { cfg: OpenClawConfig; rawFallbacks: string[]; defaultProvider: string; aliasIndex: ModelAliasIndex; }): string[] { return normalizeModelKeys( params.rawFallbacks .map((raw) => resolveFallbackModelKey({ cfg: params.cfg, raw, defaultProvider: params.defaultProvider, aliasIndex: params.aliasIndex, }), ) .filter((key): key is string => Boolean(key)), ); } function resolveModelRouteHint(provider: string): string | undefined { const normalized = normalizeProviderId(provider); if (normalized === "openai") { return "Codex runtime route"; } if (normalized === "openai-codex") { return "legacy Codex OAuth route"; } return undefined; } async function resolveLiteralPrefixProviderIds(params: { cfg: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }): Promise> { const { resolvePluginProviders } = await loadResolvedModelPickerRuntime(); const providers = resolvePluginProviders({ config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, activate: false, cache: false, includeUntrustedWorkspacePlugins: false, }); const ids = new Set(); for (const provider of providers) { if (!provider.preserveLiteralProviderPrefix) { continue; } const id = normalizeProviderId(provider.id); if (id) { ids.add(id); } for (const alias of provider.aliases ?? []) { const aliasId = normalizeProviderId(alias); if (aliasId) { ids.add(aliasId); } } } return ids; } function addModelSelectOption(params: { entry: { provider: string; id: string; name?: string; contextWindow?: number; reasoning?: boolean; }; options: WizardSelectOption[]; seen: Set; aliasIndex: ReturnType; hasAuth: (provider: string) => boolean; literalPrefixProviders: Set; }) { const normalizedRef = normalizeModelRef(params.entry.provider, params.entry.id); const key = modelKey(normalizedRef.provider, normalizedRef.model); if ( params.seen.has(key) || HIDDEN_ROUTER_MODELS.has(key) || !isModelPickerVisibleProvider(normalizedRef.provider) ) { return; } const hints: string[] = []; if (params.entry.name && params.entry.name !== params.entry.id) { hints.push(params.entry.name); } if (params.entry.contextWindow) { hints.push(`ctx ${formatTokenK(params.entry.contextWindow)}`); } if (params.entry.reasoning) { hints.push("reasoning"); } const aliases = params.aliasIndex.byKey.get(key); if (aliases?.length) { hints.push(`alias: ${aliases.join(", ")}`); } const routeHint = resolveModelRouteHint(normalizedRef.provider); if (routeHint) { hints.push(routeHint); } if (!params.hasAuth(normalizedRef.provider)) { return; } const label = params.literalPrefixProviders.has(normalizeProviderId(normalizedRef.provider)) ? formatLiteralProviderPrefixedModelRef(normalizedRef.provider, key) : key; params.options.push({ value: key, label, hint: hints.length > 0 ? hints.join(" ยท ") : undefined, }); params.seen.add(key); } function splitModelKey(key: string): { provider: string; id: string } | undefined { const slashIndex = key.indexOf("/"); if (slashIndex <= 0 || slashIndex >= key.length - 1) { return undefined; } return { provider: key.slice(0, slashIndex), id: key.slice(slashIndex + 1), }; } function addModelKeySelectOption(params: { key: string; options: WizardSelectOption[]; seen: Set; aliasIndex: ReturnType; hasAuth: (provider: string) => boolean; literalPrefixProviders?: Set; fallbackHint: string; }) { const entry = splitModelKey(params.key); if (!entry) { return; } const before = params.seen.size; addModelSelectOption({ entry, options: params.options, seen: params.seen, aliasIndex: params.aliasIndex, hasAuth: params.hasAuth, literalPrefixProviders: params.literalPrefixProviders ?? EMPTY_LITERAL_PREFIX_PROVIDERS, }); if (params.seen.size > before) { const option = params.options.at(-1); if (option && !option.hint) { option.hint = params.fallbackHint; } } } function createPreferredProviderMatcher(params: { preferredProvider: string; cfg: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }): (entryProvider: string) => boolean { const normalizedPreferredProvider = normalizeProviderId(params.preferredProvider); const preferredOwnerPluginIds = resolveOwningPluginIdsForProvider({ provider: normalizedPreferredProvider, config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, }); const preferredOwnerPluginIdSet = preferredOwnerPluginIds ? new Set(preferredOwnerPluginIds) : undefined; const entryProviderCache = new Map(); return (entryProvider: string) => { const normalizedEntryProvider = normalizeProviderId(entryProvider); if (normalizedEntryProvider === normalizedPreferredProvider) { return true; } const cached = entryProviderCache.get(normalizedEntryProvider); if (cached !== undefined) { return cached; } const value = !!preferredOwnerPluginIdSet && !!resolveOwningPluginIdsForProvider({ provider: normalizedEntryProvider, config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, })?.some((pluginId) => preferredOwnerPluginIdSet.has(pluginId)); entryProviderCache.set(normalizedEntryProvider, value); return value; }; } async function promptManualModel(params: { prompter: WizardPrompter; allowBlank: boolean; initialValue?: string; }): Promise { const modelInput = await params.prompter.text({ message: params.allowBlank ? "Default model (blank to keep)" : "Default model", initialValue: params.initialValue, placeholder: "provider/model", validate: params.allowBlank ? undefined : (value) => (normalizeOptionalString(value) ? undefined : "Required"), }); const model = (modelInput ?? "").trim(); if (!model) { return {}; } return { model: normalizeAgentModelRefForConfig(model) }; } function buildModelProviderFilterOptions( models: Array<{ provider: string }>, ): Array<{ value: string; label: string; hint: string }> { const providerIds = Array.from(new Set(models.map((entry) => entry.provider))).toSorted((a, b) => a.localeCompare(b), ); return providerIds.map((provider) => { const count = models.filter((entry) => entry.provider === provider).length; return { value: provider, label: provider, hint: `${count} model${count === 1 ? "" : "s"}`, }; }); } async function maybeFilterModelsByProvider(params: { models: Array<{ provider: string; id: string; name?: string; contextWindow?: number; reasoning?: boolean; }>; preferredProvider?: string; prompter: WizardPrompter; cfg: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }): Promise { let next = params.models.filter((entry) => isModelPickerVisibleProvider(entry.provider)); const providerIds = Array.from(new Set(next.map((entry) => entry.provider))).toSorted((a, b) => a.localeCompare(b), ); const hasPreferredProvider = !!params.preferredProvider; const shouldPromptProvider = !hasPreferredProvider && providerIds.length > 1 && next.length > PROVIDER_FILTER_THRESHOLD; const matchesPreferredProvider = params.preferredProvider ? createPreferredProviderMatcher({ preferredProvider: params.preferredProvider, cfg: params.cfg, workspaceDir: params.workspaceDir, env: params.env, }) : undefined; if (shouldPromptProvider) { const selection = await params.prompter.select({ message: "Filter models by provider", options: [{ value: "*", label: "All providers" }, ...buildModelProviderFilterOptions(next)], searchable: true, }); if (selection !== "*") { next = next.filter((entry) => entry.provider === selection); } } if (hasPreferredProvider && params.preferredProvider) { const filtered = next.filter((entry) => matchesPreferredProvider?.(entry.provider)); if (filtered.length > 0) { next = filtered; } } return next; } async function resolveProviderPluginSetupOptions(params: { cfg: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }): Promise { const runtime = await loadResolvedModelPickerRuntime(); const providerModelPickerOptions = "resolveProviderModelPickerContributions" in runtime && typeof runtime.resolveProviderModelPickerContributions === "function" ? runtime .resolveProviderModelPickerContributions({ config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, }) .map((contribution) => contribution.option) : runtime.resolveProviderModelPickerEntries({ config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, }); return providerModelPickerOptions.map((entry) => Object.assign( { value: entry.value, label: entry.label }, entry.hint ? { hint: entry.hint } : {}, ), ); } async function maybeHandleProviderPluginSelection(params: { selection: string; cfg: OpenClawConfig; prompter: WizardPrompter; agentDir?: string; workspaceDir?: string; env?: NodeJS.ProcessEnv; runtime?: RuntimeEnv; }): Promise { let pluginResolution: string | null = null; let pluginProviders: ProviderPlugin[] = []; if (params.selection.startsWith("provider-plugin:")) { pluginResolution = params.selection; } else if (!params.selection.includes("/")) { const { resolvePluginProviders } = await loadResolvedModelPickerRuntime(); pluginProviders = resolvePluginProviders({ config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, mode: "setup", }); pluginResolution = pluginProviders.some( (provider) => normalizeProviderId(provider.id) === normalizeProviderId(params.selection), ) ? params.selection : null; } if (!pluginResolution) { return null; } if (!params.agentDir || !params.runtime) { await params.prompter.note( "Provider setup requires agent and runtime context.", "Provider setup unavailable", ); return {}; } const { resolvePluginProviders, resolveProviderPluginChoice, runProviderModelSelectedHook, runProviderPluginAuthMethod, } = await loadResolvedModelPickerRuntime(); if (pluginProviders.length === 0) { pluginProviders = resolvePluginProviders({ config: params.cfg, workspaceDir: params.workspaceDir, env: params.env, mode: "setup", }); } const resolved = resolveProviderPluginChoice({ providers: pluginProviders, choice: pluginResolution, }); if (!resolved) { return {}; } const applied = await runProviderPluginAuthMethod({ config: params.cfg, runtime: params.runtime, prompter: params.prompter, method: resolved.method, agentDir: params.agentDir, workspaceDir: params.workspaceDir, }); if (applied.defaultModel) { await runProviderModelSelectedHook({ config: applied.config, model: applied.defaultModel, prompter: params.prompter, agentDir: params.agentDir, workspaceDir: params.workspaceDir, env: params.env, }); } return { model: applied.defaultModel, config: applied.config }; } export async function promptDefaultModel( params: PromptDefaultModelParams, ): Promise { const cfg = params.config; const allowKeep = params.allowKeep ?? true; const includeManual = params.includeManual ?? true; const includeProviderPluginSetups = params.includeProviderPluginSetups ?? false; const loadCatalog = params.loadCatalog ?? true; const browseCatalogOnDemand = params.browseCatalogOnDemand ?? false; const ignoreAllowlist = params.ignoreAllowlist ?? false; const preferredProviderRaw = normalizeOptionalString(params.preferredProvider); const preferredProvider = preferredProviderRaw ? normalizeProviderId(preferredProviderRaw) : undefined; const configuredRaw = resolveConfiguredModelRaw(cfg); const useStaticModelNormalization = !loadCatalog || browseCatalogOnDemand; const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, allowPluginNormalization: useStaticModelNormalization ? false : undefined, }); const resolvedKey = modelKey(resolved.provider, resolved.model); const configuredKey = configuredRaw ? resolvedKey : ""; let literalPrefixProvidersCache: Set | undefined; const resolveCachedLiteralPrefixProviders = async () => { if (!literalPrefixProvidersCache) { literalPrefixProvidersCache = await resolveLiteralPrefixProviderIds({ cfg, workspaceDir: params.workspaceDir, env: params.env, }); } return literalPrefixProvidersCache; }; const resolveConfiguredDisplayLabel = async () => { const providerId = normalizeProviderId(resolved.provider); if (!providerId) { return configuredRaw || resolvedKey; } const literalPrefixProviders = await resolveCachedLiteralPrefixProviders(); return literalPrefixProviders.has(providerId) ? formatLiteralProviderPrefixedModelRef(resolved.provider, resolvedKey) : configuredRaw || resolvedKey; }; if ( loadCatalog && browseCatalogOnDemand && preferredProvider && allowKeep && normalizeProviderId(resolved.provider) === preferredProvider ) { const configuredLabel = await resolveConfiguredDisplayLabel(); const options: WizardSelectOption[] = [ { value: KEEP_VALUE, label: configuredRaw ? `Keep current (${configuredLabel})` : `Keep current (default: ${resolvedKey})`, hint: configuredRaw && configuredRaw !== resolvedKey ? `resolves to ${resolvedKey}` : undefined, }, ]; if (includeManual) { options.push({ value: MANUAL_VALUE, label: "Enter model manually" }); } options.push({ value: BROWSE_VALUE, label: "Browse all models", hint: "loads provider catalogs", }); const selection = await params.prompter.select({ message: params.message ?? "Default model", options, initialValue: KEEP_VALUE, searchable: false, }); if (selection === KEEP_VALUE) { return {}; } if (selection === MANUAL_VALUE) { return promptManualModel({ prompter: params.prompter, allowBlank: false, initialValue: configuredRaw || resolvedKey || undefined, }); } if (selection !== BROWSE_VALUE) { return { model: selection }; } } if (!loadCatalog) { const configuredLabel = await resolveConfiguredDisplayLabel(); const options: WizardSelectOption[] = []; if (allowKeep) { options.push({ value: KEEP_VALUE, label: configuredRaw ? `Keep current (${configuredLabel})` : `Keep current (default: ${resolvedKey})`, hint: configuredRaw && configuredRaw !== resolvedKey ? `resolves to ${resolvedKey}` : undefined, }); } if (includeManual) { options.push({ value: MANUAL_VALUE, label: "Enter model manually" }); } if (configuredKey && !options.some((option) => option.value === configuredKey)) { options.push({ value: configuredKey, label: configuredKey, hint: "current", }); } if (options.length === 0) { return promptManualModel({ prompter: params.prompter, allowBlank: allowKeep, initialValue: configuredRaw || resolvedKey || undefined, }); } const selection = await params.prompter.select({ message: params.message ?? "Default model", options, initialValue: allowKeep ? KEEP_VALUE : configuredKey || MANUAL_VALUE, searchable: false, }); if (selection === KEEP_VALUE) { return {}; } if (selection === MANUAL_VALUE) { return promptManualModel({ prompter: params.prompter, allowBlank: false, initialValue: configuredRaw || resolvedKey || undefined, }); } return { model: selection }; } const catalogProgress = params.prompter.progress("Loading available models"); let catalog: Awaited>; try { catalog = await loadPickerModelCatalog(cfg); } finally { catalogProgress.stop(); } if (catalog.length === 0) { return promptManualModel({ prompter: params.prompter, allowBlank: allowKeep, initialValue: configuredRaw || resolvedKey || undefined, }); } const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER, }); const models = ignoreAllowlist ? catalog : resolveVisibleModelCatalog({ cfg, catalog, defaultProvider: DEFAULT_PROVIDER, defaultModel: resolved.model, agentDir: params.agentDir, workspaceDir: params.workspaceDir, env: params.env, }); if (models.length === 0) { return promptManualModel({ prompter: params.prompter, allowBlank: allowKeep, initialValue: configuredRaw || resolvedKey || undefined, }); } const filteredModels = await maybeFilterModelsByProvider({ models, preferredProvider, prompter: params.prompter, cfg, workspaceDir: params.workspaceDir, env: params.env, }); if (filteredModels.length === 0) { return promptManualModel({ prompter: params.prompter, allowBlank: allowKeep, initialValue: configuredRaw || resolvedKey || undefined, }); } const matchesPreferredProvider = preferredProvider ? createPreferredProviderMatcher({ preferredProvider, cfg, workspaceDir: params.workspaceDir, env: params.env, }) : undefined; const hasPreferredProvider = preferredProvider ? filteredModels.some((entry) => matchesPreferredProvider?.(entry.provider)) : false; const hasAuth = createProviderAuthChecker({ cfg, workspaceDir: params.workspaceDir, agentDir: params.agentDir, env: params.env, }); const literalPrefixProviders = await resolveCachedLiteralPrefixProviders(); // Show the literal form (e.g. nvidia/nvidia/...) in the "Keep current" label // for providers that set preserveLiteralProviderPrefix, so the user sees the // same ref they'll pick from the catalog rows. Config itself stays canonical. const configuredLabel = literalPrefixProviders.has(normalizeProviderId(resolved.provider)) ? formatLiteralProviderPrefixedModelRef(resolved.provider, resolvedKey) : configuredRaw || resolvedKey; const options: WizardSelectOption[] = []; if (allowKeep) { options.push({ value: KEEP_VALUE, label: configuredRaw ? `Keep current (${configuredLabel})` : `Keep current (default: ${resolvedKey})`, }); } if (includeManual) { options.push({ value: MANUAL_VALUE, label: "Enter model manually" }); } if (includeProviderPluginSetups && params.agentDir) { options.push( ...(await resolveProviderPluginSetupOptions({ cfg, workspaceDir: params.workspaceDir, env: params.env, })), ); } const seen = new Set(); for (const entry of filteredModels) { addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth, literalPrefixProviders, }); } if (configuredKey && !seen.has(configuredKey)) { options.push({ value: configuredKey, label: configuredLabel, hint: "current (not in catalog)", }); } let initialValue: string | undefined = allowKeep ? KEEP_VALUE : configuredKey || undefined; if ( allowKeep && hasPreferredProvider && preferredProvider && !matchesPreferredProvider?.(resolved.provider) ) { const firstModel = filteredModels[0]; if (firstModel) { initialValue = modelKey(firstModel.provider, firstModel.id); } } const selection = await params.prompter.select({ message: params.message ?? "Default model", options, initialValue, searchable: true, }); const selectedValue = selection ?? ""; if (selectedValue === KEEP_VALUE) { return {}; } if (selectedValue === MANUAL_VALUE) { return promptManualModel({ prompter: params.prompter, allowBlank: false, initialValue: configuredRaw || resolvedKey || undefined, }); } const providerPluginResult = await maybeHandleProviderPluginSelection({ selection: selectedValue, cfg, prompter: params.prompter, agentDir: params.agentDir, workspaceDir: params.workspaceDir, env: params.env, runtime: params.runtime, }); if (providerPluginResult) { return providerPluginResult; } const model = normalizeAgentModelRefForConfig(selectedValue); const { runProviderModelSelectedHook } = await loadResolvedModelPickerRuntime(); await runProviderModelSelectedHook({ config: cfg, model, prompter: params.prompter, agentDir: params.agentDir, workspaceDir: params.workspaceDir, env: params.env, }); return { model }; } export async function promptModelAllowlist(params: { config: OpenClawConfig; prompter: WizardPrompter; message?: string; agentDir?: string; workspaceDir?: string; env?: NodeJS.ProcessEnv; allowedKeys?: string[]; initialSelections?: string[]; preferredProvider?: string; loadCatalog?: boolean; }): Promise { const cfg = params.config; const existingKeys = resolveConfiguredModelKeys(cfg); const configuredRaw = resolveConfiguredModelRaw(cfg); const allowedKeys = normalizeModelKeys(params.allowedKeys ?? []); const allowedKeySet = allowedKeys.length > 0 ? new Set(allowedKeys) : null; const preferredProviderRaw = normalizeOptionalString(params.preferredProvider); const preferredProvider = preferredProviderRaw ? normalizeProviderId(preferredProviderRaw) : undefined; const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const resolvedKey = modelKey(resolved.provider, resolved.model); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER, }); const fallbackAliasIndex = resolved.provider === DEFAULT_PROVIDER ? aliasIndex : buildModelAliasIndex({ cfg, defaultProvider: resolved.provider, }); const fallbackKeys = resolveFallbackModelKeys({ cfg, rawFallbacks: resolveAgentModelFallbackValues(cfg.agents?.defaults?.model), defaultProvider: resolved.provider, aliasIndex: fallbackAliasIndex, }); const initialSeeds = normalizeModelKeys([ ...existingKeys, resolvedKey, ...fallbackKeys, ...(params.initialSelections ?? []), ]); const hasRealSeed = existingKeys.length > 0 || fallbackKeys.length > 0 || (params.initialSelections?.length ?? 0) > 0 || configuredRaw.length > 0; const hasAuth = createProviderAuthChecker({ cfg, workspaceDir: params.workspaceDir, agentDir: params.agentDir, env: params.env, }); const matchesPreferredProvider = preferredProvider ? createPreferredProviderMatcher({ preferredProvider, cfg, }) : undefined; const loadCatalog = params.loadCatalog ?? true; const scopedFastKeys = allowedKeys.length > 0 ? allowedKeys : !loadCatalog && preferredProvider && hasRealSeed ? initialSeeds.filter((key) => { const entry = splitModelKey(key); return entry ? matchesPreferredProvider?.(entry.provider) === true : false; }) : []; if (scopedFastKeys.length > 0) { const scopeKeys = allowedKeys.length > 0 ? allowedKeys : scopedFastKeys; const scopeKeySet = new Set(scopeKeys); const initialKeys = normalizeModelKeys(initialSeeds.filter((key) => scopeKeySet.has(key))); const options: WizardSelectOption[] = []; const seen = new Set(); for (const key of scopeKeys) { addModelKeySelectOption({ key, options, seen, aliasIndex, hasAuth, fallbackHint: allowedKeys.length > 0 ? "allowed" : "configured", }); } if (options.length === 0) { return {}; } const selection = await params.prompter.multiselect({ message: params.message ?? "Models in /model picker (multi-select)", options, initialValues: initialKeys.length > 0 ? initialKeys : undefined, searchable: true, }); const selected = normalizeModelKeys(selection); if (selected.length > 0) { return { models: selected, scopeKeys }; } const confirmScopedClear = await params.prompter.confirm({ message: "Remove these provider models from the /model picker?", initialValue: false, }); if (!confirmScopedClear) { return {}; } return { models: [], scopeKeys }; } if (!loadCatalog) { return {}; } const allowlistProgress = params.prompter.progress("Loading available models"); let catalog: Awaited>; try { catalog = await loadPickerModelCatalog(cfg, { preferredProvider }); } finally { allowlistProgress.stop(); } if (preferredProvider) { const configuredCatalog = buildConfiguredModelCatalog({ cfg }).filter( (entry) => matchesPreferredProvider?.(entry.provider) === true, ); const configuredKeys = new Set( configuredCatalog.map((entry) => modelKey(entry.provider, entry.id)), ); catalog = [ ...configuredCatalog, ...catalog.filter((entry) => !configuredKeys.has(modelKey(entry.provider, entry.id))), ]; } if (catalog.length === 0 && allowedKeys.length === 0) { const noCatalogInitialKeys = existingKeys.length > 0 ? normalizeModelKeys([...existingKeys, ...fallbackKeys]) : []; const raw = await params.prompter.text({ message: params.message ?? "Allowlist models (comma-separated provider/model; blank to keep current)", initialValue: noCatalogInitialKeys.join(", "), placeholder: "provider/model, other-provider/model", }); const parsed = (raw ?? "") .split(",") .map((value) => value.trim()) .filter((value) => value.length > 0); if (parsed.length === 0) { return {}; } return { models: normalizeModelKeys(parsed) }; } const literalPrefixProviders = await resolveLiteralPrefixProviderIds({ cfg, workspaceDir: params.workspaceDir, env: params.env, }); const options: WizardSelectOption[] = []; const seen = new Set(); const allowedCatalog = ( allowedKeySet ? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id))) : catalog ).filter((entry) => isModelPickerVisibleProvider(entry.provider)); const filteredCatalog = preferredProvider && allowedCatalog.some((entry) => matchesPreferredProvider?.(entry.provider)) ? allowedCatalog.filter((entry) => matchesPreferredProvider?.(entry.provider)) : allowedCatalog; const scopeKeys = allowedKeySet ? allowedKeys : preferredProvider ? filteredCatalog.map((entry) => modelKey(entry.provider, entry.id)) : undefined; const scopeKeySet = scopeKeys ? new Set(scopeKeys) : null; const selectableInitialSeeds = scopeKeySet && !allowedKeySet ? initialSeeds.filter((key) => scopeKeySet.has(key)) : initialSeeds; const initialKeys = allowedKeySet ? initialSeeds.filter((key) => allowedKeySet.has(key)) : selectableInitialSeeds.filter(isModelPickerVisibleModelRef); for (const entry of filteredCatalog) { addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth, literalPrefixProviders, }); } const supplementalKeys = (allowedKeySet ? allowedKeys : selectableInitialSeeds).filter( isModelPickerVisibleModelRef, ); for (const key of supplementalKeys) { if (seen.has(key)) { continue; } options.push({ value: key, label: key, hint: allowedKeySet ? "allowed (not in catalog)" : "configured (not in catalog)", }); seen.add(key); } if (options.length === 0) { return {}; } const selection = await params.prompter.multiselect({ message: params.message ?? "Models in /model picker (multi-select)", options, initialValues: initialKeys.length > 0 ? initialKeys : undefined, searchable: true, }); const selected = normalizeModelKeys(selection); if (selected.length > 0) { return { models: selected, ...(scopeKeys ? { scopeKeys } : {}) }; } if (scopeKeys) { const confirmScopedClear = await params.prompter.confirm({ message: "Remove these provider models from the /model picker?", initialValue: false, }); if (!confirmScopedClear) { return {}; } return { models: [], scopeKeys }; } if (existingKeys.length === 0) { return { models: [] }; } const confirmClear = await params.prompter.confirm({ message: "Clear the model allowlist? (shows all models)", initialValue: false, }); if (!confirmClear) { return {}; } return { models: [] }; } export function applyModelAllowlist( cfg: OpenClawConfig, models: string[], opts: { scopeKeys?: string[] } = {}, ): OpenClawConfig { const defaults = cfg.agents?.defaults; const normalized = normalizeModelKeys(models); const scopeKeys = opts.scopeKeys ? normalizeModelKeys(opts.scopeKeys) : []; const scopeKeySet = scopeKeys.length > 0 ? new Set(scopeKeys) : null; if (normalized.length === 0) { if (!defaults?.models) { return cfg; } if (scopeKeySet) { const nextModels = { ...defaults.models }; for (const key of scopeKeySet) { delete nextModels[key]; } const { models: _ignored, ...restDefaults } = defaults; return { ...cfg, agents: { ...cfg.agents, defaults: Object.keys(nextModels).length > 0 ? { ...defaults, models: nextModels } : restDefaults, }, }; } const { models: _ignored, ...restDefaults } = defaults; return { ...cfg, agents: { ...cfg.agents, defaults: restDefaults, }, }; } const existingModels = defaults?.models ?? {}; if (scopeKeySet) { const nextModels = { ...existingModels }; for (const key of scopeKeySet) { delete nextModels[key]; } for (const key of normalized) { nextModels[key] = existingModels[key] ?? {}; } return { ...cfg, agents: { ...cfg.agents, defaults: { ...defaults, models: nextModels, }, }, }; } const nextModels: Record = {}; for (const key of normalized) { nextModels[key] = existingModels[key] ?? {}; } return { ...cfg, agents: { ...cfg.agents, defaults: { ...defaults, models: nextModels, }, }, }; } export function applyModelFallbacksFromSelection( cfg: OpenClawConfig, selection: string[], opts: { scopeKeys?: string[] } = {}, ): OpenClawConfig { const normalized = normalizeModelKeys(selection); const scopeKeys = opts.scopeKeys ? normalizeModelKeys(opts.scopeKeys) : []; const scopeKeySet = scopeKeys.length > 0 ? new Set(scopeKeys) : null; if (normalized.length === 0 && !scopeKeySet) { return cfg; } const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const resolvedKey = modelKey(resolved.provider, resolved.model); const includesResolvedPrimary = normalized.includes(resolvedKey); if (!includesResolvedPrimary && !scopeKeySet) { return cfg; } const defaults = cfg.agents?.defaults; const existingModel = defaults?.model; const existingPrimary = typeof existingModel === "string" ? existingModel : existingModel && typeof existingModel === "object" ? existingModel.primary : undefined; const normalizedExistingPrimary = existingPrimary != null ? normalizeAgentModelRefForConfig(existingPrimary) : undefined; const preservedModelFields = existingModel && typeof existingModel === "object" ? (({ fallbacks: _oldFallbacks, ...rest }) => rest)(existingModel) : {}; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: resolved.provider, }); const existingFallbacks = existingModel && typeof existingModel === "object" && Array.isArray(existingModel.fallbacks) ? resolveFallbackModelKeys({ cfg, rawFallbacks: existingModel.fallbacks, defaultProvider: resolved.provider, aliasIndex, }) : []; const existingFallbackSet = new Set(existingFallbacks); const rawSelectedFallbacks = normalized.filter((key) => key !== resolvedKey); const selectedFallbacks = scopeKeySet && !includesResolvedPrimary ? rawSelectedFallbacks.filter((key) => existingFallbackSet.has(key)) : rawSelectedFallbacks; const preserveExistingFallback = scopeKeySet ? (fallback: string) => !scopeKeySet.has(fallback) : (fallback: string) => !isModelPickerVisibleModelRef(fallback); const fallbacks = mergeFallbackSelection({ existingFallbacks, selectedFallbacks, preserveExistingFallback, }); const nextModel = { ...preservedModelFields, ...(normalizedExistingPrimary != null ? { primary: normalizedExistingPrimary } : {}), ...(fallbacks.length > 0 ? { fallbacks } : {}), }; if (Object.keys(nextModel).length === 0) { if (!defaults || !Object.hasOwn(defaults, "model")) { return cfg; } const { model: _ignoredModel, ...restDefaults } = defaults; return { ...cfg, agents: { ...cfg.agents, defaults: restDefaults, }, }; } return { ...cfg, agents: { ...cfg.agents, defaults: { ...defaults, model: nextModel, }, }, }; } function mergeFallbackSelection(params: { existingFallbacks: string[]; selectedFallbacks: string[]; preserveExistingFallback: (fallback: string) => boolean; }): string[] { const selected = new Set(params.selectedFallbacks); const fallbacks: string[] = []; for (const fallback of params.existingFallbacks) { if (params.preserveExistingFallback(fallback)) { fallbacks.push(fallback); continue; } if (selected.delete(fallback)) { fallbacks.push(fallback); } } for (const fallback of params.selectedFallbacks) { if (selected.has(fallback)) { fallbacks.push(fallback); } } return fallbacks; }