fix(ui-cron): include configured model suggestions for scheduled jobs (openclaw#29709) thanks @Sid-Qin

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Sid
2026-03-01 21:31:47 +08:00
committed by GitHub
parent 5784963608
commit f107347608
4 changed files with 122 additions and 1 deletions

View File

@@ -65,6 +65,7 @@ import {
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
import { icons } from "./icons.ts";
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
import { resolveConfiguredCronModelSuggestions } from "./views/agents-utils.ts";
import { renderAgents } from "./views/agents.ts";
import { renderChannels } from "./views/channels.ts";
import { renderChat } from "./views/chat.ts";
@@ -178,6 +179,7 @@ export function renderApp(state: AppViewState) {
new Set(
[
...state.cronModelSuggestions,
...resolveConfiguredCronModelSuggestions(configValue),
...state.cronJobs
.map((job) => {
if (job.payload.kind !== "agentTurn" || typeof job.payload.model !== "string") {

View File

@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { resolveEffectiveModelFallbacks } from "./agents-utils.ts";
import {
resolveConfiguredCronModelSuggestions,
resolveEffectiveModelFallbacks,
} from "./agents-utils.ts";
describe("resolveEffectiveModelFallbacks", () => {
it("inherits defaults when no entry fallbacks are configured", () => {
@@ -40,3 +43,47 @@ describe("resolveEffectiveModelFallbacks", () => {
expect(resolveEffectiveModelFallbacks(entryModel, defaultModel)).toEqual([]);
});
});
describe("resolveConfiguredCronModelSuggestions", () => {
it("collects defaults primary/fallbacks, alias map keys, and per-agent model entries", () => {
const result = resolveConfiguredCronModelSuggestions({
agents: {
defaults: {
model: {
primary: "openai/gpt-5.2",
fallbacks: ["google/gemini-2.5-pro", "openai/gpt-5.2-mini"],
},
models: {
"anthropic/claude-sonnet-4-5": { alias: "smart" },
"openai/gpt-5.2": { alias: "main" },
},
},
list: {
writer: {
model: { primary: "xai/grok-4", fallbacks: ["openai/gpt-5.2-mini"] },
},
planner: {
model: "google/gemini-2.5-flash",
},
},
},
});
expect(result).toEqual([
"anthropic/claude-sonnet-4-5",
"google/gemini-2.5-flash",
"google/gemini-2.5-pro",
"openai/gpt-5.2",
"openai/gpt-5.2-mini",
"xai/grok-4",
]);
});
it("returns empty array for invalid or missing config shape", () => {
expect(resolveConfiguredCronModelSuggestions(null)).toEqual([]);
expect(resolveConfiguredCronModelSuggestions({})).toEqual([]);
expect(resolveConfiguredCronModelSuggestions({ agents: { defaults: { model: "" } } })).toEqual(
[],
);
});
});

View File

@@ -251,6 +251,77 @@ export function resolveEffectiveModelFallbacks(
return resolveModelFallbacks(entryModel) ?? resolveModelFallbacks(defaultModel);
}
function addModelId(target: Set<string>, value: unknown) {
if (typeof value !== "string") {
return;
}
const trimmed = value.trim();
if (!trimmed) {
return;
}
target.add(trimmed);
}
function addModelConfigIds(target: Set<string>, modelConfig: unknown) {
if (!modelConfig) {
return;
}
if (typeof modelConfig === "string") {
addModelId(target, modelConfig);
return;
}
if (typeof modelConfig !== "object") {
return;
}
const record = modelConfig as Record<string, unknown>;
addModelId(target, record.primary);
addModelId(target, record.model);
addModelId(target, record.id);
addModelId(target, record.value);
const fallbacks = Array.isArray(record.fallbacks)
? record.fallbacks
: Array.isArray(record.fallback)
? record.fallback
: [];
for (const fallback of fallbacks) {
addModelId(target, fallback);
}
}
export function resolveConfiguredCronModelSuggestions(
configForm: Record<string, unknown> | null,
): string[] {
if (!configForm || typeof configForm !== "object") {
return [];
}
const agents = (configForm as { agents?: unknown }).agents;
if (!agents || typeof agents !== "object") {
return [];
}
const out = new Set<string>();
const defaults = (agents as { defaults?: unknown }).defaults;
if (defaults && typeof defaults === "object") {
const defaultsRecord = defaults as Record<string, unknown>;
addModelConfigIds(out, defaultsRecord.model);
const defaultsModels = defaultsRecord.models;
if (defaultsModels && typeof defaultsModels === "object") {
for (const modelId of Object.keys(defaultsModels as Record<string, unknown>)) {
addModelId(out, modelId);
}
}
}
const list = (agents as { list?: unknown }).list;
if (list && typeof list === "object") {
for (const entry of Object.values(list as Record<string, unknown>)) {
if (!entry || typeof entry !== "object") {
continue;
}
addModelConfigIds(out, (entry as Record<string, unknown>).model);
}
}
return [...out].toSorted((a, b) => a.localeCompare(b));
}
export function parseFallbackList(value: string): string[] {
return value
.split(",")