diff --git a/src/agents/model-ref-profile.test.ts b/src/agents/model-ref-profile.test.ts index a8fb98b4bffe..b331ce938aec 100644 --- a/src/agents/model-ref-profile.test.ts +++ b/src/agents/model-ref-profile.test.ts @@ -80,6 +80,22 @@ describe("splitTrailingAuthProfile", () => { }); }); + it("keeps @iq* importance-quantization suffixes in model ids", () => { + expect(splitTrailingAuthProfile("lmstudio/qwen3.6-27b@iq3_xxs")).toEqual({ + model: "lmstudio/qwen3.6-27b@iq3_xxs", + }); + expect(splitTrailingAuthProfile("lmstudio/qwen3.6-27b@iq4_xs")).toEqual({ + model: "lmstudio/qwen3.6-27b@iq4_xs", + }); + }); + + it("supports auth profiles after @iq* quant suffixes", () => { + expect(splitTrailingAuthProfile("lmstudio/qwen3.6-27b@iq3_xxs@work")).toEqual({ + model: "lmstudio/qwen3.6-27b@iq3_xxs", + profile: "work", + }); + }); + it("keeps @4bit/@8bit quant suffixes in model ids", () => { expect(splitTrailingAuthProfile("lmstudio-mb-pro/gemma-4-31b@4bit")).toEqual({ model: "lmstudio-mb-pro/gemma-4-31b@4bit", diff --git a/src/agents/model-ref-profile.ts b/src/agents/model-ref-profile.ts index 2469bf228afb..3114ed74e57e 100644 --- a/src/agents/model-ref-profile.ts +++ b/src/agents/model-ref-profile.ts @@ -29,9 +29,12 @@ export function splitTrailingAuthProfile(raw: string): { // of the model id. These often use '@' (ex: gemma-4-31b-it@q8_0) which would // otherwise be misinterpreted as an auth profile delimiter. // + // Covers standard GGUF quant tags (q4_0, q8_0, q4_k_xl, …) and importance- + // quantization variants (iq3_xxs, iq4_xs, …) used by llama.cpp / LM Studio. + // // If an auth profile is needed, it can still be specified as a second suffix: - // lmstudio/foo@q8_0@work - if (/^(?:q\d+(?:_[a-z0-9]+)*|\d+bit)(?:@|$)/i.test(suffixAfterDelimiter())) { + // lmstudio/foo@q8_0@work lmstudio/foo@iq3_xxs@work + if (/^(?:i?q\d+(?:_[a-z0-9]+)*|\d+bit)(?:@|$)/i.test(suffixAfterDelimiter())) { const nextDelimiter = trimmed.indexOf("@", profileDelimiter + 1); if (nextDelimiter < 0) { return { model: trimmed }; diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index b7de04c4f7be..134f18f8b710 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -894,6 +894,28 @@ describe("model-selection", () => { }); }); + it("preserves LM Studio @iq* quant suffixes", () => { + const resolved = resolveModelRefFromString({ + raw: "lmstudio/qwen3.6-27b@iq3_xxs", + defaultProvider: "anthropic", + }); + expect(resolved?.ref).toEqual({ + provider: "lmstudio", + model: "qwen3.6-27b@iq3_xxs", + }); + }); + + it("splits trailing profile suffix after LM Studio @iq* quant suffixes", () => { + const resolved = resolveModelRefFromString({ + raw: "lmstudio/qwen3.6-27b@iq3_xxs@work", + defaultProvider: "anthropic", + }); + expect(resolved?.ref).toEqual({ + provider: "lmstudio", + model: "qwen3.6-27b@iq3_xxs", + }); + }); + it("strips profile suffix before alias resolution", () => { const index = { byAlias: new Map([ diff --git a/src/auto-reply/model.test.ts b/src/auto-reply/model.test.ts index 5352f892280f..c43e46bdc0e0 100644 --- a/src/auto-reply/model.test.ts +++ b/src/auto-reply/model.test.ts @@ -72,6 +72,20 @@ describe("extractModelDirective", () => { expect(result.rawProfile).toBe("cf:default"); }); + it("keeps LM Studio @iq* quant suffixes inside model ids", () => { + const result = extractModelDirective("/model lmstudio/qwen3.6-27b@iq3_xxs"); + expect(result.hasDirective).toBe(true); + expect(result.rawModel).toBe("lmstudio/qwen3.6-27b@iq3_xxs"); + expect(result.rawProfile).toBeUndefined(); + }); + + it("allows profile overrides after LM Studio @iq* quant suffixes", () => { + const result = extractModelDirective("/model lmstudio/qwen3.6-27b@iq3_xxs@work"); + expect(result.hasDirective).toBe(true); + expect(result.rawModel).toBe("lmstudio/qwen3.6-27b@iq3_xxs"); + expect(result.rawProfile).toBe("work"); + }); + it("returns no directive for plain text", () => { const result = extractModelDirective("hello world"); expect(result.hasDirective).toBe(false); diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 9528ba5dee96..ab1b41fc87e7 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -1,3 +1,4 @@ +import { splitTrailingAuthProfile } from "../agents/model-ref-profile.js"; import { normalizeProviderId } from "../agents/provider-id.js"; import { withBundledPluginVitestCompat } from "./bundled-compat.js"; import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js"; @@ -290,9 +291,7 @@ function resolveManifestRegistry(params: { } function stripModelProfileSuffix(value: string): string { - const trimmed = value.trim(); - const at = trimmed.indexOf("@"); - return at <= 0 ? trimmed : trimmed.slice(0, at).trim(); + return splitTrailingAuthProfile(value).model; } function splitExplicitModelRef(rawModel: string): { provider?: string; modelId: string } | null {