mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
fix: fail closed for invalid cron payload models
This commit is contained in:
@@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Backup: skip installed plugin `extensions/*/node_modules` dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang.
|
||||
- Cron/models: fail isolated cron runs closed when an explicit `payload.model` is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang.
|
||||
- Memory/Ollama: resolve `memorySearch.provider` custom provider ids through their configured `models.providers.<id>.api` owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as `ollama-5080` without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.
|
||||
- CLI/memory: skip eager context-window warmup for `openclaw memory` commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.
|
||||
- CLI/Telegram: route Telegram `message send` and poll actions through the running Gateway when available, so packaged installs use the staged `grammy` runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.
|
||||
|
||||
@@ -129,7 +129,7 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
Restrict which tools the job can use, for example `--tools exec,read`.
|
||||
</ParamField>
|
||||
|
||||
`--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed, cron logs a warning and falls back to the job's agent/default model selection instead.
|
||||
`--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job's agent/default model selection.
|
||||
|
||||
Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target.
|
||||
|
||||
@@ -378,7 +378,7 @@ Model override note:
|
||||
|
||||
- `openclaw cron add|edit --model ...` changes the job's selected model.
|
||||
- If the model is allowed, that exact provider/model reaches the isolated agent run.
|
||||
- If it is not allowed, cron warns and falls back to the job's agent/default model selection.
|
||||
- If it is not allowed or cannot be resolved, cron fails the run with an explicit validation error.
|
||||
- Configured fallback chains still apply because cron `--model` is a job primary, not a session `/model` override.
|
||||
- Payload `fallbacks` replaces configured fallbacks for that job; `fallbacks: []` disables fallback and makes the run strict.
|
||||
- A plain `--model` with no explicit or configured fallback list does not fall through to the agent primary as a silent extra retry target.
|
||||
|
||||
@@ -100,7 +100,7 @@ Note: cron job definitions live in `jobs.json`, while pending runtime state live
|
||||
`cron add|edit --model <ref>` selects an allowed model for the job.
|
||||
|
||||
<Warning>
|
||||
If the model is not allowed, cron warns and falls back to the job's agent or default model selection.
|
||||
If the model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of falling back to the job's agent or default model selection.
|
||||
</Warning>
|
||||
|
||||
Cron `--model` is a **job primary**, not a chat-session `/model` override. That means:
|
||||
|
||||
@@ -3,6 +3,40 @@ import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolveAllowedModelRef, resolveConfiguredModelRef } from "./model-selection-resolve.js";
|
||||
|
||||
describe("model-selection-resolve OpenRouter compat aliases", () => {
|
||||
it("preserves exact configured proxy provider ids for cron-style aliases", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"litellm/cron": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
litellm: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "http://127.0.0.1:4000/v1",
|
||||
models: [{ id: "cron", name: "Cron route" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveAllowedModelRef({
|
||||
cfg,
|
||||
catalog: [],
|
||||
raw: "litellm/cron",
|
||||
defaultProvider: "ollama",
|
||||
defaultModel: "qwen35-27b-researcher",
|
||||
}),
|
||||
).toEqual({
|
||||
key: "litellm/cron",
|
||||
ref: { provider: "litellm", model: "cron" },
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves openrouter:auto through the canonical OpenRouter auto model", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
|
||||
@@ -197,7 +197,10 @@ describe("cron model formatting and precedence edge cases", () => {
|
||||
selectModel({
|
||||
payload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "openai/" },
|
||||
}),
|
||||
).resolves.toEqual({ ok: false, error: "invalid model" });
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
error: "cron payload.model 'openai/' rejected: invalid model",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects model with leading slash (empty provider)", async () => {
|
||||
@@ -205,7 +208,30 @@ describe("cron model formatting and precedence edge cases", () => {
|
||||
selectModel({
|
||||
payload: { kind: "agentTurn", message: DEFAULT_MESSAGE, model: "/gpt-4.1-mini" },
|
||||
}),
|
||||
).resolves.toEqual({ ok: false, error: "invalid model" });
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
error: "cron payload.model '/gpt-4.1-mini' rejected: invalid model",
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the cron allowlist path when payload.model is not allowed", async () => {
|
||||
resolveAllowedModelRefMock.mockReturnValueOnce({
|
||||
error: "model not allowed: anthropic/claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
await expect(
|
||||
selectModel({
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: DEFAULT_MESSAGE,
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
ok: false,
|
||||
error:
|
||||
"cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.models allowlist: anthropic/claude-sonnet-4-6",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes provider casing", async () => {
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => {
|
||||
});
|
||||
|
||||
expect(res.status).toBe("error");
|
||||
expect(res.error).toMatch("invalid model");
|
||||
expect(res.error).toMatch("cron payload.model 'openai/' rejected: invalid model");
|
||||
expect(vi.mocked(runEmbeddedPiAgent)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,13 +35,20 @@ export type ResolveCronModelSelectionResult =
|
||||
ok: true;
|
||||
provider: string;
|
||||
model: string;
|
||||
warning?: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
function formatCronPayloadModelRejection(modelOverride: string, error: string): string {
|
||||
if (error.startsWith("model not allowed:")) {
|
||||
const modelRef = error.slice("model not allowed:".length).trim();
|
||||
return `cron payload.model '${modelOverride}' rejected by agents.defaults.models allowlist: ${modelRef}`;
|
||||
}
|
||||
return `cron payload.model '${modelOverride}' rejected: ${error}`;
|
||||
}
|
||||
|
||||
export async function resolveCronModelSelection(
|
||||
params: ResolveCronModelSelectionParams,
|
||||
): Promise<ResolveCronModelSelectionResult> {
|
||||
@@ -112,15 +119,10 @@ export async function resolveCronModelSelection(
|
||||
defaultModel: resolvedDefault.model,
|
||||
});
|
||||
if ("error" in resolvedOverride) {
|
||||
if (resolvedOverride.error.startsWith("model not allowed:")) {
|
||||
return {
|
||||
ok: true,
|
||||
provider,
|
||||
model,
|
||||
warning: `cron: payload.model '${modelOverride}' not allowed, falling back to agent defaults`,
|
||||
};
|
||||
}
|
||||
return { ok: false, error: resolvedOverride.error };
|
||||
return {
|
||||
ok: false,
|
||||
error: formatCronPayloadModelRejection(modelOverride, resolvedOverride.error),
|
||||
};
|
||||
}
|
||||
provider = resolvedOverride.ref.provider;
|
||||
model = resolvedOverride.ref.model;
|
||||
|
||||
@@ -211,30 +211,36 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
|
||||
expect(runParams.model).toBe("claude-sonnet-4-6");
|
||||
});
|
||||
|
||||
it("falls back to agent defaults when payload.model is not allowed", async () => {
|
||||
it("fails closed when payload.model is not allowed", async () => {
|
||||
resolveAllowedModelRefMock.mockReturnValueOnce({
|
||||
error: "model not allowed: anthropic/claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
await runSkillFilterCase({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai-codex/gpt-5.4", fallbacks: defaultFallbacks },
|
||||
const result = await runCronIsolatedAgentTurn(
|
||||
makeSkillParams({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openai-codex/gpt-5.4", fallbacks: defaultFallbacks },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
job: makeSkillJob({
|
||||
payload: { kind: "agentTurn", message: "test", model: "anthropic/claude-sonnet-4-6" },
|
||||
job: makeSkillJob({
|
||||
payload: {
|
||||
kind: "agentTurn",
|
||||
message: "test",
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(logWarnMock).toHaveBeenCalledWith(
|
||||
"cron: payload.model 'anthropic/claude-sonnet-4-6' not allowed, falling back to agent defaults",
|
||||
);
|
||||
expectDefaultModelCall({
|
||||
primary: "openai-codex/gpt-5.4",
|
||||
fallbacks: defaultFallbacks,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.error).toBe(
|
||||
"cron payload.model 'anthropic/claude-sonnet-4-6' rejected by agents.defaults.models allowlist: anthropic/claude-sonnet-4-6",
|
||||
);
|
||||
expect(logWarnMock).not.toHaveBeenCalled();
|
||||
expect(runWithModelFallbackMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns an error when payload.model is invalid", async () => {
|
||||
@@ -251,7 +257,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
|
||||
);
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.error).toBe("invalid model: openai/");
|
||||
expect(result.error).toBe("cron payload.model 'openai/' rejected: invalid model: openai/");
|
||||
expect(logWarnMock).not.toHaveBeenCalled();
|
||||
expect(runWithModelFallbackMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -575,9 +575,6 @@ async function prepareCronRunContext(params: {
|
||||
}
|
||||
let provider = resolvedModelSelection.provider;
|
||||
let model = resolvedModelSelection.model;
|
||||
if (resolvedModelSelection.warning) {
|
||||
logWarn(resolvedModelSelection.warning);
|
||||
}
|
||||
|
||||
const preflight = await (
|
||||
await loadCronModelPreflightRuntime()
|
||||
|
||||
Reference in New Issue
Block a user