refactor: unify model-input normalization usage

This commit is contained in:
Gustavo Madeira Santana
2026-02-23 02:58:55 -05:00
parent 5e4a67ef78
commit addba9bd8b
12 changed files with 61 additions and 96 deletions

View File

@@ -718,9 +718,14 @@ Time format in system prompt. Default: `auto` (OS preference).
}
```
- `model`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
- String form sets only the primary model.
- Object form sets primary plus ordered failover models.
- `imageModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
- Only used if the selected/default model cannot accept image input.
- `model.primary`: format `provider/model` (e.g. `anthropic/claude-opus-4-6`). If you omit the provider, OpenClaw assumes `anthropic` (deprecated).
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific: `temperature`, `maxTokens`).
- `imageModel`: only used if the primary model lacks image input.
- Config writers that mutate these fields (for example `/models set`, `/models set-image`, and fallback add/remove commands) save canonical object form and preserve existing fallback lists when possible.
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 1.
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):

View File

@@ -1,5 +1,6 @@
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import { resolveStateDir } from "../config/paths.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
@@ -205,10 +206,7 @@ export function resolveEffectiveModelFallbacks(params: {
if (!params.hasSessionModelOverride) {
return agentFallbacksOverride;
}
const defaultFallbacks =
typeof params.cfg.agents?.defaults?.model === "object"
? (params.cfg.agents.defaults.model.fallbacks ?? [])
: [];
const defaultFallbacks = resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
return agentFallbacksOverride ?? defaultFallbacks;
}

View File

@@ -1,4 +1,8 @@
import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import {
ensureAuthProfileStore,
getSoonestCooldownExpiry,
@@ -151,26 +155,13 @@ function resolveImageFallbackCandidates(params: {
if (params.modelOverride?.trim()) {
addRaw(params.modelOverride, false);
} else {
const imageModel = params.cfg?.agents?.defaults?.imageModel as
| { primary?: string }
| string
| undefined;
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
const primary = resolveAgentModelPrimaryValue(params.cfg?.agents?.defaults?.imageModel);
if (primary?.trim()) {
addRaw(primary, false);
}
}
const imageFallbacks = (() => {
const imageModel = params.cfg?.agents?.defaults?.imageModel as
| { fallbacks?: string[] }
| string
| undefined;
if (imageModel && typeof imageModel === "object") {
return imageModel.fallbacks ?? [];
}
return [];
})();
const imageFallbacks = resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.imageModel);
for (const raw of imageFallbacks) {
addRaw(raw, true);
@@ -220,14 +211,7 @@ function resolveFallbackCandidates(params: {
if (!sameModelCandidate(normalizedPrimary, configuredPrimary)) {
return []; // Override model failed → go straight to configured default
}
const model = params.cfg?.agents?.defaults?.model as
| { fallbacks?: string[] }
| string
| undefined;
if (model && typeof model === "object") {
return model.fallbacks ?? [];
}
return [];
return resolveAgentModelFallbackValues(params.cfg?.agents?.defaults?.model);
})();
for (const raw of modelFallbacks) {

View File

@@ -1,5 +1,5 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { resolveAgentModelPrimaryValue, toAgentModelListLike } from "../config/model-input.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
@@ -309,9 +309,7 @@ export function resolveDefaultModelForAgent(params: {
defaults: {
...params.cfg.agents?.defaults,
model: {
...(typeof params.cfg.agents?.defaults?.model === "object"
? params.cfg.agents.defaults.model
: undefined),
...toAgentModelListLike(params.cfg.agents?.defaults?.model),
primary: agentModelOverride,
},
},

View File

@@ -1,5 +1,9 @@
import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import { extractAssistantText } from "../pi-embedded-utils.js";
export type ImageModelConfig = { primary?: string; fallbacks?: string[] };
@@ -51,12 +55,8 @@ export function coerceImageAssistantText(params: {
}
export function coerceImageModelConfig(cfg?: OpenClawConfig): ImageModelConfig {
const imageModel = cfg?.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
const fallbacks = typeof imageModel === "object" ? (imageModel?.fallbacks ?? []) : [];
const primary = resolveAgentModelPrimaryValue(cfg?.agents?.defaults?.imageModel);
const fallbacks = resolveAgentModelFallbackValues(cfg?.agents?.defaults?.imageModel);
return {
...(primary?.trim() ? { primary: primary.trim() } : {}),
...(fallbacks.length > 0 ? { fallbacks } : {}),

View File

@@ -1,3 +1,4 @@
import { toAgentModelListLike } from "../config/model-input.js";
import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyAuthProfileConfig } from "./onboard-auth.js";
@@ -49,9 +50,7 @@ export async function applyAuthChoiceGitHubCopilot(
defaults: {
...nextConfig.agents?.defaults,
model: {
...(typeof nextConfig.agents?.defaults?.model === "object"
? nextConfig.agents.defaults.model
: undefined),
...toAgentModelListLike(nextConfig.agents?.defaults?.model),
primary: model,
},
},

View File

@@ -10,6 +10,7 @@ import {
resolveConfiguredModelRef,
} from "../agents/model-selection.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
import { formatTokenK } from "./models/shared.js";
import { OPENAI_CODEX_DEFAULT_MODEL } from "./openai-codex-model-default.js";
@@ -77,11 +78,7 @@ function createProviderAuthChecker(params: {
}
function resolveConfiguredModelRaw(cfg: OpenClawConfig): string {
const raw = cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
if (typeof raw === "string") {
return raw.trim();
}
return raw?.primary?.trim() ?? "";
return resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ?? "";
}
function resolveConfiguredModelKeys(cfg: OpenClawConfig): string[] {

View File

@@ -5,6 +5,10 @@ import {
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import type { ConfiguredEntry } from "./list.types.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER, modelKey } from "./shared.js";
@@ -37,16 +41,9 @@ export function resolveConfiguredEntries(cfg: OpenClawConfig) {
addEntry(resolvedDefault, "default");
const modelConfig = cfg.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
const imageModelConfig = cfg.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
const modelFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const imageFallbacks =
typeof imageModelConfig === "object" ? (imageModelConfig?.fallbacks ?? []) : [];
const imagePrimary = imageModelConfig?.primary?.trim() ?? "";
const modelFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.model);
const imageFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel);
const imagePrimary = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel) ?? "";
modelFallbacks.forEach((raw, idx) => {
const resolved = resolveModelRefFromString({

View File

@@ -26,6 +26,10 @@ import {
import { formatCliCommand } from "../../cli/command-format.js";
import { withProgressTotals } from "../../cli/progress.js";
import { CONFIG_PATH, loadConfig } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../../config/model-input.js";
import {
formatUsageWindowSummary,
loadProviderUsageSummary,
@@ -87,24 +91,14 @@ export async function modelsStatusCommand(
defaultModel: DEFAULT_MODEL,
});
const modelConfig = cfg.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
const imageConfig = cfg.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
const rawDefaultsModel =
typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? "");
const rawDefaultsModel = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model) ?? "";
const rawModel = agentModelPrimary ?? rawDefaultsModel;
const resolvedLabel = `${resolved.provider}/${resolved.model}`;
const defaultLabel = rawModel || resolvedLabel;
const defaultsFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const defaultsFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.model);
const fallbacks = agentFallbacksOverride ?? defaultsFallbacks;
const imageModel =
typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? "");
const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
const imageModel = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel) ?? "";
const imageFallbacks = resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel);
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<Record<string, string>>(
(acc, [key, entry]) => {
const alias = typeof entry?.alias === "string" ? entry.alias.trim() : undefined;

View File

@@ -4,6 +4,7 @@ import { type ModelScanResult, scanOpenRouterModels } from "../../agents/model-s
import { withProgressTotals } from "../../cli/progress.js";
import { loadConfig } from "../../config/config.js";
import { logConfigUpdated } from "../../config/logging.js";
import { toAgentModelListLike } from "../../config/model-input.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
stylePromptHint,
@@ -297,9 +298,7 @@ export async function modelsScanCommand(
nextModels[entry] = {};
}
}
const existingImageModel = cfg.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
const existingImageModel = toAgentModelListLike(cfg.agents?.defaults?.imageModel);
const nextImageModel =
selectedImages.length > 0
? {
@@ -308,9 +307,7 @@ export async function modelsScanCommand(
...(opts.setImage ? { primary: selectedImages[0] } : {}),
}
: cfg.agents?.defaults?.imageModel;
const existingModel = cfg.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
const existingModel = toAgentModelListLike(cfg.agents?.defaults?.model);
const defaults = {
...cfg.agents?.defaults,
model: {

View File

@@ -6,6 +6,7 @@ import { cancel, isCancel } from "@clack/prompts";
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../agents/workspace.js";
import type { OpenClawConfig } from "../config/config.js";
import { CONFIG_PATH } from "../config/config.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js";
@@ -43,7 +44,7 @@ export function summarizeExistingConfig(config: OpenClawConfig): string {
rows.push(shortenHomeInString(`workspace: ${defaults.workspace}`));
}
if (defaults?.model) {
const model = typeof defaults.model === "string" ? defaults.model : defaults.model.primary;
const model = resolveAgentModelPrimaryValue(defaults.model);
if (model) {
rows.push(shortenHomeInString(`model: ${model}`));
}

View File

@@ -10,6 +10,10 @@ import {
} from "../agents/model-catalog.js";
import type { MsgContext } from "../auto-reply/templating.js";
import type { OpenClawConfig } from "../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import type {
MediaUnderstandingConfig,
MediaUnderstandingModelConfig,
@@ -418,28 +422,19 @@ async function resolveKeyEntry(params: {
}
function resolveImageModelFromAgentDefaults(cfg: OpenClawConfig): MediaUnderstandingModelConfig[] {
const imageModel = cfg.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| string
| undefined;
if (!imageModel) {
return [];
}
const refs: string[] = [];
if (typeof imageModel === "string") {
if (imageModel.trim()) {
refs.push(imageModel.trim());
}
} else {
if (imageModel.primary?.trim()) {
refs.push(imageModel.primary.trim());
}
for (const fb of imageModel.fallbacks ?? []) {
if (fb?.trim()) {
refs.push(fb.trim());
}
const primary = resolveAgentModelPrimaryValue(cfg.agents?.defaults?.imageModel);
if (primary?.trim()) {
refs.push(primary.trim());
}
for (const fb of resolveAgentModelFallbackValues(cfg.agents?.defaults?.imageModel)) {
if (fb?.trim()) {
refs.push(fb.trim());
}
}
if (refs.length === 0) {
return [];
}
const entries: MediaUnderstandingModelConfig[] = [];
for (const ref of refs) {
const slashIdx = ref.indexOf("/");