mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 04:21:24 +00:00
fix(agents): fall back to agents.defaults.model when agent has no model config (#24210)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 0f272b1027
Co-authored-by: bianbiandashen <16240681+bianbiandashen@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -4,6 +4,8 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentDir,
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
resolveAgentExplicitModelPrimary,
|
||||
resolveEffectiveModelFallbacks,
|
||||
resolveAgentModelFallbacksOverride,
|
||||
resolveAgentModelPrimary,
|
||||
@@ -59,6 +61,43 @@ describe("resolveAgentConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves explicit and effective model primary separately", () => {
|
||||
const cfgWithStringDefault = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-sonnet-4",
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
expect(resolveAgentExplicitModelPrimary(cfgWithStringDefault, "main")).toBeUndefined();
|
||||
expect(resolveAgentEffectiveModelPrimary(cfgWithStringDefault, "main")).toBe(
|
||||
"anthropic/claude-sonnet-4",
|
||||
);
|
||||
|
||||
const cfgWithObjectDefault: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-5.2",
|
||||
fallbacks: ["anthropic/claude-sonnet-4"],
|
||||
},
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentExplicitModelPrimary(cfgWithObjectDefault, "main")).toBeUndefined();
|
||||
expect(resolveAgentEffectiveModelPrimary(cfgWithObjectDefault, "main")).toBe("openai/gpt-5.2");
|
||||
|
||||
const cfgNoDefaults: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentExplicitModelPrimary(cfgNoDefaults, "main")).toBeUndefined();
|
||||
expect(resolveAgentEffectiveModelPrimary(cfgNoDefaults, "main")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("supports per-agent model primary+fallbacks", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
@@ -81,6 +120,8 @@ describe("resolveAgentConfig", () => {
|
||||
};
|
||||
|
||||
expect(resolveAgentModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
|
||||
expect(resolveAgentExplicitModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
|
||||
expect(resolveAgentEffectiveModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
|
||||
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.2"]);
|
||||
|
||||
// If fallbacks isn't present, we don't override the global fallbacks.
|
||||
|
||||
@@ -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 {
|
||||
@@ -142,16 +143,43 @@ export function resolveAgentSkillsFilter(
|
||||
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
|
||||
}
|
||||
|
||||
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||
if (!raw) {
|
||||
function resolveModelPrimary(raw: unknown): string | undefined {
|
||||
if (typeof raw === "string") {
|
||||
const trimmed = raw.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
return raw.trim() || undefined;
|
||||
const primary = (raw as { primary?: unknown }).primary;
|
||||
if (typeof primary !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const primary = raw.primary?.trim();
|
||||
return primary || undefined;
|
||||
const trimmed = primary.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export function resolveAgentExplicitModelPrimary(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): string | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||
return resolveModelPrimary(raw);
|
||||
}
|
||||
|
||||
export function resolveAgentEffectiveModelPrimary(
|
||||
cfg: OpenClawConfig,
|
||||
agentId: string,
|
||||
): string | undefined {
|
||||
return (
|
||||
resolveAgentExplicitModelPrimary(cfg, agentId) ??
|
||||
resolveModelPrimary(cfg.agents?.defaults?.model)
|
||||
);
|
||||
}
|
||||
|
||||
// Backward-compatible alias. Prefer explicit/effective helpers at new call sites.
|
||||
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
||||
return resolveAgentExplicitModelPrimary(cfg, agentId);
|
||||
}
|
||||
|
||||
export function resolveAgentModelFallbacksOverride(
|
||||
@@ -178,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue, toAgentModelListLike } from "../config/model-input.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveAgentConfig, resolveAgentModelPrimary } from "./agent-scope.js";
|
||||
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||
import { normalizeGoogleModelId } from "./models-config.providers.js";
|
||||
@@ -259,13 +260,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
}): ModelRef {
|
||||
const rawModel = (() => {
|
||||
const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
|
||||
if (typeof raw === "string") {
|
||||
return raw.trim();
|
||||
}
|
||||
return raw?.primary?.trim() ?? "";
|
||||
})();
|
||||
const rawModel = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model) ?? "";
|
||||
if (rawModel) {
|
||||
const trimmed = rawModel.trim();
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
@@ -303,7 +298,7 @@ export function resolveDefaultModelForAgent(params: {
|
||||
agentId?: string;
|
||||
}): ModelRef {
|
||||
const agentModelOverride = params.agentId
|
||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
||||
? resolveAgentEffectiveModelPrimary(params.cfg, params.agentId)
|
||||
: undefined;
|
||||
const cfg =
|
||||
agentModelOverride && agentModelOverride.length > 0
|
||||
@@ -314,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,
|
||||
},
|
||||
},
|
||||
@@ -357,7 +350,7 @@ export function resolveSubagentSpawnModelSelection(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
}) ??
|
||||
normalizeModelSelection(params.cfg.agents?.defaults?.model?.primary) ??
|
||||
normalizeModelSelection(resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)) ??
|
||||
`${runtimeDefault.provider}/${runtimeDefault.model}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { resolveAgentModelFallbackValues } from "../../config/model-input.js";
|
||||
import { generateSecureToken } from "../../infra/secure-random.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import type { PluginHookBeforeAgentStartResult } from "../../plugins/types.js";
|
||||
@@ -231,7 +232,7 @@ export async function runEmbeddedPiAgent(
|
||||
let modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
|
||||
const fallbackConfigured =
|
||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||
resolveAgentModelFallbackValues(params.config?.agents?.defaults?.model).length > 0;
|
||||
await ensureOpenClawModelsJson(params.config, agentDir);
|
||||
|
||||
// Run before_model_resolve hooks early so plugins can override the
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
Reference in New Issue
Block a user