import "./pi-embedded-helpers-s_r2sMSQ.js"; import { L as describeFailoverError, Q as loadProviderUsageSummary, et as formatUsageWindowSummary, gr as loadModelCatalog, gt as openUrl, l as runEmbeddedPiAgent, tt as resolveUsageProviderId } from "./reply-5K4M-ATj.js"; import { t as CONFIG_PATH } from "./paths-BDd7_JUB.js"; import { a as resolveAgentModelPrimary, c as resolveDefaultAgentId, i as resolveAgentModelFallbacksOverride, r as resolveAgentDir, s as resolveAgentWorkspaceDir, w as resolveDefaultAgentWorkspaceDir } from "./agent-scope-CrgUOY3f.js"; import { I as colorize, L as isRich$1, R as theme, c as defaultRuntime } from "./subsystem-46MXi6Ip.js"; import { _ as shortenHomePath } from "./utils-Dg0Xbl6w.js"; import "./exec-CTo4hK94.js"; import { A as getCustomProviderApiKey, G as resolveAuthProfileOrder, I as getShellEnvAppliedKeys, M as resolveApiKeyForProvider, N as resolveAwsSdkEnvVarName, P as resolveEnvApiKey, V as shouldEnableShellEnvFallback, X as resolveProfileUnusableUntilForDisplay, a as modelKey, at as listProfilesForProvider, bt as DEFAULT_MODEL, ct as upsertAuthProfile, f as resolveModelRefFromString, gt as resolveAuthProfileDisplayLabel, ht as resolveOpenClawAgentDir, l as resolveConfiguredModelRef, lt as ensureAuthProfileStore, mt as resolveAuthStorePathForDisplay, n as buildModelAliasIndex, o as normalizeProviderId, s as parseModelRef, st as setAuthProfileOrder, u as resolveDefaultModelForAgent, xt as DEFAULT_PROVIDER } from "./model-selection-Cp1maz7B.js"; import "./github-copilot-token-VZsS4013.js"; import { t as formatCliCommand } from "./command-format-BQK1OIvH.js"; import "./boolean-CE7i9tBR.js"; import "./env-DOcCob95.js"; import { a as readConfigFileSnapshot, h as parseDurationMs, r as loadConfig } from "./config-BAN8QfW5.js"; import "./manifest-registry-BFpLJJDB.js"; import "./plugins-D1CxUobm.js"; import "./sandbox-CRRd7UQ-.js"; import { g as ensureOpenClawModelsJson } from "./image-Bx6WKexe.js"; import { i as discoverModels, r as discoverAuthStorage } from "./pi-model-discovery-DjGamP_B.js"; import "./chrome-D2LUApAY.js"; import "./skills-C4b1FA1e.js"; import "./routes-CB4ih7ZH.js"; import "./server-context-BjC2Yruc.js"; import "./message-channel-BD3kuDks.js"; import "./logging-BdnOSVPD.js"; import "./accounts-BlHoTziG.js"; import { a as resolveSessionTranscriptsDirForAgent, r as resolveSessionTranscriptPath } from "./paths-50eo6DV6.js"; import "./redact-BKh-zp-c.js"; import "./tool-display-rIUh61kT.js"; import "./deliver-Do5KTPQ4.js"; import "./dispatcher-DLhjSne5.js"; import "./manager-BArueSTR.js"; import "./sqlite-Dz6S6ijV.js"; import "./channel-summary-DvcMwADq.js"; import "./client-i9GLCujO.js"; import "./call-BxBOsbXG.js"; import "./login-qr-f0O41dc6.js"; import "./pairing-store-BnMngoWQ.js"; import { t as formatDocsLink } from "./links-C9fyAH-V.js"; import { n as resolveOptionFromCommand, r as runCommandWithRuntime } from "./cli-utils-CukoNm8O.js"; import { r as withProgressTotals } from "./progress-uNDQDtGB.js"; import "./pi-tools.policy-B0NP2v9o.js"; import { n as stylePromptMessage, r as stylePromptTitle, t as stylePromptHint } from "./prompt-style-gfROyHgB.js"; import "./pairing-labels-DK2aLSd2.js"; import "./control-service-yGq0G92l.js"; import "./restart-sentinel-CdcBcziq.js"; import "./channel-selection-Cmg7RpCn.js"; import { n as logConfigUpdated } from "./logging-CLCWl7Iu.js"; import "./note-BFpD-42H.js"; import { t as createClackPrompter } from "./clack-prompter-OmDa1Hm1.js"; import { a as normalizeAlias, at as validateAnthropicSetupToken, c as updateConfig, d as resolvePluginProviders, i as formatTokenK, l as createVpsAwareOAuthHandlers, n as ensureFlagCompatibility, o as resolveKnownAgentId, r as formatMs, s as resolveModelTarget, t as githubCopilotLoginCommand, u as isRemoteEnvironment, v as applyAuthProfileConfig } from "./github-copilot-auth-DujNgq_8.js"; import { t as renderTable } from "./table-DuNe7Qes.js"; import { i as redactSecrets } from "./format-hyFOmVZc.js"; import { n as buildAuthHealthSummary, r as formatRemainingShort, t as DEFAULT_OAUTH_WARN_MS } from "./auth-health-Ba9GTScq.js"; import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; import { complete, getEnvApiKey, getModel } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts"; //#region src/commands/models/aliases.ts async function modelsAliasesListCommand(opts, runtime) { ensureFlagCompatibility(opts); const models = loadConfig().agents?.defaults?.models ?? {}; const aliases = Object.entries(models).reduce((acc, [modelKey, entry]) => { const alias = entry?.alias?.trim(); if (alias) acc[alias] = modelKey; return acc; }, {}); if (opts.json) { runtime.log(JSON.stringify({ aliases }, null, 2)); return; } if (opts.plain) { for (const [alias, target] of Object.entries(aliases)) runtime.log(`${alias} ${target}`); return; } runtime.log(`Aliases (${Object.keys(aliases).length}):`); if (Object.keys(aliases).length === 0) { runtime.log("- none"); return; } for (const [alias, target] of Object.entries(aliases)) runtime.log(`- ${alias} -> ${target}`); } async function modelsAliasesAddCommand(aliasRaw, modelRaw, runtime) { const alias = normalizeAlias(aliasRaw); const resolved = resolveModelTarget({ raw: modelRaw, cfg: loadConfig() }); await updateConfig((cfg) => { const modelKey = `${resolved.provider}/${resolved.model}`; const nextModels = { ...cfg.agents?.defaults?.models }; for (const [key, entry] of Object.entries(nextModels)) { const existing = entry?.alias?.trim(); if (existing && existing === alias && key !== modelKey) throw new Error(`Alias ${alias} already points to ${key}.`); } nextModels[modelKey] = { ...nextModels[modelKey] ?? {}, alias }; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, models: nextModels } } }; }); logConfigUpdated(runtime); runtime.log(`Alias ${alias} -> ${resolved.provider}/${resolved.model}`); } async function modelsAliasesRemoveCommand(aliasRaw, runtime) { const alias = normalizeAlias(aliasRaw); const updated = await updateConfig((cfg) => { const nextModels = { ...cfg.agents?.defaults?.models }; let found = false; for (const [key, entry] of Object.entries(nextModels)) if (entry?.alias?.trim() === alias) { nextModels[key] = { ...entry, alias: void 0 }; found = true; break; } if (!found) throw new Error(`Alias not found: ${alias}`); return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, models: nextModels } } }; }); logConfigUpdated(runtime); if (!updated.agents?.defaults?.models || Object.values(updated.agents.defaults.models).every((entry) => !entry?.alias?.trim())) runtime.log("No aliases configured."); } //#endregion //#region src/commands/models/auth.ts const confirm$1 = (params) => confirm({ ...params, message: stylePromptMessage(params.message) }); const text$1 = (params) => text({ ...params, message: stylePromptMessage(params.message) }); const select$1 = (params) => select({ ...params, message: stylePromptMessage(params.message), options: params.options.map((opt) => opt.hint === void 0 ? opt : { ...opt, hint: stylePromptHint(opt.hint) }) }); function resolveTokenProvider(raw) { const trimmed = raw?.trim(); if (!trimmed) return null; if (normalizeProviderId(trimmed) === "anthropic") return "anthropic"; return "custom"; } function resolveDefaultTokenProfileId(provider) { return `${normalizeProviderId(provider)}:manual`; } async function modelsAuthSetupTokenCommand(opts, runtime) { const provider = resolveTokenProvider(opts.provider ?? "anthropic"); if (provider !== "anthropic") throw new Error("Only --provider anthropic is supported for setup-token."); if (!process.stdin.isTTY) throw new Error("setup-token requires an interactive TTY."); if (!opts.yes) { if (!await confirm$1({ message: "Have you run `claude setup-token` and copied the token?", initialValue: true })) return; } const tokenInput = await text$1({ message: "Paste Anthropic setup-token", validate: (value) => validateAnthropicSetupToken(String(value ?? "")) }); const token = String(tokenInput).trim(); const profileId = resolveDefaultTokenProfileId(provider); upsertAuthProfile({ profileId, credential: { type: "token", provider, token } }); await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" })); logConfigUpdated(runtime); runtime.log(`Auth profile: ${profileId} (${provider}/token)`); } async function modelsAuthPasteTokenCommand(opts, runtime) { const rawProvider = opts.provider?.trim(); if (!rawProvider) throw new Error("Missing --provider."); const provider = normalizeProviderId(rawProvider); const profileId = opts.profileId?.trim() || resolveDefaultTokenProfileId(provider); const tokenInput = await text$1({ message: `Paste token for ${provider}`, validate: (value) => value?.trim() ? void 0 : "Required" }); const token = String(tokenInput).trim(); const expires = opts.expiresIn?.trim() && opts.expiresIn.trim().length > 0 ? Date.now() + parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" }) : void 0; upsertAuthProfile({ profileId, credential: { type: "token", provider, token, ...expires ? { expires } : {} } }); await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" })); logConfigUpdated(runtime); runtime.log(`Auth profile: ${profileId} (${provider}/token)`); } async function modelsAuthAddCommand(_opts, runtime) { const provider = await select$1({ message: "Token provider", options: [{ value: "anthropic", label: "anthropic" }, { value: "custom", label: "custom (type provider id)" }] }); const providerId = provider === "custom" ? normalizeProviderId(String(await text$1({ message: "Provider id", validate: (value) => value?.trim() ? void 0 : "Required" }))) : provider; if (await select$1({ message: "Token method", options: [...providerId === "anthropic" ? [{ value: "setup-token", label: "setup-token (claude)", hint: "Paste a setup-token from `claude setup-token`" }] : [], { value: "paste", label: "paste token" }] }) === "setup-token") { await modelsAuthSetupTokenCommand({ provider: providerId }, runtime); return; } const profileIdDefault = resolveDefaultTokenProfileId(providerId); await modelsAuthPasteTokenCommand({ provider: providerId, profileId: String(await text$1({ message: "Profile id", initialValue: profileIdDefault, validate: (value) => value?.trim() ? void 0 : "Required" })).trim(), expiresIn: await confirm$1({ message: "Does this token expire?", initialValue: false }) ? String(await text$1({ message: "Expires in (duration)", initialValue: "365d", validate: (value) => { try { parseDurationMs(String(value ?? ""), { defaultUnit: "d" }); return; } catch { return "Invalid duration (e.g. 365d, 12h, 30m)"; } } })).trim() : void 0 }, runtime); } function resolveProviderMatch(providers, rawProvider) { const raw = rawProvider?.trim(); if (!raw) return null; const normalized = normalizeProviderId(raw); return providers.find((provider) => normalizeProviderId(provider.id) === normalized) ?? providers.find((provider) => provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false) ?? null; } function pickAuthMethod(provider, rawMethod) { const raw = rawMethod?.trim(); if (!raw) return null; const normalized = raw.toLowerCase(); return provider.auth.find((method) => method.id.toLowerCase() === normalized) ?? provider.auth.find((method) => method.label.toLowerCase() === normalized) ?? null; } function isPlainRecord(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function mergeConfigPatch(base, patch) { if (!isPlainRecord(base) || !isPlainRecord(patch)) return patch; const next = { ...base }; for (const [key, value] of Object.entries(patch)) { const existing = next[key]; if (isPlainRecord(existing) && isPlainRecord(value)) next[key] = mergeConfigPatch(existing, value); else next[key] = value; } return next; } function applyDefaultModel(cfg, model) { const models = { ...cfg.agents?.defaults?.models }; models[model] = models[model] ?? {}; const existingModel = cfg.agents?.defaults?.model; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, models, model: { ...existingModel && typeof existingModel === "object" && "fallbacks" in existingModel ? { fallbacks: existingModel.fallbacks } : void 0, primary: model } } } }; } function credentialMode(credential) { if (credential.type === "api_key") return "api_key"; if (credential.type === "token") return "token"; return "oauth"; } async function modelsAuthLoginCommand(opts, runtime) { if (!process.stdin.isTTY) throw new Error("models auth login requires an interactive TTY."); const snapshot = await readConfigFileSnapshot(); if (!snapshot.valid) { const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"); throw new Error(`Invalid config at ${snapshot.path}\n${issues}`); } const config = snapshot.config; const defaultAgentId = resolveDefaultAgentId(config); const agentDir = resolveAgentDir(config, defaultAgentId); const workspaceDir = resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir(); const providers = resolvePluginProviders({ config, workspaceDir }); if (providers.length === 0) throw new Error(`No provider plugins found. Install one via \`${formatCliCommand("openclaw plugins install")}\`.`); const prompter = createClackPrompter(); const selectedProvider = resolveProviderMatch(providers, opts.provider) ?? await prompter.select({ message: "Select a provider", options: providers.map((provider) => ({ value: provider.id, label: provider.label, hint: provider.docsPath ? `Docs: ${provider.docsPath}` : void 0 })) }).then((id) => resolveProviderMatch(providers, String(id))); if (!selectedProvider) throw new Error("Unknown provider. Use --provider to pick a provider plugin."); const chosenMethod = pickAuthMethod(selectedProvider, opts.method) ?? (selectedProvider.auth.length === 1 ? selectedProvider.auth[0] : await prompter.select({ message: `Auth method for ${selectedProvider.label}`, options: selectedProvider.auth.map((method) => ({ value: method.id, label: method.label, hint: method.hint })) }).then((id) => selectedProvider.auth.find((method) => method.id === String(id)))); if (!chosenMethod) throw new Error("Unknown auth method. Use --method to select one."); const isRemote = isRemoteEnvironment(); const result = await chosenMethod.run({ config, agentDir, workspaceDir, prompter, runtime, isRemote, openUrl: async (url) => { await openUrl(url); }, oauth: { createVpsAwareHandlers: (params) => createVpsAwareOAuthHandlers(params) } }); for (const profile of result.profiles) upsertAuthProfile({ profileId: profile.profileId, credential: profile.credential, agentDir }); await updateConfig((cfg) => { let next = cfg; if (result.configPatch) next = mergeConfigPatch(next, result.configPatch); for (const profile of result.profiles) next = applyAuthProfileConfig(next, { profileId: profile.profileId, provider: profile.credential.provider, mode: credentialMode(profile.credential) }); if (opts.setDefault && result.defaultModel) next = applyDefaultModel(next, result.defaultModel); return next; }); logConfigUpdated(runtime); for (const profile of result.profiles) runtime.log(`Auth profile: ${profile.profileId} (${profile.credential.provider}/${credentialMode(profile.credential)})`); if (result.defaultModel) runtime.log(opts.setDefault ? `Default model set to ${result.defaultModel}` : `Default model available: ${result.defaultModel} (use --set-default to apply)`); if (result.notes && result.notes.length > 0) await prompter.note(result.notes.join("\n"), "Provider notes"); } //#endregion //#region src/commands/models/auth-order.ts function resolveTargetAgent(cfg, raw) { const agentId = resolveKnownAgentId({ cfg, rawAgentId: raw }) ?? resolveDefaultAgentId(cfg); return { agentId, agentDir: resolveAgentDir(cfg, agentId) }; } function describeOrder(store, provider) { const providerKey = normalizeProviderId(provider); const order = store.order?.[providerKey]; return Array.isArray(order) ? order : []; } async function modelsAuthOrderGetCommand(opts, runtime) { const rawProvider = opts.provider?.trim(); if (!rawProvider) throw new Error("Missing --provider."); const provider = normalizeProviderId(rawProvider); const { agentId, agentDir } = resolveTargetAgent(loadConfig(), opts.agent); const order = describeOrder(ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }), provider); if (opts.json) { runtime.log(JSON.stringify({ agentId, agentDir, provider, authStorePath: shortenHomePath(`${agentDir}/auth-profiles.json`), order: order.length > 0 ? order : null }, null, 2)); return; } runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); runtime.log(`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`); runtime.log(order.length > 0 ? `Order override: ${order.join(", ")}` : "Order override: (none)"); } async function modelsAuthOrderClearCommand(opts, runtime) { const rawProvider = opts.provider?.trim(); if (!rawProvider) throw new Error("Missing --provider."); const provider = normalizeProviderId(rawProvider); const { agentId, agentDir } = resolveTargetAgent(loadConfig(), opts.agent); if (!await setAuthProfileOrder({ agentDir, provider, order: null })) throw new Error("Failed to update auth-profiles.json (lock busy?)."); runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); runtime.log("Cleared per-agent order override."); } async function modelsAuthOrderSetCommand(opts, runtime) { const rawProvider = opts.provider?.trim(); if (!rawProvider) throw new Error("Missing --provider."); const provider = normalizeProviderId(rawProvider); const { agentId, agentDir } = resolveTargetAgent(loadConfig(), opts.agent); const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const providerKey = normalizeProviderId(provider); const requested = (opts.order ?? []).map((entry) => String(entry).trim()).filter(Boolean); if (requested.length === 0) throw new Error("Missing profile ids. Provide one or more profile ids."); for (const profileId of requested) { const cred = store.profiles[profileId]; if (!cred) throw new Error(`Auth profile "${profileId}" not found in ${agentDir}.`); if (normalizeProviderId(cred.provider) !== providerKey) throw new Error(`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`); } const updated = await setAuthProfileOrder({ agentDir, provider, order: requested }); if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?)."); runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); runtime.log(`Order override: ${describeOrder(updated, provider).join(", ")}`); } //#endregion //#region src/commands/models/fallbacks.ts async function modelsFallbacksListCommand(opts, runtime) { ensureFlagCompatibility(opts); const fallbacks = loadConfig().agents?.defaults?.model?.fallbacks ?? []; if (opts.json) { runtime.log(JSON.stringify({ fallbacks }, null, 2)); return; } if (opts.plain) { for (const entry of fallbacks) runtime.log(entry); return; } runtime.log(`Fallbacks (${fallbacks.length}):`); if (fallbacks.length === 0) { runtime.log("- none"); return; } for (const entry of fallbacks) runtime.log(`- ${entry}`); } async function modelsFallbacksAddCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const targetKey = modelKey(resolved.provider, resolved.model); const nextModels = { ...cfg.agents?.defaults?.models }; if (!nextModels[targetKey]) nextModels[targetKey] = {}; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const existing = cfg.agents?.defaults?.model?.fallbacks ?? []; if (existing.map((entry) => resolveModelRefFromString({ raw: String(entry ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex })).filter((entry) => Boolean(entry)).map((entry) => modelKey(entry.ref.provider, entry.ref.model)).includes(targetKey)) return cfg; const existingModel = cfg.agents?.defaults?.model; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, model: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: [...existing, targetKey] }, models: nextModels } } }; }); logConfigUpdated(runtime); runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`); } async function modelsFallbacksRemoveCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const targetKey = modelKey(resolved.provider, resolved.model); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const existing = cfg.agents?.defaults?.model?.fallbacks ?? []; const filtered = existing.filter((entry) => { const resolvedEntry = resolveModelRefFromString({ raw: String(entry ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex }); if (!resolvedEntry) return true; return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey; }); if (filtered.length === existing.length) throw new Error(`Fallback not found: ${targetKey}`); const existingModel = cfg.agents?.defaults?.model; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, model: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: filtered } } } }; }); logConfigUpdated(runtime); runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`); } async function modelsFallbacksClearCommand(runtime) { await updateConfig((cfg) => { const existingModel = cfg.agents?.defaults?.model; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, model: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: [] } } } }; }); logConfigUpdated(runtime); runtime.log("Fallback list cleared."); } //#endregion //#region src/commands/models/image-fallbacks.ts async function modelsImageFallbacksListCommand(opts, runtime) { ensureFlagCompatibility(opts); const fallbacks = loadConfig().agents?.defaults?.imageModel?.fallbacks ?? []; if (opts.json) { runtime.log(JSON.stringify({ fallbacks }, null, 2)); return; } if (opts.plain) { for (const entry of fallbacks) runtime.log(entry); return; } runtime.log(`Image fallbacks (${fallbacks.length}):`); if (fallbacks.length === 0) { runtime.log("- none"); return; } for (const entry of fallbacks) runtime.log(`- ${entry}`); } async function modelsImageFallbacksAddCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const targetKey = modelKey(resolved.provider, resolved.model); const nextModels = { ...cfg.agents?.defaults?.models }; if (!nextModels[targetKey]) nextModels[targetKey] = {}; const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? []; if (existing.map((entry) => resolveModelRefFromString({ raw: String(entry ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex })).filter((entry) => Boolean(entry)).map((entry) => modelKey(entry.ref.provider, entry.ref.model)).includes(targetKey)) return cfg; const existingModel = cfg.agents?.defaults?.imageModel; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, imageModel: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: [...existing, targetKey] }, models: nextModels } } }; }); logConfigUpdated(runtime); runtime.log(`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`); } async function modelsImageFallbacksRemoveCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const targetKey = modelKey(resolved.provider, resolved.model); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? []; const filtered = existing.filter((entry) => { const resolvedEntry = resolveModelRefFromString({ raw: String(entry ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex }); if (!resolvedEntry) return true; return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey; }); if (filtered.length === existing.length) throw new Error(`Image fallback not found: ${targetKey}`); const existingModel = cfg.agents?.defaults?.imageModel; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, imageModel: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: filtered } } } }; }); logConfigUpdated(runtime); runtime.log(`Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`); } async function modelsImageFallbacksClearCommand(runtime) { await updateConfig((cfg) => { const existingModel = cfg.agents?.defaults?.imageModel; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, imageModel: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: [] } } } }; }); logConfigUpdated(runtime); runtime.log("Image fallback list cleared."); } //#endregion //#region src/commands/models/list.configured.ts function resolveConfiguredEntries(cfg) { const resolvedDefault = resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL }); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const order = []; const tagsByKey = /* @__PURE__ */ new Map(); const aliasesByKey = /* @__PURE__ */ new Map(); for (const [key, aliases] of aliasIndex.byKey.entries()) aliasesByKey.set(key, aliases); const addEntry = (ref, tag) => { const key = modelKey(ref.provider, ref.model); if (!tagsByKey.has(key)) { tagsByKey.set(key, /* @__PURE__ */ new Set()); order.push(key); } tagsByKey.get(key)?.add(tag); }; addEntry(resolvedDefault, "default"); const modelConfig = cfg.agents?.defaults?.model; const imageModelConfig = cfg.agents?.defaults?.imageModel; const modelFallbacks = typeof modelConfig === "object" ? modelConfig?.fallbacks ?? [] : []; const imageFallbacks = typeof imageModelConfig === "object" ? imageModelConfig?.fallbacks ?? [] : []; const imagePrimary = imageModelConfig?.primary?.trim() ?? ""; modelFallbacks.forEach((raw, idx) => { const resolved = resolveModelRefFromString({ raw: String(raw ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex }); if (!resolved) return; addEntry(resolved.ref, `fallback#${idx + 1}`); }); if (imagePrimary) { const resolved = resolveModelRefFromString({ raw: imagePrimary, defaultProvider: DEFAULT_PROVIDER, aliasIndex }); if (resolved) addEntry(resolved.ref, "image"); } imageFallbacks.forEach((raw, idx) => { const resolved = resolveModelRefFromString({ raw: String(raw ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex }); if (!resolved) return; addEntry(resolved.ref, `img-fallback#${idx + 1}`); }); for (const key of Object.keys(cfg.agents?.defaults?.models ?? {})) { const parsed = parseModelRef(String(key ?? ""), DEFAULT_PROVIDER); if (!parsed) continue; addEntry(parsed, "configured"); } return { entries: order.map((key) => { const slash = key.indexOf("/"); return { key, ref: { provider: slash === -1 ? key : key.slice(0, slash), model: slash === -1 ? "" : key.slice(slash + 1) }, tags: tagsByKey.get(key) ?? /* @__PURE__ */ new Set(), aliases: aliasesByKey.get(key) ?? [] }; }) }; } //#endregion //#region src/commands/models/list.registry.ts const isLocalBaseUrl = (baseUrl) => { try { const host = new URL(baseUrl).hostname.toLowerCase(); return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1" || host.endsWith(".local"); } catch { return false; } }; const hasAuthForProvider = (provider, cfg, authStore) => { if (listProfilesForProvider(authStore, provider).length > 0) return true; if (provider === "amazon-bedrock" && resolveAwsSdkEnvVarName()) return true; if (resolveEnvApiKey(provider)) return true; if (getCustomProviderApiKey(cfg, provider)) return true; return false; }; async function loadModelRegistry(cfg) { await ensureOpenClawModelsJson(cfg); const agentDir = resolveOpenClawAgentDir(); const registry = discoverModels(discoverAuthStorage(agentDir), agentDir); const models = registry.getAll(); const availableModels = registry.getAvailable(); return { registry, models, availableKeys: new Set(availableModels.map((model) => modelKey(model.provider, model.id))) }; } function toModelRow(params) { const { model, key, tags, aliases = [], availableKeys, cfg, authStore } = params; if (!model) return { key, name: key, input: "-", contextWindow: null, local: null, available: null, tags: [...tags, "missing"], missing: true }; const input = model.input.join("+") || "text"; const local = isLocalBaseUrl(model.baseUrl); const available = cfg && authStore ? hasAuthForProvider(model.provider, cfg, authStore) : availableKeys?.has(modelKey(model.provider, model.id)) ?? false; const aliasTags = aliases.length > 0 ? [`alias:${aliases.join(",")}`] : []; const mergedTags = new Set(tags); if (aliasTags.length > 0) { for (const tag of mergedTags) if (tag === "alias" || tag.startsWith("alias:")) mergedTags.delete(tag); for (const tag of aliasTags) mergedTags.add(tag); } return { key, name: model.name || model.id, input, contextWindow: model.contextWindow ?? null, local, available, tags: Array.from(mergedTags), missing: false }; } //#endregion //#region src/commands/models/list.format.ts const isRich = (opts) => Boolean(isRich$1() && !opts?.json && !opts?.plain); const pad$1 = (value, size) => value.padEnd(size); const formatTag = (tag, rich) => { if (!rich) return tag; if (tag === "default") return theme.success(tag); if (tag === "image") return theme.accentBright(tag); if (tag === "configured") return theme.accent(tag); if (tag === "missing") return theme.error(tag); if (tag.startsWith("fallback#")) return theme.warn(tag); if (tag.startsWith("img-fallback#")) return theme.warn(tag); if (tag.startsWith("alias:")) return theme.accentDim(tag); return theme.muted(tag); }; const truncate$1 = (value, max) => { if (value.length <= max) return value; if (max <= 3) return value.slice(0, max); return `${value.slice(0, max - 3)}...`; }; const maskApiKey = (value) => { const trimmed = value.trim(); if (!trimmed) return "missing"; if (trimmed.length <= 16) return trimmed; return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`; }; //#endregion //#region src/commands/models/list.table.ts const MODEL_PAD$1 = 42; const INPUT_PAD = 10; const CTX_PAD$1 = 8; const LOCAL_PAD = 5; const AUTH_PAD = 5; function printModelTable(rows, runtime, opts = {}) { if (opts.json) { runtime.log(JSON.stringify({ count: rows.length, models: rows }, null, 2)); return; } if (opts.plain) { for (const row of rows) runtime.log(row.key); return; } const rich = isRich(opts); const header = [ pad$1("Model", MODEL_PAD$1), pad$1("Input", INPUT_PAD), pad$1("Ctx", CTX_PAD$1), pad$1("Local", LOCAL_PAD), pad$1("Auth", AUTH_PAD), "Tags" ].join(" "); runtime.log(rich ? theme.heading(header) : header); for (const row of rows) { const keyLabel = pad$1(truncate$1(row.key, MODEL_PAD$1), MODEL_PAD$1); const inputLabel = pad$1(row.input || "-", INPUT_PAD); const ctxLabel = pad$1(formatTokenK(row.contextWindow), CTX_PAD$1); const localLabel = pad$1(row.local === null ? "-" : row.local ? "yes" : "no", LOCAL_PAD); const authLabel = pad$1(row.available === null ? "-" : row.available ? "yes" : "no", AUTH_PAD); const tagsLabel = row.tags.length > 0 ? rich ? row.tags.map((tag) => formatTag(tag, rich)).join(",") : row.tags.join(",") : ""; const coloredInput = colorize(rich, row.input.includes("image") ? theme.accentBright : theme.info, inputLabel); const coloredLocal = colorize(rich, row.local === null ? theme.muted : row.local ? theme.success : theme.muted, localLabel); const coloredAuth = colorize(rich, row.available === null ? theme.muted : row.available ? theme.success : theme.error, authLabel); const line = [ rich ? theme.accent(keyLabel) : keyLabel, coloredInput, ctxLabel, coloredLocal, coloredAuth, tagsLabel ].join(" "); runtime.log(line); } } //#endregion //#region src/commands/models/list.list-command.ts async function modelsListCommand(opts, runtime) { ensureFlagCompatibility(opts); const cfg = loadConfig(); const authStore = ensureAuthProfileStore(); const providerFilter = (() => { const raw = opts.provider?.trim(); if (!raw) return; return parseModelRef(`${raw}/_`, DEFAULT_PROVIDER)?.provider ?? raw.toLowerCase(); })(); let models = []; let availableKeys; try { const loaded = await loadModelRegistry(cfg); models = loaded.models; availableKeys = loaded.availableKeys; } catch (err) { runtime.error(`Model registry unavailable: ${String(err)}`); } const modelByKey = new Map(models.map((model) => [modelKey(model.provider, model.id), model])); const { entries } = resolveConfiguredEntries(cfg); const configuredByKey = new Map(entries.map((entry) => [entry.key, entry])); const rows = []; const isLocalBaseUrl = (baseUrl) => { try { const host = new URL(baseUrl).hostname.toLowerCase(); return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1" || host.endsWith(".local"); } catch { return false; } }; if (opts.all) { const sorted = [...models].toSorted((a, b) => { const p = a.provider.localeCompare(b.provider); if (p !== 0) return p; return a.id.localeCompare(b.id); }); for (const model of sorted) { if (providerFilter && model.provider.toLowerCase() !== providerFilter) continue; if (opts.local && !isLocalBaseUrl(model.baseUrl)) continue; const key = modelKey(model.provider, model.id); const configured = configuredByKey.get(key); rows.push(toModelRow({ model, key, tags: configured ? Array.from(configured.tags) : [], aliases: configured?.aliases ?? [], availableKeys, cfg, authStore })); } } else for (const entry of entries) { if (providerFilter && entry.ref.provider.toLowerCase() !== providerFilter) continue; const model = modelByKey.get(entry.key); if (opts.local && model && !isLocalBaseUrl(model.baseUrl)) continue; if (opts.local && !model) continue; rows.push(toModelRow({ model, key: entry.key, tags: Array.from(entry.tags), aliases: entry.aliases, availableKeys, cfg, authStore })); } if (rows.length === 0) { runtime.log("No models found."); return; } printModelTable(rows, runtime, opts); } //#endregion //#region src/commands/models/list.auth-overview.ts function resolveProviderAuthOverview(params) { const { provider, cfg, store } = params; const now = Date.now(); const profiles = listProfilesForProvider(store, provider); const withUnusableSuffix = (base, profileId) => { const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId); if (!unusableUntil || now >= unusableUntil) return base; const stats = store.usageStats?.[profileId]; return `${base} [${typeof stats?.disabledUntil === "number" && now < stats.disabledUntil ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}` : "cooldown"} ${formatRemainingShort(unusableUntil - now)}]`; }; const labels = profiles.map((profileId) => { const profile = store.profiles[profileId]; if (!profile) return `${profileId}=missing`; if (profile.type === "api_key") return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key ?? "")}`, profileId); if (profile.type === "token") return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId); const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); const suffix = display === profileId ? "" : display.startsWith(profileId) ? display.slice(profileId.length).trim() : `(${display})`; return withUnusableSuffix(`${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`, profileId); }); const oauthCount = profiles.filter((id) => store.profiles[id]?.type === "oauth").length; const tokenCount = profiles.filter((id) => store.profiles[id]?.type === "token").length; const apiKeyCount = profiles.filter((id) => store.profiles[id]?.type === "api_key").length; const envKey = resolveEnvApiKey(provider); const customKey = getCustomProviderApiKey(cfg, provider); return { provider, effective: (() => { if (profiles.length > 0) return { kind: "profiles", detail: shortenHomePath(resolveAuthStorePathForDisplay()) }; if (envKey) return { kind: "env", detail: envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth") ? "OAuth (env)" : maskApiKey(envKey.apiKey) }; if (customKey) return { kind: "models.json", detail: maskApiKey(customKey) }; return { kind: "missing", detail: "missing" }; })(), profiles: { count: profiles.length, oauth: oauthCount, token: tokenCount, apiKey: apiKeyCount, labels }, ...envKey ? { env: { value: envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth") ? "OAuth (env)" : maskApiKey(envKey.apiKey), source: envKey.source } } : {}, ...customKey ? { modelsJson: { value: maskApiKey(customKey), source: `models.json: ${shortenHomePath(params.modelsPath)}` } } : {} }; } //#endregion //#region src/commands/models/list.probe.ts const PROBE_PROMPT = "Reply with OK. Do not use tools."; const toStatus = (reason) => { if (!reason) return "unknown"; if (reason === "auth") return "auth"; if (reason === "rate_limit") return "rate_limit"; if (reason === "billing") return "billing"; if (reason === "timeout") return "timeout"; if (reason === "format") return "format"; return "unknown"; }; function buildCandidateMap(modelCandidates) { const map = /* @__PURE__ */ new Map(); for (const raw of modelCandidates) { const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER); if (!parsed) continue; const list = map.get(parsed.provider) ?? []; if (!list.includes(parsed.model)) list.push(parsed.model); map.set(parsed.provider, list); } return map; } function selectProbeModel(params) { const { provider, candidates, catalog } = params; const direct = candidates.get(provider); if (direct && direct.length > 0) return { provider, model: direct[0] }; const fromCatalog = catalog.find((entry) => entry.provider === provider); if (fromCatalog) return { provider: fromCatalog.provider, model: fromCatalog.id }; return null; } function buildProbeTargets(params) { const { cfg, providers, modelCandidates, options } = params; const store = ensureAuthProfileStore(); const providerFilter = options.provider?.trim(); const providerFilterKey = providerFilter ? normalizeProviderId(providerFilter) : null; const profileFilter = new Set((options.profileIds ?? []).map((id) => id.trim()).filter(Boolean)); return loadModelCatalog({ config: cfg }).then((catalog) => { const candidates = buildCandidateMap(modelCandidates); const targets = []; const results = []; for (const provider of providers) { const providerKey = normalizeProviderId(provider); if (providerFilterKey && providerKey !== providerFilterKey) continue; const model = selectProbeModel({ provider: providerKey, candidates, catalog }); const profileIds = listProfilesForProvider(store, providerKey); const explicitOrder = (() => { const order = store.order; if (order) { for (const [key, value] of Object.entries(order)) if (normalizeProviderId(key) === providerKey) return value; } const cfgOrder = cfg?.auth?.order; if (cfgOrder) { for (const [key, value] of Object.entries(cfgOrder)) if (normalizeProviderId(key) === providerKey) return value; } })(); const allowedProfiles = explicitOrder && explicitOrder.length > 0 ? new Set(resolveAuthProfileOrder({ cfg, store, provider: providerKey })) : null; const filteredProfiles = profileFilter.size ? profileIds.filter((id) => profileFilter.has(id)) : profileIds; if (filteredProfiles.length > 0) { for (const profileId of filteredProfiles) { const mode = store.profiles[profileId]?.type; const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId }); if (explicitOrder && !explicitOrder.includes(profileId)) { results.push({ provider: providerKey, model: model ? `${model.provider}/${model.model}` : void 0, profileId, label, source: "profile", mode, status: "unknown", error: "Excluded by auth.order for this provider." }); continue; } if (allowedProfiles && !allowedProfiles.has(profileId)) { results.push({ provider: providerKey, model: model ? `${model.provider}/${model.model}` : void 0, profileId, label, source: "profile", mode, status: "unknown", error: "Auth profile credentials are missing or expired." }); continue; } if (!model) { results.push({ provider: providerKey, model: void 0, profileId, label, source: "profile", mode, status: "no_model", error: "No model available for probe" }); continue; } targets.push({ provider: providerKey, model, profileId, label, source: "profile", mode }); } continue; } if (profileFilter.size > 0) continue; const envKey = resolveEnvApiKey(providerKey); const customKey = getCustomProviderApiKey(cfg, providerKey); if (!envKey && !customKey) continue; const label = envKey ? "env" : "models.json"; const source = envKey ? "env" : "models.json"; const mode = envKey?.source.includes("OAUTH_TOKEN") ? "oauth" : "api_key"; if (!model) { results.push({ provider: providerKey, model: void 0, label, source, mode, status: "no_model", error: "No model available for probe" }); continue; } targets.push({ provider: providerKey, model, label, source, mode }); } return { targets, results }; }); } async function probeTarget(params) { const { cfg, agentId, agentDir, workspaceDir, sessionDir, target, timeoutMs, maxTokens } = params; if (!target.model) return { provider: target.provider, model: void 0, profileId: target.profileId, label: target.label, source: target.source, mode: target.mode, status: "no_model", error: "No model available for probe" }; const sessionId = `probe-${target.provider}-${crypto.randomUUID()}`; const sessionFile = resolveSessionTranscriptPath(sessionId, agentId); await fs.mkdir(sessionDir, { recursive: true }); const start = Date.now(); try { await runEmbeddedPiAgent({ sessionId, sessionFile, workspaceDir, agentDir, config: cfg, prompt: PROBE_PROMPT, provider: target.model.provider, model: target.model.model, authProfileId: target.profileId, authProfileIdSource: target.profileId ? "user" : void 0, timeoutMs, runId: `probe-${crypto.randomUUID()}`, lane: `auth-probe:${target.provider}:${target.profileId ?? target.source}`, thinkLevel: "off", reasoningLevel: "off", verboseLevel: "off", streamParams: { maxTokens } }); return { provider: target.provider, model: `${target.model.provider}/${target.model.model}`, profileId: target.profileId, label: target.label, source: target.source, mode: target.mode, status: "ok", latencyMs: Date.now() - start }; } catch (err) { const described = describeFailoverError(err); return { provider: target.provider, model: `${target.model.provider}/${target.model.model}`, profileId: target.profileId, label: target.label, source: target.source, mode: target.mode, status: toStatus(described.reason), error: redactSecrets(described.message), latencyMs: Date.now() - start }; } } async function runTargetsWithConcurrency(params) { const { cfg, targets, timeoutMs, maxTokens, onProgress } = params; const concurrency = Math.max(1, Math.min(targets.length || 1, params.concurrency)); const agentId = resolveDefaultAgentId(cfg); const agentDir = resolveOpenClawAgentDir(); const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId) ?? resolveDefaultAgentWorkspaceDir(); const sessionDir = resolveSessionTranscriptsDirForAgent(agentId); await fs.mkdir(workspaceDir, { recursive: true }); let completed = 0; const results = Array.from({ length: targets.length }); let cursor = 0; const worker = async () => { while (true) { const index = cursor; cursor += 1; if (index >= targets.length) return; const target = targets[index]; onProgress?.({ completed, total: targets.length, label: `Probing ${target.provider}${target.profileId ? ` (${target.label})` : ""}` }); results[index] = await probeTarget({ cfg, agentId, agentDir, workspaceDir, sessionDir, target, timeoutMs, maxTokens }); completed += 1; onProgress?.({ completed, total: targets.length }); } }; await Promise.all(Array.from({ length: concurrency }, () => worker())); return results.filter((entry) => Boolean(entry)); } async function runAuthProbes(params) { const startedAt = Date.now(); const plan = await buildProbeTargets({ cfg: params.cfg, providers: params.providers, modelCandidates: params.modelCandidates, options: params.options }); const totalTargets = plan.targets.length; params.onProgress?.({ completed: 0, total: totalTargets }); const results = totalTargets ? await runTargetsWithConcurrency({ cfg: params.cfg, targets: plan.targets, timeoutMs: params.options.timeoutMs, maxTokens: params.options.maxTokens, concurrency: params.options.concurrency, onProgress: params.onProgress }) : []; const finishedAt = Date.now(); return { startedAt, finishedAt, durationMs: finishedAt - startedAt, totalTargets, options: params.options, results: [...plan.results, ...results] }; } function formatProbeLatency(latencyMs) { if (!latencyMs && latencyMs !== 0) return "-"; return formatMs(latencyMs); } function sortProbeResults(results) { return results.slice().toSorted((a, b) => { const provider = a.provider.localeCompare(b.provider); if (provider !== 0) return provider; const aLabel = a.label || a.profileId || ""; const bLabel = b.label || b.profileId || ""; return aLabel.localeCompare(bLabel); }); } function describeProbeSummary(summary) { if (summary.totalTargets === 0) return "No probe targets."; return `Probed ${summary.totalTargets} target${summary.totalTargets === 1 ? "" : "s"} in ${formatMs(summary.durationMs)}`; } //#endregion //#region src/commands/models/list.status-command.ts async function modelsStatusCommand(opts, runtime) { ensureFlagCompatibility(opts); if (opts.plain && opts.probe) throw new Error("--probe cannot be used with --plain output."); const cfg = loadConfig(); const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent }); const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir(); const agentModelPrimary = agentId ? resolveAgentModelPrimary(cfg, agentId) : void 0; const agentFallbacksOverride = agentId ? resolveAgentModelFallbacksOverride(cfg, agentId) : void 0; const resolved = agentId ? resolveDefaultModelForAgent({ cfg, agentId }) : resolveConfiguredModelRef({ cfg, defaultProvider: DEFAULT_PROVIDER, defaultModel: DEFAULT_MODEL }); const modelConfig = cfg.agents?.defaults?.model; const imageConfig = cfg.agents?.defaults?.imageModel; const rawDefaultsModel = typeof modelConfig === "string" ? modelConfig.trim() : modelConfig?.primary?.trim() ?? ""; const rawModel = agentModelPrimary ?? rawDefaultsModel; const resolvedLabel = `${resolved.provider}/${resolved.model}`; const defaultLabel = rawModel || resolvedLabel; const defaultsFallbacks = typeof modelConfig === "object" ? modelConfig?.fallbacks ?? [] : []; const fallbacks = agentFallbacksOverride ?? defaultsFallbacks; const imageModel = typeof imageConfig === "string" ? imageConfig.trim() : imageConfig?.primary?.trim() ?? ""; const imageFallbacks = typeof imageConfig === "object" ? imageConfig?.fallbacks ?? [] : []; const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce((acc, [key, entry]) => { const alias = entry?.alias?.trim(); if (alias) acc[alias] = key; return acc; }, {}); const allowed = Object.keys(cfg.agents?.defaults?.models ?? {}); const store = ensureAuthProfileStore(agentDir); const modelsPath = path.join(agentDir, "models.json"); const providersFromStore = new Set(Object.values(store.profiles).map((profile) => profile.provider).filter((p) => Boolean(p))); const providersFromConfig = new Set(Object.keys(cfg.models?.providers ?? {}).map((p) => p.trim()).filter(Boolean)); const providersFromModels = /* @__PURE__ */ new Set(); const providersInUse = /* @__PURE__ */ new Set(); for (const raw of [ defaultLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed ]) { const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER); if (parsed?.provider) providersFromModels.add(parsed.provider); } for (const raw of [ defaultLabel, ...fallbacks, imageModel, ...imageFallbacks ]) { const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER); if (parsed?.provider) providersInUse.add(parsed.provider); } const providersFromEnv = /* @__PURE__ */ new Set(); for (const provider of [ "anthropic", "github-copilot", "google-vertex", "openai", "google", "groq", "cerebras", "xai", "openrouter", "zai", "mistral", "synthetic" ]) if (resolveEnvApiKey(provider)) providersFromEnv.add(provider); const providers = Array.from(new Set([ ...providersFromStore, ...providersFromConfig, ...providersFromModels, ...providersFromEnv ])).map((p) => p.trim()).filter(Boolean).toSorted((a, b) => a.localeCompare(b)); const applied = getShellEnvAppliedKeys(); const shellFallbackEnabled = shouldEnableShellEnvFallback(process.env) || cfg.env?.shellEnv?.enabled === true; const providerAuth = providers.map((provider) => resolveProviderAuthOverview({ provider, cfg, store, modelsPath })).filter((entry) => { return entry.profiles.count > 0 || Boolean(entry.env) || Boolean(entry.modelsJson); }); const providerAuthMap = new Map(providerAuth.map((entry) => [entry.provider, entry])); const missingProvidersInUse = Array.from(providersInUse).filter((provider) => !providerAuthMap.has(provider)).toSorted((a, b) => a.localeCompare(b)); const probeProfileIds = (() => { if (!opts.probeProfile) return []; return (Array.isArray(opts.probeProfile) ? opts.probeProfile : [opts.probeProfile]).flatMap((value) => String(value ?? "").split(",")).map((value) => value.trim()).filter(Boolean); })(); const probeTimeoutMs = opts.probeTimeout ? Number(opts.probeTimeout) : 8e3; if (!Number.isFinite(probeTimeoutMs) || probeTimeoutMs <= 0) throw new Error("--probe-timeout must be a positive number (ms)."); const probeConcurrency = opts.probeConcurrency ? Number(opts.probeConcurrency) : 2; if (!Number.isFinite(probeConcurrency) || probeConcurrency <= 0) throw new Error("--probe-concurrency must be > 0."); const probeMaxTokens = opts.probeMaxTokens ? Number(opts.probeMaxTokens) : 8; if (!Number.isFinite(probeMaxTokens) || probeMaxTokens <= 0) throw new Error("--probe-max-tokens must be > 0."); const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: DEFAULT_PROVIDER }); const modelCandidates = [ rawModel || resolvedLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed ].filter(Boolean).map((raw) => resolveModelRefFromString({ raw: String(raw ?? ""), defaultProvider: DEFAULT_PROVIDER, aliasIndex })?.ref).filter((ref) => Boolean(ref)).map((ref) => `${ref.provider}/${ref.model}`); let probeSummary; if (opts.probe) probeSummary = await withProgressTotals({ label: "Probing auth profiles…", total: 1 }, async (update) => { return await runAuthProbes({ cfg, providers, modelCandidates, options: { provider: opts.probeProvider, profileIds: probeProfileIds, timeoutMs: probeTimeoutMs, concurrency: probeConcurrency, maxTokens: probeMaxTokens }, onProgress: update }); }); const providersWithOauth = providerAuth.filter((entry) => entry.profiles.oauth > 0 || entry.profiles.token > 0 || entry.env?.value === "OAuth (env)").map((entry) => { const count = entry.profiles.oauth + entry.profiles.token + (entry.env?.value === "OAuth (env)" ? 1 : 0); return `${entry.provider} (${count})`; }); const authHealth = buildAuthHealthSummary({ store, cfg, warnAfterMs: DEFAULT_OAUTH_WARN_MS, providers }); const oauthProfiles = authHealth.profiles.filter((profile) => profile.type === "oauth" || profile.type === "token"); const unusableProfiles = (() => { const now = Date.now(); const out = []; for (const profileId of Object.keys(store.usageStats ?? {})) { const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId); if (!unusableUntil || now >= unusableUntil) continue; const stats = store.usageStats?.[profileId]; const kind = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil ? "disabled" : "cooldown"; out.push({ profileId, provider: store.profiles[profileId]?.provider, kind, reason: stats?.disabledReason, until: unusableUntil, remainingMs: unusableUntil - now }); } return out.toSorted((a, b) => a.remainingMs - b.remainingMs); })(); const checkStatus = (() => { const hasExpiredOrMissing = oauthProfiles.some((profile) => ["expired", "missing"].includes(profile.status)) || missingProvidersInUse.length > 0; const hasExpiring = oauthProfiles.some((profile) => profile.status === "expiring"); if (hasExpiredOrMissing) return 1; if (hasExpiring) return 2; return 0; })(); if (opts.json) { runtime.log(JSON.stringify({ configPath: CONFIG_PATH, ...agentId ? { agentId } : {}, agentDir, defaultModel: defaultLabel, resolvedDefault: resolvedLabel, fallbacks, imageModel: imageModel || null, imageFallbacks, ...agentId ? { modelConfig: { defaultSource: agentModelPrimary ? "agent" : "defaults", fallbacksSource: agentFallbacksOverride !== void 0 ? "agent" : "defaults" } } : {}, aliases, allowed, auth: { storePath: resolveAuthStorePathForDisplay(agentDir), shellEnvFallback: { enabled: shellFallbackEnabled, appliedKeys: applied }, providersWithOAuth: providersWithOauth, missingProvidersInUse, providers: providerAuth, unusableProfiles, oauth: { warnAfterMs: authHealth.warnAfterMs, profiles: authHealth.profiles, providers: authHealth.providers }, probes: probeSummary } }, null, 2)); if (opts.check) runtime.exit(checkStatus); return; } if (opts.plain) { runtime.log(resolvedLabel); if (opts.check) runtime.exit(checkStatus); return; } const rich = isRich(opts); const label = (value) => colorize(rich, theme.accent, value.padEnd(14)); const labelWithSource = (value, source) => label(source ? `${value} (${source})` : value); const displayDefault = rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel; runtime.log(`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(CONFIG_PATH))}`); runtime.log(`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(agentDir))}`); runtime.log(`${labelWithSource("Default", agentId ? agentModelPrimary ? "agent" : "defaults" : void 0)}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.success, displayDefault)}`); runtime.log(`${labelWithSource(`Fallbacks (${fallbacks.length || 0})`, agentId ? agentFallbacksOverride !== void 0 ? "agent" : "defaults" : void 0)}${colorize(rich, theme.muted, ":")} ${colorize(rich, fallbacks.length ? theme.warn : theme.muted, fallbacks.length ? fallbacks.join(", ") : "-")}`); runtime.log(`${labelWithSource("Image model", agentId ? "defaults" : void 0)}${colorize(rich, theme.muted, ":")} ${colorize(rich, imageModel ? theme.accentBright : theme.muted, imageModel || "-")}`); runtime.log(`${labelWithSource(`Image fallbacks (${imageFallbacks.length || 0})`, agentId ? "defaults" : void 0)}${colorize(rich, theme.muted, ":")} ${colorize(rich, imageFallbacks.length ? theme.accentBright : theme.muted, imageFallbacks.length ? imageFallbacks.join(", ") : "-")}`); runtime.log(`${label(`Aliases (${Object.keys(aliases).length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize(rich, Object.keys(aliases).length ? theme.accent : theme.muted, Object.keys(aliases).length ? Object.entries(aliases).map(([alias, target]) => rich ? `${theme.accentDim(alias)} ${theme.muted("->")} ${theme.info(target)}` : `${alias} -> ${target}`).join(", ") : "-")}`); runtime.log(`${label(`Configured models (${allowed.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize(rich, allowed.length ? theme.info : theme.muted, allowed.length ? allowed.join(", ") : "all")}`); runtime.log(""); runtime.log(colorize(rich, theme.heading, "Auth overview")); runtime.log(`${label("Auth store")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(resolveAuthStorePathForDisplay(agentDir)))}`); runtime.log(`${label("Shell env")}${colorize(rich, theme.muted, ":")} ${colorize(rich, shellFallbackEnabled ? theme.success : theme.muted, shellFallbackEnabled ? "on" : "off")}${applied.length ? colorize(rich, theme.muted, ` (applied: ${applied.join(", ")})`) : ""}`); runtime.log(`${label(`Providers w/ OAuth/tokens (${providersWithOauth.length || 0})`)}${colorize(rich, theme.muted, ":")} ${colorize(rich, providersWithOauth.length ? theme.info : theme.muted, providersWithOauth.length ? providersWithOauth.join(", ") : "-")}`); const formatKey = (key) => colorize(rich, theme.warn, key); const formatKeyValue = (key, value) => `${formatKey(key)}=${colorize(rich, theme.info, value)}`; const formatSeparator = () => colorize(rich, theme.muted, " | "); for (const entry of providerAuth) { const separator = formatSeparator(); const bits = []; bits.push(formatKeyValue("effective", `${colorize(rich, theme.accentBright, entry.effective.kind)}:${colorize(rich, theme.muted, entry.effective.detail)}`)); if (entry.profiles.count > 0) { bits.push(formatKeyValue("profiles", `${entry.profiles.count} (oauth=${entry.profiles.oauth}, token=${entry.profiles.token}, api_key=${entry.profiles.apiKey})`)); if (entry.profiles.labels.length > 0) bits.push(colorize(rich, theme.info, entry.profiles.labels.join(", "))); } if (entry.env) bits.push(formatKeyValue("env", `${entry.env.value}${separator}${formatKeyValue("source", entry.env.source)}`)); if (entry.modelsJson) bits.push(formatKeyValue("models.json", `${entry.modelsJson.value}${separator}${formatKeyValue("source", entry.modelsJson.source)}`)); runtime.log(`- ${theme.heading(entry.provider)} ${bits.join(separator)}`); } if (missingProvidersInUse.length > 0) { runtime.log(""); runtime.log(colorize(rich, theme.heading, "Missing auth")); for (const provider of missingProvidersInUse) { const hint = provider === "anthropic" ? `Run \`claude setup-token\`, then \`${formatCliCommand("openclaw models auth setup-token")}\` or \`${formatCliCommand("openclaw configure")}\`.` : `Run \`${formatCliCommand("openclaw configure")}\` or set an API key env var.`; runtime.log(`- ${theme.heading(provider)} ${hint}`); } } runtime.log(""); runtime.log(colorize(rich, theme.heading, "OAuth/token status")); if (oauthProfiles.length === 0) runtime.log(colorize(rich, theme.muted, "- none")); else { const usageByProvider = /* @__PURE__ */ new Map(); const usageProviders = Array.from(new Set(oauthProfiles.map((profile) => resolveUsageProviderId(profile.provider)).filter((provider) => Boolean(provider)))); if (usageProviders.length > 0) try { const usageSummary = await loadProviderUsageSummary({ providers: usageProviders, agentDir, timeoutMs: 3500 }); for (const snapshot of usageSummary.providers) { const formatted = formatUsageWindowSummary(snapshot, { now: Date.now(), maxWindows: 2, includeResets: true }); if (formatted) usageByProvider.set(snapshot.provider, formatted); } } catch {} const formatStatus = (status) => { if (status === "ok") return colorize(rich, theme.success, "ok"); if (status === "static") return colorize(rich, theme.muted, "static"); if (status === "expiring") return colorize(rich, theme.warn, "expiring"); if (status === "missing") return colorize(rich, theme.warn, "unknown"); return colorize(rich, theme.error, "expired"); }; const profilesByProvider = /* @__PURE__ */ new Map(); for (const profile of oauthProfiles) { const current = profilesByProvider.get(profile.provider); if (current) current.push(profile); else profilesByProvider.set(profile.provider, [profile]); } for (const [provider, profiles] of profilesByProvider) { const usageKey = resolveUsageProviderId(provider); const usage = usageKey ? usageByProvider.get(usageKey) : void 0; const usageSuffix = usage ? colorize(rich, theme.muted, ` usage: ${usage}`) : ""; runtime.log(`- ${colorize(rich, theme.heading, provider)}${usageSuffix}`); for (const profile of profiles) { const labelText = profile.label || profile.profileId; const label = colorize(rich, theme.accent, labelText); const status = formatStatus(profile.status); const expiry = profile.status === "static" ? "" : profile.expiresAt ? ` expires in ${formatRemainingShort(profile.remainingMs)}` : " expires unknown"; runtime.log(` - ${label} ${status}${expiry}`); } } } if (probeSummary) { runtime.log(""); runtime.log(colorize(rich, theme.heading, "Auth probes")); if (probeSummary.results.length === 0) runtime.log(colorize(rich, theme.muted, "- none")); else { const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); const sorted = sortProbeResults(probeSummary.results); const statusColor = (status) => { if (status === "ok") return theme.success; if (status === "rate_limit") return theme.warn; if (status === "timeout" || status === "billing") return theme.warn; if (status === "auth" || status === "format") return theme.error; if (status === "no_model") return theme.muted; return theme.muted; }; const rows = sorted.map((result) => { const status = colorize(rich, statusColor(result.status), result.status); const latency = formatProbeLatency(result.latencyMs); const modelLabel = result.model ?? `${result.provider}/-`; const modeLabel = result.mode ? ` ${colorize(rich, theme.muted, `(${result.mode})`)}` : ""; const profile = `${colorize(rich, theme.accent, result.label)}${modeLabel}`; const detail = result.error?.trim(); const detailLabel = detail ? `\n${colorize(rich, theme.muted, `↳ ${detail}`)}` : ""; const statusLabel = `${status}${colorize(rich, theme.muted, ` · ${latency}`)}${detailLabel}`; return { Model: colorize(rich, theme.heading, modelLabel), Profile: profile, Status: statusLabel }; }); runtime.log(renderTable({ width: tableWidth, columns: [ { key: "Model", header: "Model", minWidth: 18 }, { key: "Profile", header: "Profile", minWidth: 24 }, { key: "Status", header: "Status", minWidth: 12 } ], rows }).trimEnd()); runtime.log(colorize(rich, theme.muted, describeProbeSummary(probeSummary))); } } if (opts.check) runtime.exit(checkStatus); } //#endregion //#region src/agents/model-scan.ts const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"; const DEFAULT_TIMEOUT_MS = 12e3; const DEFAULT_CONCURRENCY = 3; const BASE_IMAGE_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X3mIAAAAASUVORK5CYII="; const TOOL_PING = { name: "ping", description: "Return OK.", parameters: Type.Object({}) }; function normalizeCreatedAtMs(value) { if (typeof value !== "number" || !Number.isFinite(value)) return null; if (value <= 0) return null; if (value > 0xe8d4a51000) return Math.round(value); return Math.round(value * 1e3); } function inferParamBFromIdOrName(text) { const matches = text.toLowerCase().matchAll(/(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g); let best = null; for (const match of matches) { const numRaw = match[1]; if (!numRaw) continue; const value = Number(numRaw); if (!Number.isFinite(value) || value <= 0) continue; if (best === null || value > best) best = value; } return best; } function parseModality(modality) { if (!modality) return ["text"]; return modality.toLowerCase().split(/[^a-z]+/).filter(Boolean).includes("image") ? ["text", "image"] : ["text"]; } function parseNumberString(value) { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value !== "string") return null; const trimmed = value.trim(); if (!trimmed) return null; const num = Number(trimmed); if (!Number.isFinite(num)) return null; return num; } function parseOpenRouterPricing(value) { if (!value || typeof value !== "object") return null; const obj = value; const prompt = parseNumberString(obj.prompt); const completion = parseNumberString(obj.completion); const request = parseNumberString(obj.request) ?? 0; const image = parseNumberString(obj.image) ?? 0; const webSearch = parseNumberString(obj.web_search) ?? 0; const internalReasoning = parseNumberString(obj.internal_reasoning) ?? 0; if (prompt === null || completion === null) return null; return { prompt, completion, request, image, webSearch, internalReasoning }; } function isFreeOpenRouterModel(entry) { if (entry.id.endsWith(":free")) return true; if (!entry.pricing) return false; return entry.pricing.prompt === 0 && entry.pricing.completion === 0; } async function withTimeout(timeoutMs, fn) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fn(controller.signal); } finally { clearTimeout(timer); } } async function fetchOpenRouterModels(fetchImpl) { const res = await fetchImpl(OPENROUTER_MODELS_URL, { headers: { Accept: "application/json" } }); if (!res.ok) throw new Error(`OpenRouter /models failed: HTTP ${res.status}`); const payload = await res.json(); return (Array.isArray(payload.data) ? payload.data : []).map((entry) => { if (!entry || typeof entry !== "object") return null; const obj = entry; const id = typeof obj.id === "string" ? obj.id.trim() : ""; if (!id) return null; const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; const contextLength = typeof obj.context_length === "number" && Number.isFinite(obj.context_length) ? obj.context_length : null; const maxCompletionTokens = typeof obj.max_completion_tokens === "number" && Number.isFinite(obj.max_completion_tokens) ? obj.max_completion_tokens : typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens) ? obj.max_output_tokens : null; const supportedParameters = Array.isArray(obj.supported_parameters) ? obj.supported_parameters.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean) : []; return { id, name, contextLength, maxCompletionTokens, supportedParameters, supportedParametersCount: supportedParameters.length, supportsToolsMeta: supportedParameters.includes("tools"), modality: typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null, inferredParamB: inferParamBFromIdOrName(`${id} ${name}`), createdAtMs: normalizeCreatedAtMs(obj.created_at), pricing: parseOpenRouterPricing(obj.pricing) }; }).filter((entry) => Boolean(entry)); } async function probeTool(model, apiKey, timeoutMs) { const context = { messages: [{ role: "user", content: "Call the ping tool with {} and nothing else.", timestamp: Date.now() }], tools: [TOOL_PING] }; const startedAt = Date.now(); try { if (!(await withTimeout(timeoutMs, (signal) => complete(model, context, { apiKey, maxTokens: 32, temperature: 0, toolChoice: "required", signal }))).content.some((block) => block.type === "toolCall")) return { ok: false, latencyMs: Date.now() - startedAt, error: "No tool call returned" }; return { ok: true, latencyMs: Date.now() - startedAt }; } catch (err) { return { ok: false, latencyMs: Date.now() - startedAt, error: err instanceof Error ? err.message : String(err) }; } } async function probeImage(model, apiKey, timeoutMs) { const context = { messages: [{ role: "user", content: [{ type: "text", text: "Reply with OK." }, { type: "image", data: BASE_IMAGE_PNG, mimeType: "image/png" }], timestamp: Date.now() }] }; const startedAt = Date.now(); try { await withTimeout(timeoutMs, (signal) => complete(model, context, { apiKey, maxTokens: 16, temperature: 0, signal })); return { ok: true, latencyMs: Date.now() - startedAt }; } catch (err) { return { ok: false, latencyMs: Date.now() - startedAt, error: err instanceof Error ? err.message : String(err) }; } } function ensureImageInput(model) { if (model.input.includes("image")) return model; return { ...model, input: Array.from(new Set([...model.input, "image"])) }; } async function mapWithConcurrency(items, concurrency, fn, opts) { const limit = Math.max(1, Math.floor(concurrency)); const results = Array.from({ length: items.length }, () => void 0); let nextIndex = 0; let completed = 0; const worker = async () => { while (true) { const current = nextIndex; nextIndex += 1; if (current >= items.length) return; results[current] = await fn(items[current], current); completed += 1; opts?.onProgress?.(completed, items.length); } }; if (items.length === 0) { opts?.onProgress?.(0, 0); return results; } await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker())); return results; } async function scanOpenRouterModels(options = {}) { const fetchImpl = options.fetchImpl ?? fetch; const probe = options.probe ?? true; const apiKey = options.apiKey?.trim() || getEnvApiKey("openrouter") || ""; if (probe && !apiKey) throw new Error("Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan."); const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS)); const concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY)); const minParamB = Math.max(0, Math.floor(options.minParamB ?? 0)); const maxAgeDays = Math.max(0, Math.floor(options.maxAgeDays ?? 0)); const providerFilter = options.providerFilter?.trim().toLowerCase() ?? ""; const catalog = await fetchOpenRouterModels(fetchImpl); const now = Date.now(); const filtered = catalog.filter((entry) => { if (!isFreeOpenRouterModel(entry)) return false; if (providerFilter) { if ((entry.id.split("/")[0]?.toLowerCase() ?? "") !== providerFilter) return false; } if (minParamB > 0) { if ((entry.inferredParamB ?? 0) < minParamB) return false; } if (maxAgeDays > 0 && entry.createdAtMs) { if ((now - entry.createdAtMs) / (1440 * 60 * 1e3) > maxAgeDays) return false; } return true; }); const baseModel = getModel("openrouter", "openrouter/auto"); options.onProgress?.({ phase: "probe", completed: 0, total: filtered.length }); return mapWithConcurrency(filtered, concurrency, async (entry) => { const isFree = isFreeOpenRouterModel(entry); if (!probe) return { id: entry.id, name: entry.name, provider: "openrouter", modelRef: `openrouter/${entry.id}`, contextLength: entry.contextLength, maxCompletionTokens: entry.maxCompletionTokens, supportedParametersCount: entry.supportedParametersCount, supportsToolsMeta: entry.supportsToolsMeta, modality: entry.modality, inferredParamB: entry.inferredParamB, createdAtMs: entry.createdAtMs, pricing: entry.pricing, isFree, tool: { ok: false, latencyMs: null, skipped: true }, image: { ok: false, latencyMs: null, skipped: true } }; const model = { ...baseModel, id: entry.id, name: entry.name || entry.id, contextWindow: entry.contextLength ?? baseModel.contextWindow, maxTokens: entry.maxCompletionTokens ?? baseModel.maxTokens, input: parseModality(entry.modality), reasoning: baseModel.reasoning }; const toolResult = await probeTool(model, apiKey, timeoutMs); const imageResult = model.input.includes("image") ? await probeImage(ensureImageInput(model), apiKey, timeoutMs) : { ok: false, latencyMs: null, skipped: true }; return { id: entry.id, name: entry.name, provider: "openrouter", modelRef: `openrouter/${entry.id}`, contextLength: entry.contextLength, maxCompletionTokens: entry.maxCompletionTokens, supportedParametersCount: entry.supportedParametersCount, supportsToolsMeta: entry.supportsToolsMeta, modality: entry.modality, inferredParamB: entry.inferredParamB, createdAtMs: entry.createdAtMs, pricing: entry.pricing, isFree, tool: toolResult, image: imageResult }; }, { onProgress: (completed, total) => options.onProgress?.({ phase: "probe", completed, total }) }); } //#endregion //#region src/commands/models/scan.ts const MODEL_PAD = 42; const CTX_PAD = 8; const multiselect$1 = (params) => multiselect({ ...params, message: stylePromptMessage(params.message), options: params.options.map((opt) => opt.hint === void 0 ? opt : { ...opt, hint: stylePromptHint(opt.hint) }) }); const pad = (value, size) => value.padEnd(size); const truncate = (value, max) => { if (value.length <= max) return value; if (max <= 3) return value.slice(0, max); return `${value.slice(0, max - 3)}...`; }; function sortScanResults(results) { return results.slice().toSorted((a, b) => { const aImage = a.image.ok ? 1 : 0; const bImage = b.image.ok ? 1 : 0; if (aImage !== bImage) return bImage - aImage; const aToolLatency = a.tool.latencyMs ?? Number.POSITIVE_INFINITY; const bToolLatency = b.tool.latencyMs ?? Number.POSITIVE_INFINITY; if (aToolLatency !== bToolLatency) return aToolLatency - bToolLatency; const aCtx = a.contextLength ?? 0; const bCtx = b.contextLength ?? 0; if (aCtx !== bCtx) return bCtx - aCtx; const aParams = a.inferredParamB ?? 0; const bParams = b.inferredParamB ?? 0; if (aParams !== bParams) return bParams - aParams; return a.modelRef.localeCompare(b.modelRef); }); } function sortImageResults(results) { return results.slice().toSorted((a, b) => { const aLatency = a.image.latencyMs ?? Number.POSITIVE_INFINITY; const bLatency = b.image.latencyMs ?? Number.POSITIVE_INFINITY; if (aLatency !== bLatency) return aLatency - bLatency; const aCtx = a.contextLength ?? 0; const bCtx = b.contextLength ?? 0; if (aCtx !== bCtx) return bCtx - aCtx; const aParams = a.inferredParamB ?? 0; const bParams = b.inferredParamB ?? 0; if (aParams !== bParams) return bParams - aParams; return a.modelRef.localeCompare(b.modelRef); }); } function buildScanHint(result) { return [ result.tool.ok ? `tool ${formatMs(result.tool.latencyMs)}` : "tool fail", result.image.skipped ? "img skip" : result.image.ok ? `img ${formatMs(result.image.latencyMs)}` : "img fail", result.contextLength ? `ctx ${formatTokenK(result.contextLength)}` : "ctx ?", result.inferredParamB ? `${result.inferredParamB}b` : null ].filter(Boolean).join(" | "); } function printScanSummary(results, runtime) { const toolOk = results.filter((r) => r.tool.ok); const imageOk = results.filter((r) => r.image.ok); const toolImageOk = results.filter((r) => r.tool.ok && r.image.ok); const imageOnly = imageOk.filter((r) => !r.tool.ok); runtime.log(`Scan results: tested ${results.length}, tool ok ${toolOk.length}, image ok ${imageOk.length}, tool+image ok ${toolImageOk.length}, image only ${imageOnly.length}`); } function printScanTable(results, runtime) { const header = [ pad("Model", MODEL_PAD), pad("Tool", 10), pad("Image", 10), pad("Ctx", CTX_PAD), pad("Params", 8), "Notes" ].join(" "); runtime.log(header); for (const entry of results) { const modelLabel = pad(truncate(entry.modelRef, MODEL_PAD), MODEL_PAD); const toolLabel = pad(entry.tool.ok ? formatMs(entry.tool.latencyMs) : "fail", 10); const imageLabel = pad(entry.image.ok ? formatMs(entry.image.latencyMs) : entry.image.skipped ? "skip" : "fail", 10); const ctxLabel = pad(formatTokenK(entry.contextLength), CTX_PAD); const paramsLabel = pad(entry.inferredParamB ? `${entry.inferredParamB}b` : "-", 8); const notes = entry.modality ? `modality:${entry.modality}` : ""; runtime.log([ modelLabel, toolLabel, imageLabel, ctxLabel, paramsLabel, notes ].join(" ")); } } async function modelsScanCommand(opts, runtime) { const minParams = opts.minParams ? Number(opts.minParams) : void 0; if (minParams !== void 0 && (!Number.isFinite(minParams) || minParams < 0)) throw new Error("--min-params must be >= 0"); const maxAgeDays = opts.maxAgeDays ? Number(opts.maxAgeDays) : void 0; if (maxAgeDays !== void 0 && (!Number.isFinite(maxAgeDays) || maxAgeDays < 0)) throw new Error("--max-age-days must be >= 0"); const maxCandidates = opts.maxCandidates ? Number(opts.maxCandidates) : 6; if (!Number.isFinite(maxCandidates) || maxCandidates <= 0) throw new Error("--max-candidates must be > 0"); const timeout = opts.timeout ? Number(opts.timeout) : void 0; if (timeout !== void 0 && (!Number.isFinite(timeout) || timeout <= 0)) throw new Error("--timeout must be > 0"); const concurrency = opts.concurrency ? Number(opts.concurrency) : void 0; if (concurrency !== void 0 && (!Number.isFinite(concurrency) || concurrency <= 0)) throw new Error("--concurrency must be > 0"); const cfg = loadConfig(); const probe = opts.probe ?? true; let storedKey; if (probe) try { storedKey = (await resolveApiKeyForProvider({ provider: "openrouter", cfg })).apiKey; } catch { storedKey = void 0; } const results = await withProgressTotals({ label: "Scanning OpenRouter models...", indeterminate: false, enabled: opts.json !== true }, async (update) => await scanOpenRouterModels({ apiKey: storedKey ?? void 0, minParamB: minParams, maxAgeDays, providerFilter: opts.provider, timeoutMs: timeout, concurrency, probe, onProgress: ({ phase, completed, total }) => { if (phase !== "probe") return; update({ completed, total, label: `${probe ? "Probing models" : "Scanning models"} (${completed}/${total})` }); } })); if (!probe) { if (!opts.json) { runtime.log(`Found ${results.length} OpenRouter free models (metadata only; pass --probe to test tools/images).`); printScanTable(sortScanResults(results), runtime); } else runtime.log(JSON.stringify(results, null, 2)); return; } const toolOk = results.filter((entry) => entry.tool.ok); if (toolOk.length === 0) throw new Error("No tool-capable OpenRouter free models found."); const sorted = sortScanResults(results); const toolSorted = sortScanResults(toolOk); const imageSorted = sortImageResults(results.filter((entry) => entry.image.ok)); const imagePreferred = toolSorted.filter((entry) => entry.image.ok); const preselected = (imagePreferred.length > 0 ? imagePreferred : toolSorted).slice(0, Math.floor(maxCandidates)).map((entry) => entry.modelRef); const imagePreselected = imageSorted.slice(0, Math.floor(maxCandidates)).map((entry) => entry.modelRef); if (!opts.json) { printScanSummary(results, runtime); printScanTable(sorted, runtime); } const noInput = opts.input === false; const canPrompt = process.stdin.isTTY && !opts.yes && !noInput && !opts.json; let selected = preselected; let selectedImages = imagePreselected; if (canPrompt) { const selection = await multiselect$1({ message: "Select fallback models (ordered)", options: toolSorted.map((entry) => ({ value: entry.modelRef, label: entry.modelRef, hint: buildScanHint(entry) })), initialValues: preselected }); if (isCancel(selection)) { cancel(stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled."); runtime.exit(0); } selected = selection; if (imageSorted.length > 0) { const imageSelection = await multiselect$1({ message: "Select image fallback models (ordered)", options: imageSorted.map((entry) => ({ value: entry.modelRef, label: entry.modelRef, hint: buildScanHint(entry) })), initialValues: imagePreselected }); if (isCancel(imageSelection)) { cancel(stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled."); runtime.exit(0); } selectedImages = imageSelection; } } else if (!process.stdin.isTTY && !opts.yes && !noInput && !opts.json) throw new Error("Non-interactive scan: pass --yes to apply defaults."); if (selected.length === 0) throw new Error("No models selected for fallbacks."); if (opts.setImage && selectedImages.length === 0) throw new Error("No image-capable models selected for image model."); await updateConfig((cfg) => { const nextModels = { ...cfg.agents?.defaults?.models }; for (const entry of selected) if (!nextModels[entry]) nextModels[entry] = {}; for (const entry of selectedImages) if (!nextModels[entry]) nextModels[entry] = {}; const existingImageModel = cfg.agents?.defaults?.imageModel; const nextImageModel = selectedImages.length > 0 ? { ...existingImageModel?.primary ? { primary: existingImageModel.primary } : void 0, fallbacks: selectedImages, ...opts.setImage ? { primary: selectedImages[0] } : {} } : cfg.agents?.defaults?.imageModel; const existingModel = cfg.agents?.defaults?.model; const defaults = { ...cfg.agents?.defaults, model: { ...existingModel?.primary ? { primary: existingModel.primary } : void 0, fallbacks: selected, ...opts.setDefault ? { primary: selected[0] } : {} }, ...nextImageModel ? { imageModel: nextImageModel } : {}, models: nextModels }; return { ...cfg, agents: { ...cfg.agents, defaults } }; }); if (opts.json) { runtime.log(JSON.stringify({ selected, selectedImages, setDefault: Boolean(opts.setDefault), setImage: Boolean(opts.setImage), results, warnings: [] }, null, 2)); return; } logConfigUpdated(runtime); runtime.log(`Fallbacks: ${selected.join(", ")}`); if (selectedImages.length > 0) runtime.log(`Image fallbacks: ${selectedImages.join(", ")}`); if (opts.setDefault) runtime.log(`Default model: ${selected[0]}`); if (opts.setImage && selectedImages.length > 0) runtime.log(`Image model: ${selectedImages[0]}`); } //#endregion //#region src/commands/models/set.ts async function modelsSetCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const key = `${resolved.provider}/${resolved.model}`; const nextModels = { ...cfg.agents?.defaults?.models }; if (!nextModels[key]) nextModels[key] = {}; const existingModel = cfg.agents?.defaults?.model; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, model: { ...existingModel?.fallbacks ? { fallbacks: existingModel.fallbacks } : void 0, primary: key }, models: nextModels } } }; }); logConfigUpdated(runtime); runtime.log(`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`); } //#endregion //#region src/commands/models/set-image.ts async function modelsSetImageCommand(modelRaw, runtime) { const updated = await updateConfig((cfg) => { const resolved = resolveModelTarget({ raw: modelRaw, cfg }); const key = `${resolved.provider}/${resolved.model}`; const nextModels = { ...cfg.agents?.defaults?.models }; if (!nextModels[key]) nextModels[key] = {}; const existingModel = cfg.agents?.defaults?.imageModel; return { ...cfg, agents: { ...cfg.agents, defaults: { ...cfg.agents?.defaults, imageModel: { ...existingModel?.fallbacks ? { fallbacks: existingModel.fallbacks } : void 0, primary: key }, models: nextModels } } }; }); logConfigUpdated(runtime); runtime.log(`Image model: ${updated.agents?.defaults?.imageModel?.primary ?? modelRaw}`); } //#endregion //#region src/cli/models-cli.ts function runModelsCommand(action) { return runCommandWithRuntime(defaultRuntime, action); } function registerModelsCli(program) { const models = program.command("models").description("Model discovery, scanning, and configuration").option("--status-json", "Output JSON (alias for `models status --json`)", false).option("--status-plain", "Plain output (alias for `models status --plain`)", false).option("--agent ", "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)").addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/models", "docs.openclaw.ai/cli/models")}\n`); models.command("list").description("List models (configured by default)").option("--all", "Show full model catalog", false).option("--local", "Filter to local models", false).option("--provider ", "Filter by provider").option("--json", "Output JSON", false).option("--plain", "Plain line output", false).action(async (opts) => { await runModelsCommand(async () => { await modelsListCommand(opts, defaultRuntime); }); }); models.command("status").description("Show configured model state").option("--json", "Output JSON", false).option("--plain", "Plain output", false).option("--check", "Exit non-zero if auth is expiring/expired (1=expired/missing, 2=expiring)", false).option("--probe", "Probe configured provider auth (live)", false).option("--probe-provider ", "Only probe a single provider").option("--probe-profile ", "Only probe specific auth profile ids (repeat or comma-separated)", (value, previous) => { const next = Array.isArray(previous) ? previous : previous ? [previous] : []; next.push(value); return next; }).option("--probe-timeout ", "Per-probe timeout in ms").option("--probe-concurrency ", "Concurrent probes").option("--probe-max-tokens ", "Probe max tokens (best-effort)").option("--agent ", "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)").action(async (opts, command) => { const agent = resolveOptionFromCommand(command, "agent") ?? opts.agent; await runModelsCommand(async () => { await modelsStatusCommand({ json: Boolean(opts.json), plain: Boolean(opts.plain), check: Boolean(opts.check), probe: Boolean(opts.probe), probeProvider: opts.probeProvider, probeProfile: opts.probeProfile, probeTimeout: opts.probeTimeout, probeConcurrency: opts.probeConcurrency, probeMaxTokens: opts.probeMaxTokens, agent }, defaultRuntime); }); }); models.command("set").description("Set the default model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsSetCommand(model, defaultRuntime); }); }); models.command("set-image").description("Set the image model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsSetImageCommand(model, defaultRuntime); }); }); const aliases = models.command("aliases").description("Manage model aliases"); aliases.command("list").description("List model aliases").option("--json", "Output JSON", false).option("--plain", "Plain output", false).action(async (opts) => { await runModelsCommand(async () => { await modelsAliasesListCommand(opts, defaultRuntime); }); }); aliases.command("add").description("Add or update a model alias").argument("", "Alias name").argument("", "Model id or alias").action(async (alias, model) => { await runModelsCommand(async () => { await modelsAliasesAddCommand(alias, model, defaultRuntime); }); }); aliases.command("remove").description("Remove a model alias").argument("", "Alias name").action(async (alias) => { await runModelsCommand(async () => { await modelsAliasesRemoveCommand(alias, defaultRuntime); }); }); const fallbacks = models.command("fallbacks").description("Manage model fallback list"); fallbacks.command("list").description("List fallback models").option("--json", "Output JSON", false).option("--plain", "Plain output", false).action(async (opts) => { await runModelsCommand(async () => { await modelsFallbacksListCommand(opts, defaultRuntime); }); }); fallbacks.command("add").description("Add a fallback model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsFallbacksAddCommand(model, defaultRuntime); }); }); fallbacks.command("remove").description("Remove a fallback model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsFallbacksRemoveCommand(model, defaultRuntime); }); }); fallbacks.command("clear").description("Clear all fallback models").action(async () => { await runModelsCommand(async () => { await modelsFallbacksClearCommand(defaultRuntime); }); }); const imageFallbacks = models.command("image-fallbacks").description("Manage image model fallback list"); imageFallbacks.command("list").description("List image fallback models").option("--json", "Output JSON", false).option("--plain", "Plain output", false).action(async (opts) => { await runModelsCommand(async () => { await modelsImageFallbacksListCommand(opts, defaultRuntime); }); }); imageFallbacks.command("add").description("Add an image fallback model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsImageFallbacksAddCommand(model, defaultRuntime); }); }); imageFallbacks.command("remove").description("Remove an image fallback model").argument("", "Model id or alias").action(async (model) => { await runModelsCommand(async () => { await modelsImageFallbacksRemoveCommand(model, defaultRuntime); }); }); imageFallbacks.command("clear").description("Clear all image fallback models").action(async () => { await runModelsCommand(async () => { await modelsImageFallbacksClearCommand(defaultRuntime); }); }); models.command("scan").description("Scan OpenRouter free models for tools + images").option("--min-params ", "Minimum parameter size (billions)").option("--max-age-days ", "Skip models older than N days").option("--provider ", "Filter by provider prefix").option("--max-candidates ", "Max fallback candidates", "6").option("--timeout ", "Per-probe timeout in ms").option("--concurrency ", "Probe concurrency").option("--no-probe", "Skip live probes; list free candidates only").option("--yes", "Accept defaults without prompting", false).option("--no-input", "Disable prompts (use defaults)").option("--set-default", "Set agents.defaults.model to the first selection", false).option("--set-image", "Set agents.defaults.imageModel to the first image selection", false).option("--json", "Output JSON", false).action(async (opts) => { await runModelsCommand(async () => { await modelsScanCommand(opts, defaultRuntime); }); }); models.action(async (opts) => { await runModelsCommand(async () => { await modelsStatusCommand({ json: Boolean(opts?.statusJson), plain: Boolean(opts?.statusPlain), agent: opts?.agent }, defaultRuntime); }); }); const auth = models.command("auth").description("Manage model auth profiles"); auth.option("--agent ", "Agent id for auth order get/set/clear"); auth.action(() => { auth.help(); }); auth.command("add").description("Interactive auth helper (setup-token or paste token)").action(async () => { await runModelsCommand(async () => { await modelsAuthAddCommand({}, defaultRuntime); }); }); auth.command("login").description("Run a provider plugin auth flow (OAuth/API key)").option("--provider ", "Provider id registered by a plugin").option("--method ", "Provider auth method id").option("--set-default", "Apply the provider's default model recommendation", false).action(async (opts) => { await runModelsCommand(async () => { await modelsAuthLoginCommand({ provider: opts.provider, method: opts.method, setDefault: Boolean(opts.setDefault) }, defaultRuntime); }); }); auth.command("setup-token").description("Run a provider CLI to create/sync a token (TTY required)").option("--provider ", "Provider id (default: anthropic)").option("--yes", "Skip confirmation", false).action(async (opts) => { await runModelsCommand(async () => { await modelsAuthSetupTokenCommand({ provider: opts.provider, yes: Boolean(opts.yes) }, defaultRuntime); }); }); auth.command("paste-token").description("Paste a token into auth-profiles.json and update config").requiredOption("--provider ", "Provider id (e.g. anthropic)").option("--profile-id ", "Auth profile id (default: :manual)").option("--expires-in ", "Optional expiry duration (e.g. 365d, 12h). Stored as absolute expiresAt.").action(async (opts) => { await runModelsCommand(async () => { await modelsAuthPasteTokenCommand({ provider: opts.provider, profileId: opts.profileId, expiresIn: opts.expiresIn }, defaultRuntime); }); }); auth.command("login-github-copilot").description("Login to GitHub Copilot via GitHub device flow (TTY required)").option("--profile-id ", "Auth profile id (default: github-copilot:github)").option("--yes", "Overwrite existing profile without prompting", false).action(async (opts) => { await runModelsCommand(async () => { await githubCopilotLoginCommand({ profileId: opts.profileId, yes: Boolean(opts.yes) }, defaultRuntime); }); }); const order = auth.command("order").description("Manage per-agent auth profile order overrides"); order.command("get").description("Show per-agent auth order override (from auth-profiles.json)").requiredOption("--provider ", "Provider id (e.g. anthropic)").option("--agent ", "Agent id (default: configured default agent)").option("--json", "Output JSON", false).action(async (opts, command) => { const agent = resolveOptionFromCommand(command, "agent") ?? opts.agent; await runModelsCommand(async () => { await modelsAuthOrderGetCommand({ provider: opts.provider, agent, json: Boolean(opts.json) }, defaultRuntime); }); }); order.command("set").description("Set per-agent auth order override (locks rotation to this list)").requiredOption("--provider ", "Provider id (e.g. anthropic)").option("--agent ", "Agent id (default: configured default agent)").argument("", "Auth profile ids (e.g. anthropic:default)").action(async (profileIds, opts, command) => { const agent = resolveOptionFromCommand(command, "agent") ?? opts.agent; await runModelsCommand(async () => { await modelsAuthOrderSetCommand({ provider: opts.provider, agent, order: profileIds }, defaultRuntime); }); }); order.command("clear").description("Clear per-agent auth order override (fall back to config/round-robin)").requiredOption("--provider ", "Provider id (e.g. anthropic)").option("--agent ", "Agent id (default: configured default agent)").action(async (opts, command) => { const agent = resolveOptionFromCommand(command, "agent") ?? opts.agent; await runModelsCommand(async () => { await modelsAuthOrderClearCommand({ provider: opts.provider, agent }, defaultRuntime); }); }); } //#endregion export { registerModelsCli };