fix: let lmstudio skip native preload

This commit is contained in:
Peter Steinberger
2026-05-02 06:09:05 +01:00
parent cbec76c198
commit 0b3d260285
11 changed files with 110 additions and 13 deletions
+1
View File
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- Web search/Exa: accept `plugins.entries.exa.config.webSearch.baseUrl`, normalize it to the Exa `/search` endpoint, and partition cached results by endpoint. Fixes #54928 and supersedes #54939. Thanks @mrpl327 and @lyfuci.
- Web search/MiniMax: include MiniMax Search in the web-search setup flow and let `MINIMAX_API_KEY` participate in MiniMax Search auto-detection. Supersedes #65828. Thanks @Jah-yee.
- Plugins/ClawHub: preserve official source-linked trust through archive installs, so OpenClaw can install trusted ClawHub plugin packages that trigger the built-in dangerous-pattern scanner. Thanks @vincentkoc.
- Providers/LM Studio: allow `models.providers.lmstudio.params.preload: false` to skip OpenClaw's native model-load call so LM Studio JIT loading, idle TTL, and auto-evict can own model lifecycle. Fixes #75921. Thanks @garyd9.
- Telegram: inherit the process DNS result order for Bot API transport and downgrade recovered sticky IPv4 fallback promotions to debug logs, while keeping pinned-IP escalation warnings visible. Fixes #75904. Thanks @highfly-hi and @neeravmakwana.
- Web search/MiniMax: allow `MINIMAX_OAUTH_TOKEN` to satisfy MiniMax Search credentials, so OAuth-authorized MiniMax Token Plan setups do not need a separate web-search key. Fixes #65768. Thanks @kikibrian and @zhouhe-xydt.
- Providers/MiniMax: derive Coding Plan usage polling from the configured MiniMax base URL, so global setups no longer query the CN usage host. Fixes #65054. Thanks @sixone74 and @Yanhu007.
+2 -2
View File
@@ -1,4 +1,4 @@
051884bad7339a302ecb75e5f61831b1726c6f0360de27485aac76097570c808 config-baseline.json
80e6e8dce647aef2d1310de55a81d27de52cca47fc24bd7ad81b80f43a72b84c config-baseline.core.json
94f7879b0771e81973c0749c719c19283fdc26e0e42fe6536f8ee563be6a44e5 config-baseline.json
a38ea77d2f0f0188f14ce0e3a8a564ff80e51415849359042f51921eb01ec2d9 config-baseline.core.json
eab8a85eefa2792fb8b98a07698e5ec31ff0b6f8af6222767e8049dcc5c4f529 config-baseline.channel.json
6bd6c72b17801072b2d3285c82f4c21adcc95f0edffc1e6f64e767d0a07b678f config-baseline.plugin.json
+1 -1
View File
@@ -555,7 +555,7 @@ Then set a model (replace with one of the IDs returned by `http://localhost:1234
}
```
OpenClaw uses LM Studio's native `/api/v1/models` and `/api/v1/models/load` for discovery + auto-load, with `/v1/chat/completions` for inference by default. See [/providers/lmstudio](/providers/lmstudio) for setup and troubleshooting.
OpenClaw uses LM Studio's native `/api/v1/models` and `/api/v1/models/load` for discovery + auto-load, with `/v1/chat/completions` for inference by default. If you want LM Studio JIT loading, TTL, and auto-evict to own model lifecycle, set `models.providers.lmstudio.params.preload: false`. See [/providers/lmstudio](/providers/lmstudio) for setup and troubleshooting.
### Ollama
+16 -1
View File
@@ -176,7 +176,22 @@ If setup reports HTTP 401, verify your API key:
### Just-in-time model loading
LM Studio supports just-in-time (JIT) model loading, where models are loaded on first request. Make sure you have this enabled to avoid 'Model not loaded' errors.
LM Studio supports just-in-time (JIT) model loading, where models are loaded on first request. OpenClaw preloads models through LM Studio's native load endpoint by default, which helps when JIT is disabled. To let LM Studio's JIT, idle TTL, and auto-evict behavior own model lifecycle, disable OpenClaw's preload step:
```json5
{
models: {
providers: {
lmstudio: {
baseUrl: "http://localhost:1234/v1",
api: "openai-completions",
params: { preload: false },
models: [{ id: "qwen/qwen3.5-9b" }],
},
},
},
}
```
### LAN or tailnet LM Studio host
+43
View File
@@ -199,6 +199,49 @@ describe("lmstudio stream wrapper", () => {
expect(baseStream).toHaveBeenCalledTimes(1);
});
it("skips native model preload when provider params disable it", async () => {
const baseStream = buildDoneStreamFn();
const wrapped = wrapLmstudioInferencePreload({
provider: "lmstudio",
modelId: "qwen3-8b-instruct",
config: {
models: {
providers: {
lmstudio: {
baseUrl: "http://localhost:1234",
params: { preload: false },
models: [],
},
},
},
},
streamFn: baseStream,
} as never);
const events = await collectEvents(
wrapped(
{
provider: "lmstudio",
api: "openai-completions",
id: "qwen3-8b-instruct",
} as never,
{ messages: [] } as never,
undefined as never,
),
);
expect(events).toEqual([expect.objectContaining({ type: "done" })]);
expect(ensureLmstudioModelLoadedMock).not.toHaveBeenCalled();
expect(baseStream).toHaveBeenCalledTimes(1);
expect(baseStream).toHaveBeenCalledWith(
expect.objectContaining({
compat: expect.objectContaining({ supportsUsageInStreaming: true }),
}),
expect.anything(),
undefined,
);
});
it("dedupes concurrent preload requests for the same model and context", async () => {
let resolvePreload: (() => void) | undefined;
ensureLmstudioModelLoadedMock.mockImplementationOnce(
+26 -9
View File
@@ -121,6 +121,22 @@ function toRecord(value: unknown): Record<string, unknown> | undefined {
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
}
function shouldPreloadLmstudioModels(value: unknown): boolean {
const providerConfig = toRecord(value);
const params = toRecord(providerConfig?.params);
return params?.preload !== false;
}
function withLmstudioUsageCompat(model: StreamModel): StreamModel {
return {
...model,
compat: {
...(model.compat && typeof model.compat === "object" ? model.compat : {}),
supportsUsageInStreaming: true,
},
};
}
function resolveContextToolNames(context: StreamContext): Set<string> {
const tools = (context as { tools?: unknown }).tools;
if (!Array.isArray(tools)) {
@@ -381,7 +397,15 @@ export function wrapLmstudioInferencePreload(ctx: ProviderWrapStreamFnContext):
if (!modelKey) {
return underlying(model, context, options);
}
const providerBaseUrl = ctx.config?.models?.providers?.[LMSTUDIO_PROVIDER_ID]?.baseUrl;
const providerConfig = ctx.config?.models?.providers?.[LMSTUDIO_PROVIDER_ID];
if (!shouldPreloadLmstudioModels(providerConfig)) {
const stream = underlying(withLmstudioUsageCompat(model), context, options);
return (async () => {
const resolvedStream = stream instanceof Promise ? await stream : stream;
return wrapLmstudioPlainTextToolCalls(resolvedStream, context);
})();
}
const providerBaseUrl = providerConfig?.baseUrl;
const resolvedBaseUrl = resolveLmstudioInferenceBase(
typeof model.baseUrl === "string" ? model.baseUrl : providerBaseUrl,
);
@@ -454,14 +478,7 @@ export function wrapLmstudioInferencePreload(ctx: ProviderWrapStreamFnContext):
// LM Studio uses OpenAI-compatible streaming usage payloads when requested via
// `stream_options.include_usage`. Force this compat flag at call time so usage
// reporting remains enabled even when catalog entries omitted compat metadata.
const modelWithUsageCompat = {
...model,
compat: {
...(model.compat && typeof model.compat === "object" ? model.compat : {}),
supportsUsageInStreaming: true,
},
};
const stream = underlying(modelWithUsageCompat, context, options);
const stream = underlying(withLmstudioUsageCompat(model), context, options);
const resolvedStream = stream instanceof Promise ? await stream : stream;
return wrapLmstudioPlainTextToolCalls(resolvedStream, context);
})();
+15
View File
@@ -1688,6 +1688,16 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
description:
"Controls whether OpenClaw injects `options.num_ctx` for Ollama providers configured with the OpenAI-compatible adapter (`openai-completions`). Default is true. Set false only if your proxy/upstream rejects unknown `options` payload fields.",
},
params: {
type: "object",
propertyNames: {
type: "string",
},
additionalProperties: {},
title: "Model Provider Runtime Parameters",
description:
"Provider-specific runtime parameters interpreted by provider plugins. Keep keys documented by the provider, and prefer explicit provider docs over ad hoc shared assumptions.",
},
headers: {
type: "object",
propertyNames: {
@@ -26994,6 +27004,11 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
help: "Controls whether OpenClaw injects `options.num_ctx` for Ollama providers configured with the OpenAI-compatible adapter (`openai-completions`). Default is true. Set false only if your proxy/upstream rejects unknown `options` payload fields.",
tags: ["models"],
},
"models.providers.*.params": {
label: "Model Provider Runtime Parameters",
help: "Provider-specific runtime parameters interpreted by provider plugins. Keep keys documented by the provider, and prefer explicit provider docs over ad hoc shared assumptions.",
tags: ["models"],
},
"models.providers.*.headers": {
label: "Model Provider Headers",
help: "Static HTTP headers merged into provider requests for tenant routing, proxy auth, or custom gateway requirements. Use this sparingly and keep sensitive header values in secrets.",
+2
View File
@@ -866,6 +866,8 @@ export const FIELD_HELP: Record<string, string> = {
"Optional per-provider model request timeout in seconds. Applies to provider HTTP fetches, including connect, headers, body, and total request abort handling. Use this for slow local or self-hosted model servers instead of changing global agent timeouts.",
"models.providers.*.injectNumCtxForOpenAICompat":
"Controls whether OpenClaw injects `options.num_ctx` for Ollama providers configured with the OpenAI-compatible adapter (`openai-completions`). Default is true. Set false only if your proxy/upstream rejects unknown `options` payload fields.",
"models.providers.*.params":
"Provider-specific runtime parameters interpreted by provider plugins. Keep keys documented by the provider, and prefer explicit provider docs over ad hoc shared assumptions.",
"models.providers.*.headers":
"Static HTTP headers merged into provider requests for tenant routing, proxy auth, or custom gateway requirements. Use this sparingly and keep sensitive header values in secrets.",
"models.providers.*.authHeader":
+1
View File
@@ -535,6 +535,7 @@ export const FIELD_LABELS: Record<string, string> = {
"models.providers.*.maxTokens": "Model Provider Max Tokens",
"models.providers.*.timeoutSeconds": "Model Provider Request Timeout",
"models.providers.*.injectNumCtxForOpenAICompat": "Model Provider Inject num_ctx (OpenAI Compat)",
"models.providers.*.params": "Model Provider Runtime Parameters",
"models.providers.*.headers": "Model Provider Headers",
"models.providers.*.authHeader": "Model Provider Authorization Header",
"models.providers.*.request": "Model Provider Request Overrides",
+2
View File
@@ -124,6 +124,8 @@ export type ModelProviderConfig = {
maxTokens?: number;
timeoutSeconds?: number;
injectNumCtxForOpenAICompat?: boolean;
/** Provider-specific runtime parameters interpreted by provider plugins. */
params?: Record<string, unknown>;
headers?: Record<string, SecretInput>;
authHeader?: boolean;
request?: ConfiguredModelProviderRequest;
+1
View File
@@ -362,6 +362,7 @@ const ModelProviderSchema = z
maxTokens: z.number().positive().optional(),
timeoutSeconds: z.number().int().positive().optional(),
injectNumCtxForOpenAICompat: z.boolean().optional(),
params: z.record(z.string(), z.unknown()).optional(),
headers: z.record(z.string(), SecretInputSchema.register(sensitive)).optional(),
authHeader: z.boolean().optional(),
request: ConfiguredModelProviderRequestSchema,