fix: wire up fallbackOnErrors config through all call sites

- Update shouldTriggerFallback() to use reason !== null for default behavior
- Add resolveAgentModelFallbackOnErrors() resolver in model-input.ts
- Add resolveRunModelFallbackOnErrors() in agent-scope.ts
- Wire fallbackOnErrors through resolveModelFallbackOptions()
- Update agent-command.ts call site
- Update cron/isolated-agent/run.ts call site
This commit is contained in:
subaochen 2026-03-21 11:54:14 +08:00
parent 02e23d4468
commit 731008500e
6 changed files with 80 additions and 27 deletions

View File

@ -57,6 +57,7 @@ import {
listAgentIds,
resolveAgentDir,
resolveEffectiveModelFallbacks,
resolveRunModelFallbackOnErrors,
resolveSessionAgentId,
resolveAgentSkillsFilter,
resolveAgentWorkspaceDir,
@ -1177,6 +1178,11 @@ async function agentCommandInternal(
runId,
agentDir,
fallbacksOverride: effectiveFallbacksOverride,
fallbackOnErrors: resolveRunModelFallbackOnErrors({
cfg,
agentId: sessionAgentId,
sessionKey,
}),
run: (providerOverride, modelOverride, runOptions) => {
const isFallbackRetry = fallbackAttemptIndex > 0;
fallbackAttemptIndex += 1;

View File

@ -1,8 +1,12 @@
import fs from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveAgentModelFallbackValues } from "../config/model-input.js";
import {
resolveAgentModelFallbackOnErrors,
resolveAgentModelFallbackValues,
} from "../config/model-input.js";
import { resolveStateDir } from "../config/paths.js";
import type { FallbackOnErrorCodes } from "../config/types.agents-shared.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
DEFAULT_AGENT_ID,
@ -230,6 +234,29 @@ export function resolveRunModelFallbacksOverride(params: {
);
}
export function resolveAgentModelFallbackOnErrorsOverride(
cfg: OpenClawConfig,
agentId: string,
): FallbackOnErrorCodes | undefined {
const raw = resolveAgentConfig(cfg, agentId)?.model;
return resolveAgentModelFallbackOnErrors(raw);
}
export function resolveRunModelFallbackOnErrors(params: {
cfg: OpenClawConfig | undefined;
agentId?: string | null;
sessionKey?: string | null;
}): FallbackOnErrorCodes | undefined {
if (!params.cfg) {
return undefined;
}
const raw = resolveAgentConfig(
params.cfg,
resolveFallbackAgentId({ agentId: params.agentId, sessionKey: params.sessionKey }),
)?.model;
return resolveAgentModelFallbackOnErrors(raw);
}
export function hasConfiguredModelFallbacks(params: {
cfg: OpenClawConfig | undefined;
agentId?: string | null;

View File

@ -330,21 +330,15 @@ export function coerceToFailoverError(
});
}
/**
* Default HTTP status codes that trigger fallback.
* - Server errors: 500, 502, 503, 504
* - Rate limits: 429
* - Timeouts: 408
* - Not found: 404 (model may have been removed)
*/
const DEFAULT_FALLBACK_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 404]);
/**
* Check if an error should trigger fallback based on the configured error codes.
*
* For "default" or undefined, this delegates to resolveFailoverReasonFromError() which
* matches the original behavior (no regression).
*
* @param err - The error to check
* @param fallbackOnErrors - Configuration for which errors should trigger fallback
* - "default": Use default behavior (server errors + rate limits + timeout + not found)
* - "default": Use original behavior (same as no config) - any recognized failover reason
* - "all": All HTTP errors (4xx and 5xx) trigger fallback
* - number[]: Custom list of status codes
* @returns true if the error should trigger fallback
@ -354,25 +348,23 @@ export function shouldTriggerFallback(
fallbackOnErrors?: FallbackOnErrorCodes,
): boolean {
const status = getStatusCode(err);
const reason = resolveFailoverReasonFromError(err);
// If no status code found, try to determine from error reason
if (status === undefined) {
const reason = resolveFailoverReasonFromError(err);
// For any mode, only trigger fallback if we have a recognized failover reason
// This prevents unrelated runtime errors (parse errors, filesystem errors) from triggering fallback
// For "default" or undefined, match original behavior exactly
// This delegates to the existing reason classification logic
if (fallbackOnErrors === undefined || fallbackOnErrors === "default") {
return reason !== null;
}
// Determine if status code should trigger fallback
if (fallbackOnErrors === undefined || fallbackOnErrors === "default") {
return DEFAULT_FALLBACK_STATUS_CODES.has(status);
} else if (fallbackOnErrors === "all") {
// "all" means all HTTP errors (4xx and 5xx)
return status >= 400;
} else {
// Custom list of status codes
return new Set(fallbackOnErrors).has(status);
// For "all", check if HTTP error (4xx or 5xx)
// Also allow non-HTTP errors with recognized reasons
if (fallbackOnErrors === "all") {
return status !== undefined ? status >= 400 : reason !== null;
}
// For custom array, check specific status codes only
// Ignore non-HTTP errors even if they have a recognized reason
return status !== undefined && new Set(fallbackOnErrors).has(status);
}
/**

View File

@ -1,4 +1,7 @@
import { resolveRunModelFallbacksOverride } from "../../agents/agent-scope.js";
import {
resolveRunModelFallbacksOverride,
resolveRunModelFallbackOnErrors,
} from "../../agents/agent-scope.js";
import type { NormalizedUsage } from "../../agents/usage.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { ChannelId, ChannelThreadingToolContext } from "../../channels/plugins/types.js";
@ -165,6 +168,11 @@ export function resolveModelFallbackOptions(run: FollowupRun["run"]) {
agentId: run.agentId,
sessionKey: run.sessionKey,
}),
fallbackOnErrors: resolveRunModelFallbackOnErrors({
cfg: run.config,
agentId: run.agentId,
sessionKey: run.sessionKey,
}),
};
}

View File

@ -1,4 +1,4 @@
import type { AgentModelConfig } from "./types.agents-shared.js";
import type { AgentModelConfig, FallbackOnErrorCodes } from "./types.agents-shared.js";
type AgentModelListLike = {
primary?: string;
@ -24,6 +24,21 @@ export function resolveAgentModelFallbackValues(model?: AgentModelConfig): strin
return Array.isArray(model.fallbacks) ? model.fallbacks : [];
}
/**
* Resolve the fallbackOnErrors configuration from an AgentModelConfig.
*
* @param model - The agent model configuration
* @returns The fallbackOnErrors value ("all", "default", number[], or undefined)
*/
export function resolveAgentModelFallbackOnErrors(
model?: AgentModelConfig,
): FallbackOnErrorCodes | undefined {
if (!model || typeof model !== "object") {
return undefined;
}
return model.fallbackOnErrors;
}
export function toAgentModelListLike(model?: AgentModelConfig): AgentModelListLike | undefined {
if (typeof model === "string") {
const primary = model.trim();

View File

@ -5,6 +5,7 @@ import {
resolveAgentModelFallbacksOverride,
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
resolveRunModelFallbackOnErrors,
} from "../../agents/agent-scope.js";
import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js";
import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js";
@ -586,6 +587,10 @@ export async function runCronIsolatedAgentTurn(params: {
agentDir,
fallbacksOverride:
payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId),
fallbackOnErrors: resolveRunModelFallbackOnErrors({
cfg: cfgWithAgentDefaults,
agentId,
}),
run: async (providerOverride, modelOverride, runOptions) => {
if (abortSignal?.aborted) {
throw new Error(abortReason());