fix(auto-reply): show config models in /model
This commit is contained in:
@@ -66,7 +66,8 @@
|
|||||||
- Config: expand `~` in `CLAWDBOT_CONFIG_PATH` and common path-like config fields (including `plugins.load.paths`); guard invalid `$include` paths. (#731) — thanks @pasogott.
|
- Config: expand `~` in `CLAWDBOT_CONFIG_PATH` and common path-like config fields (including `plugins.load.paths`); guard invalid `$include` paths. (#731) — thanks @pasogott.
|
||||||
- Agents: stop pre-creating session transcripts so first user messages persist in JSONL history.
|
- Agents: stop pre-creating session transcripts so first user messages persist in JSONL history.
|
||||||
- Agents: skip pre-compaction memory flush when the session workspace is read-only.
|
- Agents: skip pre-compaction memory flush when the session workspace is read-only.
|
||||||
- Auto-reply: ignore inline `/status` directives unless the message is directive-only.
|
- Auto-reply: allow inline `/status` for allowlisted senders (stripped before the model); unauthorized senders see it as plain text.
|
||||||
|
- Auto-reply: include config-only allowlisted models in `/model` even when the catalog is partial.
|
||||||
- Auto-reply: align `/think` default display with model reasoning defaults. (#751) — thanks @gabriel-trigo.
|
- Auto-reply: align `/think` default display with model reasoning defaults. (#751) — thanks @gabriel-trigo.
|
||||||
- Auto-reply: flush block reply buffers on tool boundaries. (#750) — thanks @sebslight.
|
- Auto-reply: flush block reply buffers on tool boundaries. (#750) — thanks @sebslight.
|
||||||
- Auto-reply: allow sender fallback for command authorization when `SenderId` is empty (WhatsApp self-chat). (#755) — thanks @juanpablodlc.
|
- Auto-reply: allow sender fallback for command authorization when `SenderId` is empty (WhatsApp self-chat). (#755) — thanks @juanpablodlc.
|
||||||
|
|||||||
@@ -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 () => {
|
it("does not repeat missing auth labels on /model list", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ import {
|
|||||||
persistInlineDirectives,
|
persistInlineDirectives,
|
||||||
resolveDefaultModel,
|
resolveDefaultModel,
|
||||||
} from "./reply/directive-handling.js";
|
} from "./reply/directive-handling.js";
|
||||||
import { extractStatusDirective } from "./reply/directives.js";
|
|
||||||
import {
|
import {
|
||||||
buildGroupIntro,
|
buildGroupIntro,
|
||||||
defaultGroupActivation,
|
defaultGroupActivation,
|
||||||
@@ -491,15 +490,6 @@ export async function getReplyFromConfig(
|
|||||||
let parsedDirectives = parseInlineDirectives(commandSource, {
|
let parsedDirectives = parseInlineDirectives(commandSource, {
|
||||||
modelAliases: configuredAliases,
|
modelAliases: configuredAliases,
|
||||||
});
|
});
|
||||||
const hasInlineStatus =
|
|
||||||
parsedDirectives.hasStatusDirective &&
|
|
||||||
parsedDirectives.cleaned.trim().length > 0;
|
|
||||||
if (hasInlineStatus) {
|
|
||||||
parsedDirectives = {
|
|
||||||
...parsedDirectives,
|
|
||||||
hasStatusDirective: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
isGroup &&
|
isGroup &&
|
||||||
ctx.WasMentioned !== true &&
|
ctx.WasMentioned !== true &&
|
||||||
@@ -702,7 +692,8 @@ export async function getReplyFromConfig(
|
|||||||
: directives.rawModelDirective;
|
: directives.rawModelDirective;
|
||||||
|
|
||||||
if (!command.isAuthorizedSender) {
|
if (!command.isAuthorizedSender) {
|
||||||
cleanedBody = extractStatusDirective(existingBody).cleaned;
|
// Treat slash tokens as plain text for unauthorized senders.
|
||||||
|
cleanedBody = existingBody;
|
||||||
sessionCtx.Body = cleanedBody;
|
sessionCtx.Body = cleanedBody;
|
||||||
sessionCtx.BodyStripped = cleanedBody;
|
sessionCtx.BodyStripped = cleanedBody;
|
||||||
directives = {
|
directives = {
|
||||||
|
|||||||
@@ -664,9 +664,24 @@ export async function handleDirectiveOnly(params: {
|
|||||||
defaultModel,
|
defaultModel,
|
||||||
});
|
});
|
||||||
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
|
const pickerCatalog: ModelPickerCatalogEntry[] = (() => {
|
||||||
if (allowedModelCatalog.length > 0) return allowedModelCatalog;
|
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
const out: ModelPickerCatalogEntry[] = [];
|
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(
|
for (const raw of Object.keys(
|
||||||
params.cfg.agents?.defaults?.models ?? {},
|
params.cfg.agents?.defaults?.models ?? {},
|
||||||
)) {
|
)) {
|
||||||
@@ -676,24 +691,22 @@ export async function handleDirectiveOnly(params: {
|
|||||||
aliasIndex,
|
aliasIndex,
|
||||||
});
|
});
|
||||||
if (!resolved) continue;
|
if (!resolved) continue;
|
||||||
const key = modelKey(resolved.ref.provider, resolved.ref.model);
|
push({
|
||||||
if (keys.has(key)) continue;
|
|
||||||
keys.add(key);
|
|
||||||
out.push({
|
|
||||||
provider: resolved.ref.provider,
|
provider: resolved.ref.provider,
|
||||||
id: resolved.ref.model,
|
id: resolved.ref.model,
|
||||||
name: resolved.ref.model,
|
name: resolved.ref.model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (out.length === 0 && resolvedDefault.model) {
|
|
||||||
const key = modelKey(resolvedDefault.provider, resolvedDefault.model);
|
// Ensure the configured default is always present (even when no allowlist).
|
||||||
keys.add(key);
|
if (resolvedDefault.model) {
|
||||||
out.push({
|
push({
|
||||||
provider: resolvedDefault.provider,
|
provider: resolvedDefault.provider,
|
||||||
id: resolvedDefault.model,
|
id: resolvedDefault.model,
|
||||||
name: resolvedDefault.model,
|
name: resolvedDefault.model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user