mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix: clear auto-failover model overrides (#69365) (thanks @Chevron7Locked)
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/pairing: treat loopback shared-secret node-host, TUI, and gateway clients as local for pairing decisions, so trusted local tools no longer reconnect as remote clients and fail with `pairing required`. (#69431) Thanks @SARAMALI15792.
|
||||
- Active Memory: degrade gracefully when memory recall fails during prompt building, logging a warning and letting the reply continue without memory context instead of failing the whole turn. (#69485) Thanks @Magicray1217.
|
||||
- Ollama: add provider-policy defaults for `baseUrl` and `models` so implicit local discovery can run before config validation rejects a minimal Ollama provider config. (#69370) Thanks @PratikRai0101.
|
||||
- Agents/model selection: clear transient auto-failover session overrides before each turn so recovered primary models are retried immediately without emitting user-override reset warnings. (#69365) Thanks @hitesh-github99.
|
||||
- Telegram/status reactions: honor `messages.removeAckAfterReply` when lifecycle status reactions are enabled, clearing or restoring the reaction after success/error using the configured hold timings. (#68067) Thanks @poiskgit.
|
||||
- Web search/plugins: resolve plugin-scoped SecretRef API keys for bundled Exa, Firecrawl, Gemini, Kimi, Perplexity, Tavily, and Grok web-search providers when they are selected through the shared web-search config. (#68424) Thanks @afurm.
|
||||
- Telegram/polling: raise the default polling watchdog threshold from 90s to 120s and add configurable `channels.telegram.pollingStallThresholdMs` (also per-account) so long-running Telegram work gets more room before polling is treated as stalled. (#57737) Thanks @Vitalcheffe.
|
||||
|
||||
@@ -568,7 +568,6 @@ describe("createModelSelectionState auto-failover override self-healing", () =>
|
||||
modelOverrideSource: "auto",
|
||||
});
|
||||
|
||||
// Provider/model should revert to the configured primary, not the fallback.
|
||||
// Provider/model should revert to the configured primary, not the fallback.
|
||||
expect(state.provider).toBe(defaultProvider);
|
||||
expect(state.model).toBe(defaultModel);
|
||||
@@ -581,6 +580,45 @@ describe("createModelSelectionState auto-failover override self-healing", () =>
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
});
|
||||
|
||||
it("clears a disallowed auto-failover override without reporting an allowlist reset", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: `${defaultProvider}/${defaultModel}` },
|
||||
models: {
|
||||
[`${defaultProvider}/${defaultModel}`]: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const sessionEntry = makeEntry({
|
||||
providerOverride: "openrouter",
|
||||
modelOverride: "minimax/minimax-m2.7",
|
||||
modelOverrideSource: "auto",
|
||||
});
|
||||
const sessionStore = { [sessionKey]: sessionEntry };
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
provider: "openrouter",
|
||||
model: "minimax/minimax-m2.7",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
expect(state.provider).toBe(defaultProvider);
|
||||
expect(state.model).toBe(defaultModel);
|
||||
expect(state.resetModelOverride).toBe(false);
|
||||
expect(sessionStore[sessionKey]?.providerOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelOverride).toBeUndefined();
|
||||
expect(sessionStore[sessionKey]?.modelOverrideSource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resets in-memory provider/model even when caller pre-loaded the fallback", async () => {
|
||||
// Simulates get-reply-directives.ts preloading provider/model from stored override
|
||||
// before calling createModelSelectionState. Our fix must update those in-memory
|
||||
|
||||
@@ -304,6 +304,8 @@ export async function createModelSelectionState(params: {
|
||||
overrideProvider: sessionEntry?.providerOverride,
|
||||
overrideModel: sessionEntry?.modelOverride,
|
||||
});
|
||||
const hadDirectAutoSessionOverride =
|
||||
sessionEntry?.modelOverrideSource === "auto" && Boolean(directStoredOverride);
|
||||
|
||||
if (needsModelCatalog) {
|
||||
modelCatalog = await (await loadModelCatalogRuntime()).loadModelCatalog({ config: cfg });
|
||||
@@ -339,7 +341,42 @@ export async function createModelSelectionState(params: {
|
||||
logStage("configured-catalog-ready", `entries=${configuredModelCatalog.length}`);
|
||||
}
|
||||
|
||||
if (sessionEntry && sessionStore && sessionKey && directStoredOverride) {
|
||||
// Auto-failover overrides are transient: on this turn, retry the configured
|
||||
// primary so the session self-heals when the primary recovers. The fallback loop
|
||||
// in runWithModelFallback will re-set the override if the primary is still down.
|
||||
// User-selected overrides (/model command) are preserved across turns.
|
||||
//
|
||||
// Clear this before allowlist validation so an old fallback outside the current
|
||||
// agent allowlist does not emit the unrelated "Model override not allowed" event.
|
||||
if (hadDirectAutoSessionOverride && sessionEntry && sessionStore && sessionKey) {
|
||||
const { updated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
selection: { provider: defaultProvider, model: defaultModel, isDefault: true },
|
||||
});
|
||||
if (updated) {
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await (
|
||||
await loadSessionStoreRuntime()
|
||||
).updateSessionStore(storePath, (store) => {
|
||||
store[sessionKey] = sessionEntry;
|
||||
});
|
||||
}
|
||||
// Reset in-memory selection to the configured primary. The caller-provided
|
||||
// provider/model may already be set to the fallback by stored-override preload
|
||||
// in get-reply.ts; updating them here ensures this turn retries the primary.
|
||||
provider = defaultProvider;
|
||||
model = defaultModel;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sessionEntry &&
|
||||
sessionStore &&
|
||||
sessionKey &&
|
||||
directStoredOverride &&
|
||||
!hadDirectAutoSessionOverride
|
||||
) {
|
||||
const normalizedOverride = normalizeModelRef(
|
||||
directStoredOverride.provider,
|
||||
directStoredOverride.model,
|
||||
@@ -364,55 +401,21 @@ export async function createModelSelectionState(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const storedOverride = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
defaultProvider,
|
||||
});
|
||||
const storedOverride = hadDirectAutoSessionOverride
|
||||
? undefined
|
||||
: resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
parentSessionKey,
|
||||
defaultProvider,
|
||||
});
|
||||
// Skip stored session model override only when an explicit heartbeat.model
|
||||
// was resolved. Heartbeat runs without heartbeat.model should still inherit
|
||||
// the regular session/parent model override behavior.
|
||||
const skipStoredOverride = params.hasResolvedHeartbeatModelOverride === true;
|
||||
|
||||
// Auto-failover overrides are transient: on this turn, retry the configured
|
||||
// primary so the session self-heals when the primary recovers. The fallback loop
|
||||
// in runWithModelFallback will re-set the override if the primary is still down.
|
||||
// User-selected overrides (/model command) are preserved across turns.
|
||||
//
|
||||
// Note: channel model overrides (channels.modelByChannel) are skipped when
|
||||
// hasSessionModelOverride was true at get-reply-directives preload time. They
|
||||
// resume on the following turn once the session state is clear.
|
||||
const isAutoSessionOverride =
|
||||
storedOverride?.source === "session" && sessionEntry?.modelOverrideSource === "auto";
|
||||
if (isAutoSessionOverride && sessionEntry && sessionStore && sessionKey && !resetModelOverride) {
|
||||
const { updated } = applyModelOverrideToSessionEntry({
|
||||
entry: sessionEntry,
|
||||
selection: { provider: defaultProvider, model: defaultModel, isDefault: true },
|
||||
});
|
||||
if (updated) {
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await (
|
||||
await loadSessionStoreRuntime()
|
||||
).updateSessionStore(storePath, (store) => {
|
||||
store[sessionKey] = sessionEntry;
|
||||
});
|
||||
}
|
||||
// Reset in-memory selection to the configured primary. The caller-provided
|
||||
// provider/model were already set to the fallback by the stored-override
|
||||
// preload in get-reply-directives.ts; updating them here ensures this turn
|
||||
// retries the primary rather than incurring one extra fallback call.
|
||||
provider = defaultProvider;
|
||||
model = defaultModel;
|
||||
// Do NOT set resetModelOverride — that flag triggers a "Model override not
|
||||
// allowed for this agent" system event, which is incorrect for auto-heal.
|
||||
// The override was valid; it just expired after the primary recovered.
|
||||
}
|
||||
}
|
||||
|
||||
if (storedOverride?.model && !skipStoredOverride && !isAutoSessionOverride) {
|
||||
if (storedOverride?.model && !skipStoredOverride) {
|
||||
const normalizedStoredOverride = normalizeModelRef(
|
||||
storedOverride.provider || defaultProvider,
|
||||
storedOverride.model,
|
||||
|
||||
Reference in New Issue
Block a user