mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(cli): respect replace mode in model picker
This commit is contained in:
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.
|
||||
- Control UI/models: request the configured Gateway model-list view so dashboards with only `models.providers.*.models` show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw.
|
||||
- CLI/models: keep default-model and allowlist pickers on explicit `models.providers.*.models` entries when `models.mode` is `replace` instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg.
|
||||
- Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip `InteractionEventListener` listener timeouts. Fixes #73204. Thanks @slideshow-dingo.
|
||||
- Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.
|
||||
- Models/fallbacks: record first-class `model.fallback_step` trajectory events with from/to models, failure detail, chain position, and final outcome so support exports preserve the primary model failure even when a later fallback also fails. Fixes #71744. Thanks @nikolaykazakovvs-ux.
|
||||
|
||||
@@ -61,6 +61,7 @@ The same `provider/model` can mean different things depending on where it came f
|
||||
- Auto fallback selections are temporary recovery state. They are stored with `modelOverrideSource: "auto"` so later turns can keep using the fallback chain without probing a known-bad primary first.
|
||||
- User session selections are exact. `/model`, the model picker, `session_status(model=...)`, and `sessions.patch` store `modelOverrideSource: "user"`; if that selected provider/model is unreachable, OpenClaw fails visibly instead of falling through to another configured model.
|
||||
- Cron `--model` / payload `model` is a per-job primary. It still uses configured fallbacks unless the job supplies explicit payload `fallbacks` (use `fallbacks: []` for a strict cron run).
|
||||
- CLI default-model and allowlist pickers respect `models.mode: "replace"` by listing explicit `models.providers.*.models` instead of loading the full built-in catalog.
|
||||
- The Control UI model picker asks the Gateway for its configured model view: `agents.defaults.models` when present, otherwise explicit `models.providers.*.models`, otherwise the full catalog so fresh installs are not blank.
|
||||
|
||||
## Quick model policy
|
||||
|
||||
@@ -97,6 +97,18 @@ function createSelectAllMultiselect() {
|
||||
return vi.fn(async (params) => params.options.map((option: { value: string }) => option.value));
|
||||
}
|
||||
|
||||
function configuredTextModel(id: string, name: string) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
reasoning: false,
|
||||
input: ["text" as const],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
providerModelPickerContributionRuntime.enabled = false;
|
||||
@@ -186,6 +198,45 @@ describe("promptDefaultModel", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses configured provider models without loading the full catalog in replace mode", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{ provider: "openai", id: "gpt-5.5", name: "GPT-5.5" },
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet" },
|
||||
]);
|
||||
|
||||
const select = vi.fn(async (params) => params.options[0]?.value as never);
|
||||
const prompter = makePrompter({ select });
|
||||
const config = {
|
||||
models: {
|
||||
mode: "replace",
|
||||
providers: {
|
||||
minimax: {
|
||||
baseUrl: "https://api.minimax.test/v1",
|
||||
models: [configuredTextModel("MiniMax-M2.7-highspeed", "MiniMax M2.7 Highspeed")],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: { defaults: {} },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptDefaultModel({
|
||||
config,
|
||||
prompter,
|
||||
allowKeep: false,
|
||||
includeManual: false,
|
||||
ignoreAllowlist: true,
|
||||
});
|
||||
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
expect(select.mock.calls[0]?.[0]?.options).toEqual([
|
||||
expect.objectContaining({
|
||||
value: "minimax/MiniMax-M2.7-highspeed",
|
||||
hint: expect.stringContaining("MiniMax M2.7 Highspeed"),
|
||||
}),
|
||||
]);
|
||||
expect(result.model).toBe("minimax/MiniMax-M2.7-highspeed");
|
||||
});
|
||||
|
||||
it("treats byteplus plan models as preferred-provider matches", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
@@ -514,6 +565,43 @@ describe("promptModelAllowlist", () => {
|
||||
expect(result.scopeKeys).toEqual(["anthropic/claude-opus-4-6"]);
|
||||
});
|
||||
|
||||
it("uses configured provider models without loading the full catalog in replace mode", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
provider: "openai",
|
||||
id: "gpt-5.5",
|
||||
name: "GPT-5.5",
|
||||
},
|
||||
]);
|
||||
|
||||
const multiselect = createSelectAllMultiselect();
|
||||
const prompter = makePrompter({ multiselect });
|
||||
const config = {
|
||||
models: {
|
||||
mode: "replace",
|
||||
providers: {
|
||||
minimax: {
|
||||
baseUrl: "https://api.minimax.test/v1",
|
||||
models: [configuredTextModel("MiniMax-M2.7-highspeed", "MiniMax M2.7 Highspeed")],
|
||||
},
|
||||
zhipu: {
|
||||
baseUrl: "https://api.zhipu.test/v1",
|
||||
models: [configuredTextModel("glm-4.5-air", "GLM 4.5 Air")],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: { defaults: {} },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = await promptModelAllowlist({ config, prompter });
|
||||
|
||||
expect(loadModelCatalog).not.toHaveBeenCalled();
|
||||
expect(
|
||||
multiselect.mock.calls[0]?.[0]?.options.map((option: { value: string }) => option.value),
|
||||
).toEqual(["minimax/MiniMax-M2.7-highspeed", "zhipu/glm-4.5-air"]);
|
||||
expect(result.models).toEqual(["minimax/MiniMax-M2.7-highspeed", "zhipu/glm-4.5-air"]);
|
||||
});
|
||||
|
||||
it("scopes the initial allowlist picker to the preferred provider", async () => {
|
||||
loadModelCatalog.mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../agents/model-picker-visibility.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
buildConfiguredModelCatalog,
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
@@ -115,6 +116,13 @@ function resolveConfiguredModelKeys(cfg: OpenClawConfig): string[] {
|
||||
.filter((key) => key.length > 0);
|
||||
}
|
||||
|
||||
function loadPickerModelCatalog(cfg: OpenClawConfig): ReturnType<typeof loadModelCatalog> {
|
||||
if (cfg.models?.mode === "replace") {
|
||||
return Promise.resolve(buildConfiguredModelCatalog({ cfg }));
|
||||
}
|
||||
return loadModelCatalog({ config: cfg });
|
||||
}
|
||||
|
||||
function normalizeModelKeys(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const next: string[] = [];
|
||||
@@ -625,7 +633,7 @@ export async function promptDefaultModel(
|
||||
const catalogProgress = params.prompter.progress("Loading available models");
|
||||
let catalog: Awaited<ReturnType<typeof loadModelCatalog>>;
|
||||
try {
|
||||
catalog = await loadModelCatalog({ config: cfg });
|
||||
catalog = await loadPickerModelCatalog(cfg);
|
||||
} finally {
|
||||
catalogProgress.stop();
|
||||
}
|
||||
@@ -897,7 +905,7 @@ export async function promptModelAllowlist(params: {
|
||||
const allowlistProgress = params.prompter.progress("Loading available models");
|
||||
let catalog: Awaited<ReturnType<typeof loadModelCatalog>>;
|
||||
try {
|
||||
catalog = await loadModelCatalog({ config: cfg });
|
||||
catalog = await loadPickerModelCatalog(cfg);
|
||||
} finally {
|
||||
allowlistProgress.stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user