Files
openclaw/extensions/lmstudio/index.test.ts
Rugved Somwanshi 0cfb83edfa feat: LM Studio Integration (#53248)
* Feat: LM Studio Integration

* Format

* Support usage in streaming true

Fix token count

* Add custom window check

* Drop max tokens fallback

* tweak docs

Update generated

* Avoid error if stale header does not resolve

* Fix test

* Fix test

* Fix rebase issues

Trim code

* Fix tests

Drop keyless

Fixes

* Fix linter issues in tests

* Update generated artifacts

* Do not have fatal header resoltuion for discovery

* Do the same for API key as well

* fix: honor lmstudio preload runtime auth

* fix: clear stale lmstudio header auth

* fix: lazy-load lmstudio runtime facade

* fix: preserve lmstudio shared synthetic auth

* fix: clear stale lmstudio header auth in discovery

* fix: prefer lmstudio header auth for discovery

* fix: honor lmstudio header auth in warmup paths

* fix: clear stale lmstudio profile auth

* fix: ignore lmstudio env auth on header migration

* fix: use local lmstudio setup seam

* fix: resolve lmstudio rebase fallout

---------

Co-authored-by: Frank Yang <frank.ekn@gmail.com>
2026-04-13 15:22:44 +08:00

195 lines
5.3 KiB
TypeScript

import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { CUSTOM_LOCAL_AUTH_MARKER } from "openclaw/plugin-sdk/provider-auth";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { capturePluginRegistration } from "openclaw/plugin-sdk/testing";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
import { LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER } from "./src/defaults.js";
function registerProvider() {
const captured = capturePluginRegistration(plugin);
const provider = captured.providers[0];
expect(provider?.id).toBe("lmstudio");
return provider;
}
function createRemoteProviderConfig(overrides?: Partial<ModelProviderConfig>): ModelProviderConfig {
return {
api: "openai-completions",
baseUrl: "http://lmstudio.internal:1234/v1",
models: [
{
id: "qwen/qwen3.5-9b",
name: "Qwen 3.5 9B",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 131072,
maxTokens: 8192,
},
],
...overrides,
};
}
describe("lmstudio plugin", () => {
it("canonicalizes base URLs during provider normalization", () => {
const provider = registerProvider();
expect(
provider?.normalizeConfig?.({
provider: "lmstudio",
providerConfig: createRemoteProviderConfig({
baseUrl: "http://localhost:1234/api/v1/",
}),
}),
).toMatchObject({
baseUrl: "http://localhost:1234/v1",
});
});
it("synthesizes placeholder auth for configured lmstudio models without API key auth", () => {
const provider = registerProvider();
expect(
provider?.resolveSyntheticAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig({
headers: {
"X-Proxy-Auth": "proxy-token",
},
}),
}),
).toEqual({
apiKey: CUSTOM_LOCAL_AUTH_MARKER,
source: "models.providers.lmstudio (synthetic local key)",
mode: "api-key",
});
});
it("still synthesizes placeholder auth when explicit api-key auth has no key", () => {
const provider = registerProvider();
expect(
provider?.resolveSyntheticAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig({
auth: "api-key",
}),
}),
).toEqual({
apiKey: CUSTOM_LOCAL_AUTH_MARKER,
source: "models.providers.lmstudio (synthetic local key)",
mode: "api-key",
});
});
it("does not synthesize placeholder auth when Authorization header is configured", () => {
const provider = registerProvider();
expect(
provider?.resolveSyntheticAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig({
headers: {
Authorization: "Bearer proxy-token",
},
}),
}),
).toBeUndefined();
});
it("defers stored lmstudio-local profile auth so real credentials can win", () => {
const provider = registerProvider();
expect(
provider?.shouldDeferSyntheticProfileAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig(),
resolvedApiKey: LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER,
}),
).toBe(true);
expect(
provider?.shouldDeferSyntheticProfileAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig(),
resolvedApiKey: CUSTOM_LOCAL_AUTH_MARKER,
}),
).toBe(true);
expect(
provider?.shouldDeferSyntheticProfileAuth?.({
provider: "lmstudio",
config: {},
providerConfig: createRemoteProviderConfig(),
resolvedApiKey: "lmstudio-real-key",
}),
).toBe(false);
});
it("augments the catalog with configured lmstudio models", () => {
const provider = registerProvider();
const config = {
models: {
providers: {
lmstudio: {
models: [
{
id: "qwen3-8b-instruct",
name: "Qwen 3 8B Instruct",
contextWindow: 32768,
contextTokens: 8192,
reasoning: true,
input: ["text", "image"],
},
{
id: "phi-4",
},
{
id: " ",
name: "ignored",
},
],
},
},
},
} as unknown as OpenClawConfig;
expect(
provider?.augmentModelCatalog?.({
config,
agentDir: "/tmp/openclaw",
env: {},
entries: [],
}),
).toEqual([
{
provider: "lmstudio",
id: "qwen3-8b-instruct",
name: "Qwen 3 8B Instruct",
compat: { supportsUsageInStreaming: true },
contextWindow: 32768,
contextTokens: 8192,
reasoning: true,
input: ["text", "image"],
},
{
provider: "lmstudio",
id: "phi-4",
name: "phi-4",
compat: { supportsUsageInStreaming: true },
contextWindow: undefined,
contextTokens: undefined,
reasoning: undefined,
input: undefined,
},
]);
});
});