From 6e044b5f2f49cc7bd416093b952eacb3fcb4b68c Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Wed, 21 Jan 2026 13:14:18 -0800 Subject: [PATCH] fix(models): include configured providers/models + ignore page with all --- src/auto-reply/reply/commands-models.test.ts | 35 +++++++++++++ src/auto-reply/reply/commands-models.ts | 55 +++++++++++++++++--- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/auto-reply/reply/commands-models.test.ts b/src/auto-reply/reply/commands-models.test.ts index d225db991..c32abfc7d 100644 --- a/src/auto-reply/reply/commands-models.test.ts +++ b/src/auto-reply/reply/commands-models.test.ts @@ -80,6 +80,16 @@ describe("/models command", () => { expect(result.reply?.text).toContain("All: /models anthropic all"); }); + it("ignores page argument when all flag is present", async () => { + const params = buildParams("/models anthropic 3 all", cfg); + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Models (anthropic)"); + expect(result.reply?.text).toContain("page 1/1"); + expect(result.reply?.text).toContain("anthropic/claude-opus-4-5"); + expect(result.reply?.text).not.toContain("Page out of range"); + }); + it("errors on out-of-range pages", async () => { const params = buildParams("/models anthropic 4", cfg); const result = await handleCommands(params); @@ -95,4 +105,29 @@ describe("/models command", () => { expect(result.reply?.text).toContain("Unknown provider"); expect(result.reply?.text).toContain("Available providers"); }); + + it("lists configured models outside the curated catalog", async () => { + const customCfg = { + commands: { text: true }, + agents: { + defaults: { + model: { + primary: "localai/ultra-chat", + fallbacks: ["anthropic/claude-opus-4-5"], + }, + imageModel: "visionpro/studio-v1", + }, + }, + } as unknown as ClawdbotConfig; + + const providerList = await handleCommands(buildParams("/models", customCfg)); + expect(providerList.reply?.text).toContain("localai"); + expect(providerList.reply?.text).toContain("visionpro"); + + const result = await handleCommands(buildParams("/models localai", customCfg)); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Models (localai)"); + expect(result.reply?.text).toContain("localai/ultra-chat"); + expect(result.reply?.text).not.toContain("Unknown provider"); + }); }); diff --git a/src/auto-reply/reply/commands-models.ts b/src/auto-reply/reply/commands-models.ts index 3bb545746..652c8d0e8 100644 --- a/src/auto-reply/reply/commands-models.ts +++ b/src/auto-reply/reply/commands-models.ts @@ -1,8 +1,10 @@ import { loadModelCatalog } from "../../agents/model-catalog.js"; import { buildAllowedModelSet, + buildModelAliasIndex, normalizeProviderId, resolveConfiguredModelRef, + resolveModelRefFromString, } from "../../agents/model-selection.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; import type { ReplyPayload } from "../types.js"; @@ -89,6 +91,11 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma defaultModel: resolvedDefault.model, }); + const aliasIndex = buildModelAliasIndex({ + cfg: params.cfg, + defaultProvider: resolvedDefault.provider, + }); + const byProvider = new Map>(); const add = (p: string, m: string) => { const key = normalizeProviderId(p); @@ -97,22 +104,54 @@ export const handleModelsCommand: CommandHandler = async (params, allowTextComma byProvider.set(key, set); }; + const addRawModelRef = (raw?: string) => { + const trimmed = raw?.trim(); + if (!trimmed) return; + const resolved = resolveModelRefFromString({ + raw: trimmed, + defaultProvider: resolvedDefault.provider, + aliasIndex, + }); + if (!resolved) return; + add(resolved.ref.provider, resolved.ref.model); + }; + + const addModelConfigEntries = () => { + const modelConfig = params.cfg.agents?.defaults?.model; + if (typeof modelConfig === "string") { + addRawModelRef(modelConfig); + } else if (modelConfig && typeof modelConfig === "object") { + addRawModelRef(modelConfig.primary); + for (const fallback of modelConfig.fallbacks ?? []) { + addRawModelRef(fallback); + } + } + + const imageConfig = params.cfg.agents?.defaults?.imageModel; + if (typeof imageConfig === "string") { + addRawModelRef(imageConfig); + } else if (imageConfig && typeof imageConfig === "object") { + addRawModelRef(imageConfig.primary); + for (const fallback of imageConfig.fallbacks ?? []) { + addRawModelRef(fallback); + } + } + }; + for (const entry of allowed.allowedCatalog) { add(entry.provider, entry.id); } // Include config-only allowlist keys that aren't in the curated catalog. for (const raw of Object.keys(params.cfg.agents?.defaults?.models ?? {})) { - const rawKey = String(raw ?? "").trim(); - if (!rawKey) continue; - const slash = rawKey.indexOf("/"); - if (slash === -1) continue; - const p = normalizeProviderId(rawKey.slice(0, slash)); - const m = rawKey.slice(slash + 1).trim(); - if (!p || !m) continue; - add(p, m); + addRawModelRef(raw); } + // Ensure configured defaults/fallbacks/image models show up even when the + // curated catalog doesn't know about them (custom providers, dev builds, etc.). + add(resolvedDefault.provider, resolvedDefault.model); + addModelConfigEntries(); + const providers = [...byProvider.keys()].sort(); if (!provider) {