fix: preserve LM Studio quant model refs (#71486)

This commit is contained in:
Peter Steinberger
2026-04-26 01:30:22 +01:00
parent 5bb78ea7ed
commit 753ccf615c
5 changed files with 89 additions and 2 deletions

View File

@@ -70,6 +70,9 @@ Docs: https://docs.openclaw.ai
- Gateway/Fly.io: seed Control UI allowed origins from the actual runtime
bind and port so CLI-driven non-loopback starts do not crash before config
exists. Fixes #71823.
- Models/LM Studio: preserve `@iq*` quant suffixes in model refs and provider
matching so `/model lmstudio/...@iq3_xxs` keeps the exact LM Studio variant.
Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.
- Feishu: accept Schema 2.0 card action callbacks that report
`context.open_chat_id` instead of legacy `context.chat_id`, so button
callbacks no longer drop as malformed. Fixes #71670. Thanks @eddy1068.

View File

@@ -29,8 +29,8 @@ 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.
// 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 lmstudio/foo@iq3_xxs@work

View File

@@ -894,6 +894,30 @@ 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([

View File

@@ -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);

View File

@@ -1321,6 +1321,52 @@ describe("resolvePluginProviders", () => {
expectModelOwningPluginIds("gpt-5.4", ["workspace-openai"]);
});
it("preserves LM Studio @iq* quant suffixes when resolving model-owned provider plugins", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "lmstudio",
providerIds: ["lmstudio"],
modelSupport: {
modelPatterns: ["^qwen3\\.6-27b@iq3_xxs$"],
},
}),
]);
const provider: ProviderPlugin = {
id: "lmstudio",
label: "LM Studio",
auth: [],
};
const registry = createEmptyPluginRegistry();
registry.providers.push({ pluginId: "lmstudio", provider, source: "bundled" });
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
expectModelOwningPluginIds("qwen3.6-27b@iq3_xxs", ["lmstudio"]);
expectModelOwningPluginIds("qwen3.6-27b", undefined);
const providers = resolvePluginProviders({
config: {},
modelRefs: ["qwen3.6-27b@iq3_xxs"],
bundledProviderAllowlistCompat: true,
});
expectResolvedProviders(providers, [
{ id: "lmstudio", label: "LM Studio", auth: [], pluginId: "lmstudio" },
]);
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["lmstudio"],
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["lmstudio"],
entries: {
lmstudio: { enabled: true },
},
}),
}),
}),
);
});
it("auto-loads a model-owned provider plugin from shorthand model refs", () => {
setManifestPlugins([
createManifestProviderPlugin({