fix(auto-reply): show config models in /model

This commit is contained in:
Peter Steinberger
2026-01-12 07:31:20 +00:00
parent 7466575120
commit 097e66391f
4 changed files with 78 additions and 21 deletions

View File

@@ -1551,6 +1551,58 @@ describe("directive behavior", () => {
});
});
it("merges config allowlist models even when catalog is present", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
// Catalog present but missing custom providers: /model should still include
// allowlisted provider/model keys from config.
vi.mocked(loadModelCatalog).mockResolvedValueOnce([
{
provider: "anthropic",
id: "claude-opus-4-5",
name: "Claude Opus 4.5",
},
{ provider: "openai", id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
]);
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model list", From: "+1222", To: "+1222" },
{},
{
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-5" },
workspace: path.join(home, "clawd"),
models: {
"anthropic/claude-opus-4-5": {},
"openai/gpt-4.1-mini": {},
"minimax/MiniMax-M2.1": { alias: "minimax" },
},
},
},
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
api: "anthropic-messages",
models: [{ id: "MiniMax-M2.1", name: "MiniMax M2.1" }],
},
},
},
session: { store: storePath },
},
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("claude-opus-4-5 — anthropic");
expect(text).toContain("gpt-4.1-mini — openai");
expect(text).toContain("MiniMax-M2.1 — minimax");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("does not repeat missing auth labels on /model list", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();

View File

@@ -64,7 +64,6 @@ import {
persistInlineDirectives,
resolveDefaultModel,
} from "./reply/directive-handling.js";
import { extractStatusDirective } from "./reply/directives.js";
import {
buildGroupIntro,
defaultGroupActivation,
@@ -491,15 +490,6 @@ export async function getReplyFromConfig(
let parsedDirectives = parseInlineDirectives(commandSource, {
modelAliases: configuredAliases,
});
const hasInlineStatus =
parsedDirectives.hasStatusDirective &&
parsedDirectives.cleaned.trim().length > 0;
if (hasInlineStatus) {
parsedDirectives = {
...parsedDirectives,
hasStatusDirective: false,
};
}
if (
isGroup &&
ctx.WasMentioned !== true &&
@@ -702,7 +692,8 @@ export async function getReplyFromConfig(
: directives.rawModelDirective;
if (!command.isAuthorizedSender) {
cleanedBody = extractStatusDirective(existingBody).cleaned;
// Treat slash tokens as plain text for unauthorized senders.
cleanedBody = existingBody;
sessionCtx.Body = cleanedBody;
sessionCtx.BodyStripped = cleanedBody;
directives = {

View File

@@ -664,9 +664,24 @@ export async function handleDirectiveOnly(params: {
defaultModel,
});
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
if (allowedModelCatalog.length > 0) return allowedModelCatalog;
const keys = new Set<string>();
const out: ModelPickerCatalogEntry[] = [];
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
const key = modelKey(provider, id);
if (keys.has(key)) return;
keys.add(key);
out.push({ provider, id, name: entry.name });
};
// Prefer catalog entries (when available), but always merge in config-only
// allowlist entries. This keeps custom providers/models visible in /model.
for (const entry of allowedModelCatalog) push(entry);
// Merge any configured allowlist keys that the catalog doesn't know about.
for (const raw of Object.keys(
params.cfg.agents?.defaults?.models ?? {},
)) {
@@ -676,24 +691,22 @@ export async function handleDirectiveOnly(params: {
aliasIndex,
});
if (!resolved) continue;
const key = modelKey(resolved.ref.provider, resolved.ref.model);
if (keys.has(key)) continue;
keys.add(key);
out.push({
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
name: resolved.ref.model,
});
}
if (out.length === 0 && resolvedDefault.model) {
const key = modelKey(resolvedDefault.provider, resolvedDefault.model);
keys.add(key);
out.push({
// Ensure the configured default is always present (even when no allowlist).
if (resolvedDefault.model) {
push({
provider: resolvedDefault.provider,
id: resolvedDefault.model,
name: resolvedDefault.model,
});
}
return out;
})();