mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
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:
parent
d2e6ccf4bb
commit
b3e68a093c
2 changed files with 152 additions and 1 deletions
115
extensions/lmstudio/src/embedding-provider.test.ts
Normal file
115
extensions/lmstudio/src/embedding-provider.test.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue