import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { hasUsableCustomProviderApiKey, resolveEnvApiKey } from "../agents/model-auth.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { buildAllowedModelSet, buildModelAliasIndex, modelKey, normalizeProviderId, resolveConfiguredModelRef, } from "../agents/model-selection.js"; import { formatTokenK } from "../commands/models/shared.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import { applyPrimaryModel } from "../plugins/provider-model-primary.js"; import type { ProviderPlugin } from "../plugins/types.js"; import type { RuntimeEnv } from "../runtime.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.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 PROVIDER_FILTER_THRESHOLD = 30; // 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; 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[] }; async function loadModelPickerRuntime() { return import("../commands/model-picker.runtime.js"); } const loadResolvedModelPickerRuntime = createLazyRuntimeSurface( loadModelPickerRuntime, ({ modelPickerRuntime }) => modelPickerRuntime, ); function hasAuthForProvider( provider: string, cfg: OpenClawConfig, store: ReturnType, ) { if (listProfilesForProvider(store, provider).length > 0) { return true; } if (resolveEnvApiKey(provider)) { return true; } if (hasUsableCustomProviderApiKey(cfg, provider)) { return true; } return false; } function createProviderAuthChecker(params: { cfg: OpenClawConfig; agentDir?: string; }): (provider: string) => boolean { const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); const authCache = new Map(); return (provider: string) => { const cached = authCache.get(provider); if (cached !== undefined) { return cached; } const value = hasAuthForProvider(provider, params.cfg, authStore); authCache.set(provider, value); return value; }; } 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) => String(key ?? "").trim()) .filter((key) => key.length > 0); } function normalizeModelKeys(values: string[]): string[] { const seen = new Set(); const next: string[] = []; for (const raw of values) { const value = String(raw ?? "").trim(); if (!value || seen.has(value)) { continue; } seen.add(value); next.push(value); } return next; } function addModelSelectOption(params: { entry: { provider: string; id: string; name?: string; contextWindow?: number; reasoning?: boolean; }; options: WizardSelectOption[]; seen: Set; aliasIndex: ReturnType; hasAuth: (provider: string) => boolean; }) { const key = modelKey(params.entry.provider, params.entry.id); if (params.seen.has(key) || HIDDEN_ROUTER_MODELS.has(key)) { 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(", ")}`); } if (!params.hasAuth(params.entry.provider)) { hints.push("auth missing"); } params.options.push({ value: key, label: key, hint: hints.length > 0 ? hints.join(" ยท ") : undefined, }); params.seen.add(key); } function matchesPreferredProvider(entryProvider: string, preferredProvider: string): boolean { if (preferredProvider === "volcengine") { return entryProvider === "volcengine" || entryProvider === "volcengine-plan"; } if (preferredProvider === "byteplus") { return entryProvider === "byteplus" || entryProvider === "byteplus-plan"; } return entryProvider === preferredProvider; } 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) => (value?.trim() ? undefined : "Required"), }); const model = String(modelInput ?? "").trim(); if (!model) { return {}; } return { 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; }): Promise { const providerIds = Array.from(new Set(params.models.map((entry) => entry.provider))).toSorted( (a, b) => a.localeCompare(b), ); const hasPreferredProvider = params.preferredProvider ? providerIds.includes(params.preferredProvider) : false; const shouldPromptProvider = !hasPreferredProvider && providerIds.length > 1 && params.models.length > PROVIDER_FILTER_THRESHOLD; let next = params.models; if (shouldPromptProvider) { const selection = await params.prompter.select({ message: "Filter models by provider", options: [{ value: "*", label: "All providers" }, ...buildModelProviderFilterOptions(next)], }); if (selection !== "*") { next = next.filter((entry) => entry.provider === selection); } } if (hasPreferredProvider && params.preferredProvider) { next = next.filter((entry) => matchesPreferredProvider(entry.provider, params.preferredProvider!), ); } 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) => ({ 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, bundledProviderAllowlistCompat: true, bundledProviderVitestCompat: true, }); 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, bundledProviderAllowlistCompat: true, bundledProviderVitestCompat: true, }); } 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 ignoreAllowlist = params.ignoreAllowlist ?? false; const preferredProvider = params.preferredProvider?.trim() ? normalizeProviderId(params.preferredProvider) : undefined; const configuredRaw = resolveConfiguredModelRaw(cfg); const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const resolvedKey = modelKey(resolved.provider, resolved.model); const configuredKey = configuredRaw ? resolvedKey : ""; const catalog = await loadModelCatalog({ config: cfg, useCache: false }); 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 : (() => { const { allowedCatalog } = buildAllowedModelSet({ cfg, catalog, defaultProvider: DEFAULT_PROVIDER, }); return allowedCatalog.length > 0 ? allowedCatalog : catalog; })(); if (models.length === 0) { return promptManualModel({ prompter: params.prompter, allowBlank: allowKeep, initialValue: configuredRaw || resolvedKey || undefined, }); } const filteredModels = await maybeFilterModelsByProvider({ models, preferredProvider, prompter: params.prompter, }); const hasPreferredProvider = preferredProvider ? filteredModels.some((entry) => matchesPreferredProvider(entry.provider, preferredProvider)) : false; const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir }); const options: WizardSelectOption[] = []; if (allowKeep) { options.push({ value: KEEP_VALUE, label: configuredRaw ? `Keep current (${configuredRaw})` : `Keep current (default: ${resolvedKey})`, hint: configuredRaw && configuredRaw !== resolvedKey ? `resolves to ${resolvedKey}` : undefined, }); } 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 }); } if (configuredKey && !seen.has(configuredKey)) { options.push({ value: configuredKey, label: configuredKey, hint: "current (not in catalog)", }); } let initialValue: string | undefined = allowKeep ? KEEP_VALUE : configuredKey || undefined; if ( allowKeep && hasPreferredProvider && preferredProvider && resolved.provider !== preferredProvider ) { 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, }); if (selection === KEEP_VALUE) { return {}; } if (selection === MANUAL_VALUE) { return promptManualModel({ prompter: params.prompter, allowBlank: false, initialValue: configuredRaw || resolvedKey || undefined, }); } const providerPluginResult = await maybeHandleProviderPluginSelection({ selection: String(selection), cfg, prompter: params.prompter, agentDir: params.agentDir, workspaceDir: params.workspaceDir, env: params.env, runtime: params.runtime, }); if (providerPluginResult) { return providerPluginResult; } const model = String(selection); 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; allowedKeys?: string[]; initialSelections?: string[]; preferredProvider?: string; }): Promise { const cfg = params.config; const existingKeys = resolveConfiguredModelKeys(cfg); const allowedKeys = normalizeModelKeys(params.allowedKeys ?? []); const allowedKeySet = allowedKeys.length > 0 ? new Set(allowedKeys) : null; const preferredProvider = params.preferredProvider?.trim() ? normalizeProviderId(params.preferredProvider) : undefined; const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const resolvedKey = modelKey(resolved.provider, resolved.model); const initialSeeds = normalizeModelKeys([ ...existingKeys, resolvedKey, ...(params.initialSelections ?? []), ]); const initialKeys = allowedKeySet ? initialSeeds.filter((key) => allowedKeySet.has(key)) : initialSeeds; const catalog = await loadModelCatalog({ config: cfg, useCache: false }); if (catalog.length === 0 && allowedKeys.length === 0) { const raw = await params.prompter.text({ message: params.message ?? "Allowlist models (comma-separated provider/model; blank to keep current)", initialValue: existingKeys.join(", "), placeholder: "provider/model, other-provider/model", }); const parsed = String(raw ?? "") .split(",") .map((value) => value.trim()) .filter((value) => value.length > 0); if (parsed.length === 0) { return {}; } return { models: normalizeModelKeys(parsed) }; } const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER, }); const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir }); const options: WizardSelectOption[] = []; const seen = new Set(); const allowedCatalog = allowedKeySet ? catalog.filter((entry) => allowedKeySet.has(modelKey(entry.provider, entry.id))) : catalog; const filteredCatalog = preferredProvider && allowedCatalog.some((entry) => matchesPreferredProvider(entry.provider, preferredProvider)) ? allowedCatalog.filter((entry) => matchesPreferredProvider(entry.provider, preferredProvider), ) : allowedCatalog; for (const entry of filteredCatalog) { addModelSelectOption({ entry, options, seen, aliasIndex, hasAuth }); } const supplementalKeys = allowedKeySet ? allowedKeys : existingKeys; 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.map((value) => String(value))); if (selected.length > 0) { return { models: selected }; } 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[]): OpenClawConfig { const defaults = cfg.agents?.defaults; const normalized = normalizeModelKeys(models); if (normalized.length === 0) { if (!defaults?.models) { return cfg; } const { models: _ignored, ...restDefaults } = defaults; return { ...cfg, agents: { ...cfg.agents, defaults: restDefaults, }, }; } const existingModels = defaults?.models ?? {}; 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[], ): OpenClawConfig { const normalized = normalizeModelKeys(selection); if (normalized.length <= 1) { return cfg; } const resolved = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL, }); const resolvedKey = modelKey(resolved.provider, resolved.model); if (!normalized.includes(resolvedKey)) { 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 fallbacks = normalized.filter((key) => key !== resolvedKey); return { ...cfg, agents: { ...cfg.agents, defaults: { ...defaults, model: { ...(typeof existingModel === "object" ? existingModel : undefined), primary: existingPrimary ?? resolvedKey, fallbacks, }, }, }, }; }