Skip to content

Commit c45ceb8

Browse files
fix(models): honor replace mode in list output
Replace the stale fork PR with the reviewed fix on current main. Refs: #94735 Co-authored-by: 毛宇豪0668001457 <[email protected]>
1 parent bb16ca5 commit c45ceb8

3 files changed

Lines changed: 100 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
2525

2626
### Fixes
2727

28+
- **Model list replace mode:** keep authenticated catalog rows out of default `models list` output when `models.mode: "replace"` while preserving explicit `--all` and `--provider` browsing. (#94705) Thanks @Monkey-wusky.
2829
- **Codex computer control:** publish fixed-length coordinate pairs as homogeneous array schemas so Codex app-server can start threads with the `computer` tool instead of rejecting tuple-valued `items`.
2930
- **Google Chat request deadlines:** bound control calls to 30 seconds while giving media transfers size-aware total budgets and a separate 30-second stalled-body guard, preventing hung Chat API requests without breaking large attachment uploads. (#102227) Thanks @hugenshen.
3031
- **Google Gemini prefixed model IDs:** recognize `google/gemini-*` and `models/gemini-*` when selecting multimodal function-response behavior, preserving the Gemini 2 image fallback without regressing Gemini 3 inline image responses. (#102382) Thanks @LiLan0125.

src/commands/models/list.list-command.forward-compat.test.ts

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -620,13 +620,15 @@ describe("modelsListCommand forward-compat", () => {
620620
});
621621
});
622622

623-
it("includes configured provider and auth-backed catalog rows in configured-mode lists", async () => {
623+
function configureAuthenticatedCatalogScenario(mode: "merge" | "replace") {
624624
const config = {
625625
agents: { defaults: { model: { primary: "xiaomi/mimo-v2.5-pro" } } },
626626
models: {
627+
mode,
627628
providers: {
628629
xiaomi: {
629-
api: "openai-completions",
630+
// Replace-mode configured models can rely on runtime transport inference.
631+
...(mode === "merge" ? { api: "openai-completions" as const } : {}),
630632
apiKey: "tp-fixture",
631633
baseUrl: "https://api.xiaomi.example/v1",
632634
models: [
@@ -637,12 +639,12 @@ describe("modelsListCommand forward-compat", () => {
637639
},
638640
},
639641
};
640-
mocks.loadModelsConfigWithSource.mockResolvedValueOnce({
642+
mocks.loadModelsConfigWithSource.mockResolvedValue({
641643
sourceConfig: config,
642644
resolvedConfig: config,
643645
diagnostics: [],
644646
});
645-
mocks.ensureAuthProfileStore.mockReturnValueOnce({
647+
mocks.ensureAuthProfileStore.mockReturnValue({
646648
version: 1,
647649
profiles: {
648650
"google:default": {
@@ -653,7 +655,7 @@ describe("modelsListCommand forward-compat", () => {
653655
},
654656
order: {},
655657
});
656-
mocks.resolveConfiguredEntries.mockReturnValueOnce({
658+
mocks.resolveConfiguredEntries.mockReturnValue({
657659
entries: [
658660
{
659661
key: "xiaomi/mimo-v2.5-pro",
@@ -663,7 +665,7 @@ describe("modelsListCommand forward-compat", () => {
663665
},
664666
],
665667
});
666-
mocks.loadModelCatalog.mockResolvedValueOnce([
668+
mocks.loadModelCatalog.mockResolvedValue([
667669
{
668670
provider: "google",
669671
id: "gemini-3.1-flash-lite",
@@ -672,23 +674,92 @@ describe("modelsListCommand forward-compat", () => {
672674
contextWindow: 1_000_000,
673675
},
674676
]);
675-
const runtime = createRuntime();
677+
}
676678

677-
await modelsListCommand({ json: true }, runtime as never);
679+
it.each([
680+
{
681+
mode: "merge",
682+
expectedKeys: [
683+
"xiaomi/mimo-v2.5-pro",
684+
"xiaomi/mimo-v2.5",
685+
"google/gemini-3.1-flash-lite",
686+
],
687+
},
688+
{
689+
mode: "replace",
690+
expectedKeys: ["xiaomi/mimo-v2.5-pro", "xiaomi/mimo-v2.5"],
691+
},
692+
] as const)(
693+
"uses $mode catalog semantics for the default list",
694+
async ({ mode, expectedKeys }) => {
695+
configureAuthenticatedCatalogScenario(mode);
696+
const runtime = createRuntime();
678697

679-
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
680-
const rows = lastPrintedRows<{ key: string; name: string; available: boolean }>();
681-
expectRowKeys(rows, [
682-
"xiaomi/mimo-v2.5-pro",
683-
"xiaomi/mimo-v2.5",
684-
"google/gemini-3.1-flash-lite",
685-
]);
686-
expectRowFields(rows, "xiaomi/mimo-v2.5-pro", { name: "MiMo V2.5 Pro" });
687-
expectRowFields(rows, "xiaomi/mimo-v2.5", { name: "MiMo V2.5" });
688-
expectRowFields(rows, "google/gemini-3.1-flash-lite", {
698+
await modelsListCommand({ json: true }, runtime as never);
699+
700+
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
701+
const rows = lastPrintedRows<{ key: string; name: string; available: boolean }>();
702+
expectRowKeys(rows, [...expectedKeys]);
703+
expectRowFields(rows, "xiaomi/mimo-v2.5-pro", { name: "MiMo V2.5 Pro" });
704+
expectRowFields(rows, "xiaomi/mimo-v2.5", { name: "MiMo V2.5" });
705+
if (mode === "merge") {
706+
expectRowFields(rows, "google/gemini-3.1-flash-lite", {
707+
name: "Gemini 3.1 Flash Lite",
708+
available: true,
709+
});
710+
expect(mocks.loadModelCatalog).toHaveBeenCalledOnce();
711+
} else {
712+
expect(mocks.loadModelCatalog).not.toHaveBeenCalled();
713+
}
714+
},
715+
);
716+
717+
it("preserves explicit all and provider browsing in replace mode", async () => {
718+
configureAuthenticatedCatalogScenario("replace");
719+
const googleModelKey = "google/gemini-3.1-flash-lite";
720+
const googleModel = {
721+
provider: "google",
722+
id: "gemini-3.1-flash-lite",
689723
name: "Gemini 3.1 Flash Lite",
690-
available: true,
724+
api: "google-generative-ai",
725+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
726+
input: ["text"],
727+
contextWindow: 1_000_000,
728+
maxTokens: 65_536,
729+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
730+
};
731+
mocks.loadModelRegistry.mockResolvedValueOnce({
732+
models: [googleModel],
733+
availableKeys: new Set([googleModelKey]),
734+
registry: { getAll: () => [googleModel] },
691735
});
736+
const runtime = createRuntime();
737+
738+
await modelsListCommand({ all: true, json: true }, runtime as never);
739+
740+
expectRowFields(
741+
lastPrintedRows<{ key: string; available: boolean }>(),
742+
googleModelKey,
743+
{ available: true },
744+
);
745+
expect(mocks.loadModelRegistry).toHaveBeenCalledOnce();
746+
747+
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([
748+
{
749+
...googleModel,
750+
ref: googleModelKey,
751+
mergeKey: "google::gemini-3.1-flash-lite",
752+
source: "manifest",
753+
reasoning: false,
754+
status: "available",
755+
},
756+
]);
757+
758+
await modelsListCommand({ provider: "google", json: true }, runtime as never);
759+
760+
expectRowKeys(lastPrintedRows<{ key: string }>(), [googleModelKey]);
761+
expect(mocks.loadModelRegistry).toHaveBeenCalledOnce();
762+
expect(mocks.loadModelCatalog).not.toHaveBeenCalled();
692763
});
693764

694765
it("does not mark configured codex model as missing when forward-compat can build a fallback", async () => {

src/commands/models/list.rows.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,11 @@ export async function appendConfiguredProviderRows(params: {
360360
params.context.cfg.models?.providers ?? {},
361361
)) {
362362
for (const configuredModel of providerConfig.models ?? []) {
363-
if (!shouldListConfiguredProviderModel({ providerConfig, model: configuredModel })) {
363+
// Replace-mode rows are canonical even when runtime infers their transport.
364+
if (
365+
params.context.cfg.models?.mode !== "replace" &&
366+
!shouldListConfiguredProviderModel({ providerConfig, model: configuredModel })
367+
) {
364368
continue;
365369
}
366370
const key = modelKey(provider, configuredModel.id);
@@ -388,6 +392,10 @@ export async function appendAuthenticatedCatalogRows(params: {
388392
context: RowBuilderContext;
389393
seenKeys: Set<string>;
390394
}): Promise<void> {
395+
// Replace narrows default configured lists; explicit browse paths skip this helper.
396+
if (params.context.cfg.models?.mode === "replace") {
397+
return;
398+
}
391399
const { loadModelCatalog } = await loadModelCatalogModule();
392400
const catalog = await loadModelCatalog({
393401
config: params.context.cfg,

0 commit comments

Comments
 (0)