mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
fix(models): keep agent primaries strict
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/models: keep per-agent primary models strict when `fallbacks` is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.
|
||||
- Cron/Telegram: preserve explicit `:topic:` delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.
|
||||
- Build/runtime: write the runtime-postbuild stamp after `pnpm build` writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.
|
||||
- CLI/model probes: reject empty or whitespace-only `infer model run --prompt` values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.
|
||||
|
||||
@@ -58,7 +58,8 @@ That prevents a failed fallback retry from overwriting newer unrelated session m
|
||||
|
||||
OpenClaw separates the selected provider/model from why it was selected. That source controls whether the fallback chain is allowed:
|
||||
|
||||
- **Configured default**: `agents.defaults.model.primary` (or an agent-specific primary) uses the configured fallback chain.
|
||||
- **Configured default**: `agents.defaults.model.primary` uses `agents.defaults.model.fallbacks`.
|
||||
- **Agent primary**: `agents.list[].model` is strict unless that agent model object includes its own `fallbacks`. Use `fallbacks: []` to make the strict behavior explicit, or provide a non-empty list to opt that agent into model fallback.
|
||||
- **Auto fallback override**: a runtime fallback writes `providerOverride`, `modelOverride`, and `modelOverrideSource: "auto"` before retrying. That auto override can keep walking the configured fallback chain and is cleared by `/new`, `/reset`, and `sessions.reset`.
|
||||
- **User session override**: `/model`, the model picker, `session_status(model=...)`, and `sessions.patch` write `modelOverrideSource: "user"`. That is an exact session selection. If the selected provider/model fails before producing a reply, OpenClaw reports the failure instead of answering from an unrelated configured fallback.
|
||||
- **Legacy session override**: older session entries may have `modelOverride` without `modelOverrideSource`. OpenClaw treats those as user overrides so an explicit old selection is not silently converted into fallback behavior.
|
||||
@@ -217,7 +218,7 @@ If all profiles for a provider fail, OpenClaw moves to the next model in `agents
|
||||
|
||||
Overloaded and rate-limit errors are handled more aggressively than billing cooldowns. By default, OpenClaw allows one same-provider auth-profile retry, then switches to the next configured model fallback without waiting. Provider-busy signals such as `ModelNotReadyException` land in that overloaded bucket. Tune this with `auth.cooldowns.overloadedProfileRotations`, `auth.cooldowns.overloadedBackoffMs`, and `auth.cooldowns.rateLimitedProfileRotations`.
|
||||
|
||||
When a run starts from the configured primary, a cron job primary, or an auto-selected fallback override, OpenClaw can walk the configured fallback chain. Explicit user selections (for example `/model ollama/qwen3.5:27b`, the model picker, `sessions.patch`, or one-off CLI provider/model overrides) are strict: if that provider/model is unreachable or fails before producing a reply, OpenClaw reports the failure instead of answering from an unrelated fallback.
|
||||
When a run starts from the configured default primary, a cron job primary, an agent primary with explicit fallbacks, or an auto-selected fallback override, OpenClaw can walk the matching configured fallback chain. Agent primaries without explicit fallbacks and explicit user selections (for example `/model ollama/qwen3.5:27b`, the model picker, `sessions.patch`, or one-off CLI provider/model overrides) are strict: if that provider/model is unreachable or fails before producing a reply, OpenClaw reports the failure instead of answering from an unrelated fallback.
|
||||
|
||||
### Candidate chain rules
|
||||
|
||||
|
||||
@@ -969,7 +969,7 @@ for provider examples and precedence.
|
||||
|
||||
- `id`: stable agent id (required).
|
||||
- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
|
||||
- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks). Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`.
|
||||
- `model`: string form sets a strict per-agent primary with no model fallback; object form `{ primary }` is also strict unless you add `fallbacks`. Use `{ primary, fallbacks: [...] }` to opt that agent into fallback, or `{ primary, fallbacks: [] }` to make strict behavior explicit. Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`.
|
||||
- `params`: per-agent stream params merged over the selected model entry in `agents.defaults.models`. Use this for agent-specific overrides like `cacheRetention`, `temperature`, or `maxTokens` without duplicating the whole model catalog.
|
||||
- `tts`: optional per-agent text-to-speech overrides. The block deep-merges over `messages.tts`, so keep shared provider credentials and fallback policy in `messages.tts` and set only persona-specific values such as provider, voice, model, style, or auto mode here.
|
||||
- `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills.
|
||||
|
||||
@@ -182,7 +182,7 @@ describe("resolveAgentConfig", () => {
|
||||
expect(resolveAgentEffectiveModelPrimary(cfg, "linus")).toBe("anthropic/claude-sonnet-4-6");
|
||||
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.4"]);
|
||||
|
||||
// If fallbacks isn't present, we don't override the global fallbacks.
|
||||
// If an agent owns a primary, missing fallbacks means no model fallback.
|
||||
const cfgNoOverride: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
@@ -195,7 +195,55 @@ describe("resolveAgentConfig", () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(undefined);
|
||||
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toEqual([]);
|
||||
expect(
|
||||
resolveEffectiveModelFallbacks({
|
||||
cfg: cfgNoOverride,
|
||||
agentId: "linus",
|
||||
hasSessionModelOverride: false,
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
const cfgStringModel: OpenClawConfig = {
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentModelFallbacksOverride(cfgStringModel, "linus")).toEqual([]);
|
||||
|
||||
const cfgStrictAgentWithDefaultFallbacks: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
fallbacks: ["custom-opencode-go-extras/deepseek-v4-flash"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: {
|
||||
primary: "opencode-go/minimax-m2.7",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(resolveAgentModelFallbacksOverride(cfgStrictAgentWithDefaultFallbacks, "linus")).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(
|
||||
resolveEffectiveModelFallbacks({
|
||||
cfg: cfgStrictAgentWithDefaultFallbacks,
|
||||
agentId: "linus",
|
||||
hasSessionModelOverride: true,
|
||||
modelOverrideSource: "auto",
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
// Explicit empty list disables global fallbacks for that agent.
|
||||
const cfgDisable: OpenClawConfig = {
|
||||
@@ -251,26 +299,19 @@ describe("resolveAgentConfig", () => {
|
||||
}),
|
||||
).toEqual([]);
|
||||
|
||||
const cfgInheritDefaults: OpenClawConfig = {
|
||||
const cfgInheritDefaultsWithoutAgentModel: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
fallbacks: ["openai/gpt-5.4"],
|
||||
},
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "linus",
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
},
|
||||
],
|
||||
list: [{ id: "linus" }],
|
||||
},
|
||||
};
|
||||
expect(
|
||||
resolveEffectiveModelFallbacks({
|
||||
cfg: cfgInheritDefaults,
|
||||
cfg: cfgInheritDefaultsWithoutAgentModel,
|
||||
agentId: "linus",
|
||||
hasSessionModelOverride: true,
|
||||
modelOverrideSource: "auto",
|
||||
|
||||
@@ -156,12 +156,15 @@ export function resolveAgentModelFallbacksOverride(
|
||||
agentId: string,
|
||||
): string[] | undefined {
|
||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||
if (!raw || typeof raw === "string") {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof raw === "string") {
|
||||
return resolvePrimaryStringValue(raw) ? [] : undefined;
|
||||
}
|
||||
// Important: treat an explicitly provided empty array as an override to disable global fallbacks.
|
||||
if (!Object.hasOwn(raw, "fallbacks")) {
|
||||
return undefined;
|
||||
return Object.hasOwn(raw, "primary") && resolvePrimaryStringValue(raw) ? [] : undefined;
|
||||
}
|
||||
return Array.isArray(raw.fallbacks) ? raw.fallbacks : undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user