fix: refine model directive handling
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
|
- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
|
||||||
- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
|
- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
|
||||||
- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
|
- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
|
||||||
|
- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
|
||||||
|
|
||||||
## 2026.1.22
|
## 2026.1.22
|
||||||
|
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ describe("extractModelDirective", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("edge cases", () => {
|
describe("edge cases", () => {
|
||||||
it("preserves spacing when /model is followed by a path segment", () => {
|
it("absorbs path-like segments when /model includes extra slashes", () => {
|
||||||
const result = extractModelDirective("thats not /model gpt-5/tmp/hello");
|
const result = extractModelDirective("thats not /model gpt-5/tmp/hello");
|
||||||
expect(result.hasDirective).toBe(true);
|
expect(result.hasDirective).toBe(true);
|
||||||
expect(result.cleaned).toBe("thats not /hello");
|
expect(result.cleaned).toBe("thats not");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles alias with special regex characters", () => {
|
it("handles alias with special regex characters", () => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function extractModelDirective(
|
|||||||
if (!body) return { cleaned: "", hasDirective: false };
|
if (!body) return { cleaned: "", hasDirective: false };
|
||||||
|
|
||||||
const modelMatch = body.match(
|
const modelMatch = body.match(
|
||||||
/(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i,
|
/(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i,
|
||||||
);
|
);
|
||||||
|
|
||||||
const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean);
|
const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean);
|
||||||
|
|||||||
@@ -84,9 +84,9 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
expect(text).toContain("Model listing moved.");
|
||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("Use: /models (providers) or /models <provider> (models)");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("Switch: /model <provider/model>");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -115,9 +115,9 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
expect(text).toContain("Current: anthropic/claude-opus-4-5");
|
||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("Browse: /models (providers) or /models <provider> (models)");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("More: /model status");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -150,10 +150,9 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("Model listing moved.");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("Use: /models (providers) or /models <provider> (models)");
|
||||||
expect(text).toContain("minimax/MiniMax-M2.1");
|
expect(text).toContain("Switch: /model <provider/model>");
|
||||||
expect(text).toContain("xai/grok-4");
|
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -202,9 +201,9 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("Model listing moved.");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("Use: /models (providers) or /models <provider> (models)");
|
||||||
expect(text).toContain("minimax/MiniMax-M2.1");
|
expect(text).toContain("Switch: /model <provider/model>");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -231,6 +230,7 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Model listing moved.");
|
||||||
expect(text).not.toContain("missing (missing)");
|
expect(text).not.toContain("missing (missing)");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,10 +211,9 @@ describe("directive behavior", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("anthropic/claude-opus-4-5");
|
expect(text).toContain("Current: anthropic/claude-opus-4-5");
|
||||||
expect(text).toContain("Pick: /model <#> or /model <provider/model>");
|
expect(text).toContain("Browse: /models (providers) or /models <provider> (models)");
|
||||||
expect(text).toContain("openai/gpt-4.1-mini");
|
expect(text).toContain("More: /model status");
|
||||||
expect(text).not.toContain("claude-sonnet-4-1");
|
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -341,16 +341,41 @@ export function resolveModelSelectionFromDirective(params: {
|
|||||||
|
|
||||||
if (resolved.selection) {
|
if (resolved.selection) {
|
||||||
const suggestion = `${resolved.selection.provider}/${resolved.selection.model}`;
|
const suggestion = `${resolved.selection.provider}/${resolved.selection.model}`;
|
||||||
return {
|
const rawHasSlash = raw.includes("/");
|
||||||
errorText: [
|
const shouldAutoSelect = (() => {
|
||||||
`Unrecognized model: ${raw}`,
|
if (!rawHasSlash) return true;
|
||||||
"",
|
const slash = raw.indexOf("/");
|
||||||
`Did you mean: ${suggestion}`,
|
if (slash <= 0) return true;
|
||||||
`Try: /model ${suggestion}`,
|
const rawProvider = normalizeProviderId(raw.slice(0, slash));
|
||||||
"",
|
const rawFragment = raw
|
||||||
"Browse: /models or /models <provider>",
|
.slice(slash + 1)
|
||||||
].join("\n"),
|
.trim()
|
||||||
};
|
.toLowerCase();
|
||||||
|
if (!rawFragment) return false;
|
||||||
|
const resolvedProvider = normalizeProviderId(resolved.selection.provider);
|
||||||
|
if (rawProvider !== resolvedProvider) return false;
|
||||||
|
const resolvedModel = resolved.selection.model.toLowerCase();
|
||||||
|
return (
|
||||||
|
resolvedModel.startsWith(rawFragment) ||
|
||||||
|
resolvedModel.includes(rawFragment) ||
|
||||||
|
rawFragment.startsWith(resolvedModel)
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (shouldAutoSelect) {
|
||||||
|
modelSelection = resolved.selection;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
errorText: [
|
||||||
|
`Unrecognized model: ${raw}`,
|
||||||
|
"",
|
||||||
|
`Did you mean: ${suggestion}`,
|
||||||
|
`Try: /model ${suggestion}`,
|
||||||
|
"",
|
||||||
|
"Browse: /models or /models <provider>",
|
||||||
|
].join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user