fix(lmstudio): honor embedding preload context (#100750)

Co-authored-by: Zakaria Rahali <zakariarahali288@gmail.com>
Co-authored-by: 徐闻涵0668001344 <xu.wenhan1@xydigit.com>
This commit is contained in:
Peter Steinberger 2026-07-06 18:28:48 +01:00 committed by GitHub
parent d2e6ccf4bb
commit b3e68a093c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 152 additions and 1 deletions

View file

@ -0,0 +1,115 @@
// LM Studio embedding provider tests cover preload context-length precedence.
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createLmstudioEmbeddingProvider } from "./embedding-provider.js";
const ensureLmstudioModelLoadedMock = vi.hoisted(() =>
vi.fn(
async (_params?: { requestedContextLength?: number }) => "text-embedding-nomic-embed-text-v1.5",
),
);
const resolveLmstudioProviderHeadersMock = vi.hoisted(() =>
vi.fn(async (_params?: unknown) => undefined),
);
const resolveLmstudioRuntimeApiKeyMock = vi.hoisted(() =>
vi.fn(async (_params?: unknown) => undefined),
);
vi.mock("./models.fetch.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./models.fetch.js")>();
return {
...actual,
ensureLmstudioModelLoaded: (params: { requestedContextLength?: number }) =>
ensureLmstudioModelLoadedMock(params),
};
});
vi.mock("./runtime.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./runtime.js")>();
return {
...actual,
resolveLmstudioProviderHeaders: (params: unknown) => resolveLmstudioProviderHeadersMock(params),
resolveLmstudioRuntimeApiKey: (params: unknown) => resolveLmstudioRuntimeApiKeyMock(params),
};
});
const EMBEDDING_MODEL = "text-embedding-nomic-embed-text-v1.5";
function buildConfig(params: {
model?: Record<string, unknown>;
provider?: Record<string, unknown>;
}): OpenClawConfig {
return {
models: {
providers: {
lmstudio: {
baseUrl: "http://localhost:1234/v1",
models: [{ id: EMBEDDING_MODEL, ...params.model }],
...params.provider,
},
},
},
} as unknown as OpenClawConfig;
}
async function readRequestedContextLength(config: OpenClawConfig): Promise<unknown> {
await createLmstudioEmbeddingProvider({
config,
provider: "lmstudio",
model: EMBEDDING_MODEL,
fallback: "none",
});
expect(ensureLmstudioModelLoadedMock).toHaveBeenCalledTimes(1);
return ensureLmstudioModelLoadedMock.mock.calls[0]?.[0]?.requestedContextLength;
}
describe("createLmstudioEmbeddingProvider preload context length", () => {
beforeEach(() => {
ensureLmstudioModelLoadedMock.mockClear();
});
it.each([
{
name: "model contextTokens before every fallback",
model: { contextTokens: 4096, contextWindow: 8192 },
provider: { contextTokens: 2048, contextWindow: 16384 },
expected: 4096,
},
{
name: "provider contextTokens as the model's effective cap",
model: { contextWindow: 8192 },
provider: { contextTokens: 4096, contextWindow: 16384 },
expected: 4096,
},
{
name: "model contextWindow when below the provider cap",
model: { contextWindow: 8192 },
provider: { contextTokens: 16384, contextWindow: 32768 },
expected: 8192,
},
{
name: "provider contextTokens when the model has no context fields",
provider: { contextTokens: 4096, contextWindow: 16384 },
expected: 4096,
},
{
name: "model contextWindow before provider contextWindow",
model: { contextWindow: 8192 },
provider: { contextWindow: 16384 },
expected: 8192,
},
{
name: "provider contextWindow as the final configured fallback",
provider: { contextWindow: 16384 },
expected: 16384,
},
{
name: "the loader default when no context is configured",
expected: undefined,
},
])("uses $name", async ({ model, provider, expected }) => {
await expect(readRequestedContextLength(buildConfig({ model, provider }))).resolves.toBe(
expected,
);
});
});

View file

@ -9,9 +9,13 @@ import {
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { resolveMemorySecretInputString } from "openclaw/plugin-sdk/memory-core-host-secret";
import { formatErrorMessage, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
import { asPositiveSafeInteger } from "openclaw/plugin-sdk/string-coerce-runtime";
import { LMSTUDIO_DEFAULT_EMBEDDING_MODEL, LMSTUDIO_PROVIDER_ID } from "./defaults.js";
import { ensureLmstudioModelLoaded } from "./models.fetch.js";
import { resolveLmstudioInferenceBase } from "./models.js";
import {
normalizeLmstudioConfiguredCatalogEntries,
resolveLmstudioInferenceBase,
} from "./models.js";
import {
buildLmstudioAuthHeaders,
resolveLmstudioProviderHeaders,
@ -65,6 +69,31 @@ async function resolveLmstudioApiKey(
}
}
function resolveEmbeddingPreloadContextLength(params: {
model: string;
models: unknown;
providerContextTokens: unknown;
providerContextWindow: unknown;
}): number | undefined {
const configuredModel = normalizeLmstudioConfiguredCatalogEntries(params.models).find(
(entry) => normalizeLmstudioModel(entry.id) === params.model,
);
if (configuredModel?.contextTokens !== undefined) {
return configuredModel.contextTokens;
}
// Provider contextTokens is the model default, so it caps an explicit model
// window only when that model did not declare its own effective token cap.
const providerContextTokens = asPositiveSafeInteger(params.providerContextTokens);
if (configuredModel?.contextWindow !== undefined && providerContextTokens !== undefined) {
return Math.min(configuredModel.contextWindow, providerContextTokens);
}
return (
providerContextTokens ??
configuredModel?.contextWindow ??
asPositiveSafeInteger(params.providerContextWindow)
);
}
/** Creates the LM Studio embedding provider client and preloads the target model before return. */
export async function createLmstudioEmbeddingProvider(
options: MemoryEmbeddingProviderCreateOptions,
@ -119,6 +148,12 @@ export async function createLmstudioEmbeddingProvider(
headers,
ssrfPolicy,
};
const requestedContextLength = resolveEmbeddingPreloadContextLength({
model,
models: providerConfig?.models,
providerContextTokens: providerConfig?.contextTokens,
providerContextWindow: providerConfig?.contextWindow,
});
try {
await ensureLmstudioModelLoaded({
@ -127,6 +162,7 @@ export async function createLmstudioEmbeddingProvider(
headers: headerOverrides,
ssrfPolicy,
modelKey: model,
requestedContextLength,
timeoutMs: 120_000,
});
} catch (error) {