fix: treat /model status as model list

This commit is contained in:
Peter Steinberger
2025-12-27 12:10:44 +00:00
parent c0fb814658
commit cae78b3f91
3 changed files with 38 additions and 6 deletions

View File

@@ -42,6 +42,7 @@
- LM Studio responses API: tools payloads no longer include `strict: null`, and LM Studio no longer gets forced `<think>/<final>` tags. - LM Studio responses API: tools payloads no longer include `strict: null`, and LM Studio no longer gets forced `<think>/<final>` tags.
- Identity emoji no longer auto-prefixes replies (set `messages.responsePrefix` explicitly if desired). - Identity emoji no longer auto-prefixes replies (set `messages.responsePrefix` explicitly if desired).
- Model switches now enqueue a system event so the next run knows the active model. - Model switches now enqueue a system event so the next run knows the active model.
- `/model status` now lists available models (same as `/model`).
- `process log` pagination is now line-based (omit `offset` to grab the last N lines). - `process log` pagination is now line-based (omit `offset` to grab the last N lines).
- macOS WebChat: assistant bubbles now update correctly when toggling light/dark mode. - macOS WebChat: assistant bubbles now update correctly when toggling light/dark mode.
- macOS: avoid spawning a duplicate gateway process when an external listener already exists. - macOS: avoid spawning a duplicate gateway process when an external listener already exists.

View File

@@ -366,6 +366,32 @@ describe("directive parsing", () => {
}); });
}); });
it("lists allowlisted models on /model status", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model status", From: "+1222", To: "+1222" },
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
},
session: { store: storePath },
},
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("anthropic/claude-opus-4-5");
expect(text).toContain("openai/gpt-4.1-mini");
expect(text).not.toContain("claude-sonnet-4-1");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("sets model override on /model directive", async () => { it("sets model override on /model directive", async () => {
await withTempHome(async (home) => { await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset(); vi.mocked(runEmbeddedPiAgent).mockReset();

View File

@@ -554,6 +554,11 @@ export async function getReplyFromConfig(
alias alias
? `Model switched to ${alias} (${label}).` ? `Model switched to ${alias} (${label}).`
: `Model switched to ${label}.`; : `Model switched to ${label}.`;
const isModelListAlias =
hasModelDirective && rawModelDirective?.trim().toLowerCase() === "status";
const effectiveModelDirective = isModelListAlias
? undefined
: rawModelDirective;
const directiveOnly = (() => { const directiveOnly = (() => {
if ( if (
@@ -569,7 +574,7 @@ export async function getReplyFromConfig(
})(); })();
if (directiveOnly) { if (directiveOnly) {
if (hasModelDirective && !rawModelDirective) { if (hasModelDirective && (!rawModelDirective || isModelListAlias)) {
if (allowedModelCatalog.length === 0) { if (allowedModelCatalog.length === 0) {
cleanupTyping(); cleanupTyping();
return { text: "No models available." }; return { text: "No models available." };
@@ -620,16 +625,16 @@ export async function getReplyFromConfig(
let modelSelection: let modelSelection:
| { provider: string; model: string; isDefault: boolean; alias?: string } | { provider: string; model: string; isDefault: boolean; alias?: string }
| undefined; | undefined;
if (hasModelDirective && rawModelDirective) { if (hasModelDirective && effectiveModelDirective) {
const resolved = resolveModelRefFromString({ const resolved = resolveModelRefFromString({
raw: rawModelDirective, raw: effectiveModelDirective,
defaultProvider, defaultProvider,
aliasIndex, aliasIndex,
}); });
if (!resolved) { if (!resolved) {
cleanupTyping(); cleanupTyping();
return { return {
text: `Unrecognized model "${rawModelDirective}". Use /model to list available models.`, text: `Unrecognized model "${effectiveModelDirective}". Use /model to list available models.`,
}; };
} }
const key = modelKey(resolved.ref.provider, resolved.ref.model); const key = modelKey(resolved.ref.provider, resolved.ref.model);
@@ -742,9 +747,9 @@ export async function getReplyFromConfig(
} }
updated = true; updated = true;
} }
if (hasModelDirective && rawModelDirective) { if (hasModelDirective && effectiveModelDirective) {
const resolved = resolveModelRefFromString({ const resolved = resolveModelRefFromString({
raw: rawModelDirective, raw: effectiveModelDirective,
defaultProvider, defaultProvider,
aliasIndex, aliasIndex,
}); });