diff --git a/src/commands/models/fallbacks-shared.ts b/src/commands/models/fallbacks-shared.ts new file mode 100644 index 0000000000..1070249ddd --- /dev/null +++ b/src/commands/models/fallbacks-shared.ts @@ -0,0 +1,158 @@ +import type { OpenClawConfig } from "../../config/config.js"; +import type { RuntimeEnv } from "../../runtime.js"; +import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js"; +import { loadConfig } from "../../config/config.js"; +import { logConfigUpdated } from "../../config/logging.js"; +import { + DEFAULT_PROVIDER, + ensureFlagCompatibility, + mergePrimaryFallbackConfig, + type PrimaryFallbackConfig, + modelKey, + resolveModelTarget, + resolveModelKeysFromEntries, + updateConfig, +} from "./shared.js"; + +type DefaultsFallbackKey = "model" | "imageModel"; + +function getFallbacks(cfg: OpenClawConfig, key: DefaultsFallbackKey): string[] { + const entry = cfg.agents?.defaults?.[key] as unknown as PrimaryFallbackConfig | undefined; + return entry?.fallbacks ?? []; +} + +function patchDefaultsFallbacks( + cfg: OpenClawConfig, + params: { key: DefaultsFallbackKey; fallbacks: string[]; models?: Record }, +): OpenClawConfig { + const existing = cfg.agents?.defaults?.[params.key] as unknown as + | PrimaryFallbackConfig + | undefined; + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + [params.key]: mergePrimaryFallbackConfig(existing, { fallbacks: params.fallbacks }), + ...(params.models ? { models: params.models as never } : undefined), + }, + }, + }; +} + +export async function listFallbacksCommand( + params: { label: string; key: DefaultsFallbackKey }, + opts: { json?: boolean; plain?: boolean }, + runtime: RuntimeEnv, +) { + ensureFlagCompatibility(opts); + const cfg = loadConfig(); + const fallbacks = getFallbacks(cfg, params.key); + + 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(`${params.label} (${fallbacks.length}):`); + if (fallbacks.length === 0) { + runtime.log("- none"); + return; + } + for (const entry of fallbacks) { + runtime.log(`- ${entry}`); + } +} + +export async function addFallbackCommand( + params: { + label: string; + key: DefaultsFallbackKey; + logPrefix: string; + }, + modelRaw: string, + runtime: RuntimeEnv, +) { + const updated = await updateConfig((cfg) => { + const resolved = resolveModelTarget({ raw: modelRaw, cfg }); + const targetKey = modelKey(resolved.provider, resolved.model); + const nextModels = { ...cfg.agents?.defaults?.models } as Record; + if (!nextModels[targetKey]) { + nextModels[targetKey] = {}; + } + const existing = getFallbacks(cfg, params.key); + const existingKeys = resolveModelKeysFromEntries({ cfg, entries: existing }); + if (existingKeys.includes(targetKey)) { + return cfg; + } + + return patchDefaultsFallbacks(cfg, { + key: params.key, + fallbacks: [...existing, targetKey], + models: nextModels, + }); + }); + + logConfigUpdated(runtime); + runtime.log(`${params.logPrefix}: ${getFallbacks(updated, params.key).join(", ")}`); +} + +export async function removeFallbackCommand( + params: { + label: string; + key: DefaultsFallbackKey; + notFoundLabel: string; + logPrefix: string; + }, + modelRaw: string, + runtime: RuntimeEnv, +) { + 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 = getFallbacks(cfg, params.key); + 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(`${params.notFoundLabel} not found: ${targetKey}`); + } + + return patchDefaultsFallbacks(cfg, { key: params.key, fallbacks: filtered }); + }); + + logConfigUpdated(runtime); + runtime.log(`${params.logPrefix}: ${getFallbacks(updated, params.key).join(", ")}`); +} + +export async function clearFallbacksCommand( + params: { key: DefaultsFallbackKey; clearedMessage: string }, + runtime: RuntimeEnv, +) { + await updateConfig((cfg) => { + return patchDefaultsFallbacks(cfg, { key: params.key, fallbacks: [] }); + }); + + logConfigUpdated(runtime); + runtime.log(params.clearedMessage); +} diff --git a/src/commands/models/fallbacks.ts b/src/commands/models/fallbacks.ts index a3e57390c6..f588dbc245 100644 --- a/src/commands/models/fallbacks.ts +++ b/src/commands/models/fallbacks.ts @@ -1,143 +1,42 @@ import type { RuntimeEnv } from "../../runtime.js"; -import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js"; -import { loadConfig } from "../../config/config.js"; -import { logConfigUpdated } from "../../config/logging.js"; import { - DEFAULT_PROVIDER, - ensureFlagCompatibility, - mergePrimaryFallbackConfig, - type PrimaryFallbackConfig, - modelKey, - resolveModelTarget, - resolveModelKeysFromEntries, - updateConfig, -} from "./shared.js"; + addFallbackCommand, + clearFallbacksCommand, + listFallbacksCommand, + removeFallbackCommand, +} from "./fallbacks-shared.js"; export async function modelsFallbacksListCommand( opts: { json?: boolean; plain?: boolean }, runtime: RuntimeEnv, ) { - ensureFlagCompatibility(opts); - const cfg = loadConfig(); - const fallbacks = cfg.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}`); - } + return await listFallbacksCommand({ label: "Fallbacks", key: "model" }, opts, runtime); } export async function modelsFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) { - 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 existing = cfg.agents?.defaults?.model?.fallbacks ?? []; - const existingKeys = resolveModelKeysFromEntries({ cfg, entries: existing }); - - if (existingKeys.includes(targetKey)) { - return cfg; - } - - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - model: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.model as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: [...existing, targetKey] }, - ), - models: nextModels, - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`); + return await addFallbackCommand( + { label: "Fallbacks", key: "model", logPrefix: "Fallbacks" }, + modelRaw, + runtime, + ); } export async function modelsFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) { - 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}`); - } - - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - model: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.model as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: filtered }, - ), - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`); + return await removeFallbackCommand( + { + label: "Fallbacks", + key: "model", + notFoundLabel: "Fallback", + logPrefix: "Fallbacks", + }, + modelRaw, + runtime, + ); } export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) { - await updateConfig((cfg) => { - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - model: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.model as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: [] }, - ), - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log("Fallback list cleared."); + return await clearFallbacksCommand( + { key: "model", clearedMessage: "Fallback list cleared." }, + runtime, + ); } diff --git a/src/commands/models/image-fallbacks.ts b/src/commands/models/image-fallbacks.ts index 43d1f51c60..99ae401972 100644 --- a/src/commands/models/image-fallbacks.ts +++ b/src/commands/models/image-fallbacks.ts @@ -1,147 +1,42 @@ import type { RuntimeEnv } from "../../runtime.js"; -import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js"; -import { loadConfig } from "../../config/config.js"; -import { logConfigUpdated } from "../../config/logging.js"; import { - DEFAULT_PROVIDER, - ensureFlagCompatibility, - mergePrimaryFallbackConfig, - type PrimaryFallbackConfig, - modelKey, - resolveModelTarget, - resolveModelKeysFromEntries, - updateConfig, -} from "./shared.js"; + addFallbackCommand, + clearFallbacksCommand, + listFallbacksCommand, + removeFallbackCommand, +} from "./fallbacks-shared.js"; export async function modelsImageFallbacksListCommand( opts: { json?: boolean; plain?: boolean }, runtime: RuntimeEnv, ) { - ensureFlagCompatibility(opts); - const cfg = loadConfig(); - const fallbacks = cfg.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}`); - } + return await listFallbacksCommand({ label: "Image fallbacks", key: "imageModel" }, opts, runtime); } export async function modelsImageFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) { - 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 existing = cfg.agents?.defaults?.imageModel?.fallbacks ?? []; - const existingKeys = resolveModelKeysFromEntries({ cfg, entries: existing }); - - if (existingKeys.includes(targetKey)) { - return cfg; - } - - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - imageModel: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.imageModel as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: [...existing, targetKey] }, - ), - models: nextModels, - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log( - `Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`, + return await addFallbackCommand( + { label: "Image fallbacks", key: "imageModel", logPrefix: "Image fallbacks" }, + modelRaw, + runtime, ); } export async function modelsImageFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) { - 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}`); - } - - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - imageModel: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.imageModel as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: filtered }, - ), - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log( - `Image fallbacks: ${(updated.agents?.defaults?.imageModel?.fallbacks ?? []).join(", ")}`, + return await removeFallbackCommand( + { + label: "Image fallbacks", + key: "imageModel", + notFoundLabel: "Image fallback", + logPrefix: "Image fallbacks", + }, + modelRaw, + runtime, ); } export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) { - await updateConfig((cfg) => { - return { - ...cfg, - agents: { - ...cfg.agents, - defaults: { - ...cfg.agents?.defaults, - imageModel: mergePrimaryFallbackConfig( - cfg.agents?.defaults?.imageModel as unknown as PrimaryFallbackConfig | undefined, - { fallbacks: [] }, - ), - }, - }, - }; - }); - - logConfigUpdated(runtime); - runtime.log("Image fallback list cleared."); + return await clearFallbacksCommand( + { key: "imageModel", clearedMessage: "Image fallback list cleared." }, + runtime, + ); }