fix(lmstudio): cap model fetch timeout delays

This commit is contained in:
Peter Steinberger
2026-05-29 15:05:16 -04:00
parent 7aca070723
commit 11e82bdef2
2 changed files with 45 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";
import { clampTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { SELF_HOSTED_DEFAULT_COST } from "openclaw/plugin-sdk/provider-setup";
@@ -44,11 +45,12 @@ async function fetchLmstudioEndpoint(params: {
ssrfPolicy?: SsrFPolicy;
auditContext: string;
}): Promise<{ response: Response; release: () => Promise<void> }> {
const timeoutMs = clampTimerTimeoutMs(params.timeoutMs) ?? 1;
if (params.ssrfPolicy) {
return await fetchWithSsrFGuard({
url: params.url,
init: params.init,
timeoutMs: params.timeoutMs,
timeoutMs,
fetchImpl: params.fetchImpl,
policy: params.ssrfPolicy,
auditContext: params.auditContext,
@@ -58,7 +60,7 @@ async function fetchLmstudioEndpoint(params: {
return {
response: await fetchFn(params.url, {
...params.init,
signal: AbortSignal.timeout(params.timeoutMs),
signal: AbortSignal.timeout(timeoutMs),
}),
release: async () => {},
};

View File

@@ -1,3 +1,4 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import {
SELF_HOSTED_DEFAULT_CONTEXT_WINDOW,
SELF_HOSTED_DEFAULT_MAX_TOKENS,
@@ -90,6 +91,7 @@ describe("lmstudio-models", () => {
afterEach(() => {
fetchWithSsrFGuardMock.mockReset();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
@@ -379,6 +381,45 @@ describe("lmstudio-models", () => {
}
});
it("caps oversized direct fetch timeouts before discovering models", async () => {
const timeoutController = new AbortController();
const timeoutSpy = vi.spyOn(AbortSignal, "timeout").mockReturnValue(timeoutController.signal);
const fetchMock = vi.fn(async (_url: string | URL, init?: RequestInit) => ({
ok: true,
status: 200,
requestInit: init,
json: async () => ({ models: [] }),
}));
const result = await fetchLmstudioModels({
baseUrl: "http://localhost:1234/v1",
timeoutMs: Number.MAX_SAFE_INTEGER,
fetchImpl: asFetch(fetchMock),
});
expect(result.reachable).toBe(true);
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
expect(fetchMock.mock.calls[0]?.[1]?.signal).toBe(timeoutController.signal);
});
it("caps oversized guarded-fetch timeouts before discovering models", async () => {
fetchWithSsrFGuardMock.mockResolvedValue({
response: new Response(JSON.stringify({ models: [] }), { status: 200 }),
release: vi.fn(async () => undefined),
});
const result = await fetchLmstudioModels({
baseUrl: "http://localhost:1234/v1",
timeoutMs: Number.MAX_SAFE_INTEGER,
ssrfPolicy: {},
});
expect(result.reachable).toBe(true);
expect(fetchWithSsrFGuardMock.mock.calls[0]?.[0]).toMatchObject({
timeoutMs: MAX_TIMER_TIMEOUT_MS,
});
});
it("skips model load when already loaded", async () => {
const fetchMock = createModelLoadFetchMock({ loadedContextLength: 64000 });
vi.stubGlobal("fetch", asFetch(fetchMock));