mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(lmstudio): cancel model discovery response body on non-ok (#109718)
* fix(lmstudio): cancel model discovery response body on non-ok Wire the direct-fetch release hook to cancel unread bodies so failed model discovery releases the TCP connection instead of leaving it open. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(lmstudio): unify response cleanup Co-authored-by: Zeng Wen <27948732+ZengWen-DT@users.noreply.github.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Zeng Wen <27948732+ZengWen-DT@users.noreply.github.com>
This commit is contained in:
co-authored by
Cursor
Peter Steinberger
Zeng Wen
parent
4ecd85e0b7
commit
1573c78f5b
@@ -48,6 +48,12 @@ type DiscoverLmstudioModelsParams = {
|
||||
fetchImpl?: typeof fetch;
|
||||
};
|
||||
|
||||
async function cancelUnreadResponseBody(response: Response): Promise<void> {
|
||||
if (!response.bodyUsed) {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLmstudioEndpoint(params: {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
@@ -57,8 +63,10 @@ async function fetchLmstudioEndpoint(params: {
|
||||
auditContext: string;
|
||||
}): Promise<{ response: Response; release: () => Promise<void> }> {
|
||||
const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 1);
|
||||
let response: Response;
|
||||
let release: () => Promise<void>;
|
||||
if (params.ssrfPolicy) {
|
||||
return await fetchWithSsrFGuard({
|
||||
const guarded = await fetchWithSsrFGuard({
|
||||
url: params.url,
|
||||
init: params.init,
|
||||
timeoutMs,
|
||||
@@ -66,14 +74,22 @@ async function fetchLmstudioEndpoint(params: {
|
||||
policy: params.ssrfPolicy,
|
||||
auditContext: params.auditContext,
|
||||
});
|
||||
}
|
||||
const fetchFn = params.fetchImpl ?? fetch;
|
||||
return {
|
||||
response: await fetchFn(params.url, {
|
||||
response = guarded.response;
|
||||
release = guarded.release;
|
||||
} else {
|
||||
const fetchFn = params.fetchImpl ?? fetch;
|
||||
response = await fetchFn(params.url, {
|
||||
...params.init,
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
}),
|
||||
release: async () => {},
|
||||
});
|
||||
release = async () => undefined;
|
||||
}
|
||||
return {
|
||||
response,
|
||||
release: async () => {
|
||||
await cancelUnreadResponseBody(response);
|
||||
await release();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -392,6 +392,38 @@ describe("lmstudio-models", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels the response body after a non-ok model discovery response", async () => {
|
||||
const tracked = cancelTrackedResponse("unavailable", { status: 503 });
|
||||
const fetchMock = vi.fn(async () => tracked.response);
|
||||
|
||||
const result = await fetchLmstudioModels({
|
||||
baseUrl: "http://localhost:1234/v1",
|
||||
fetchImpl: asFetch(fetchMock),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
reachable: true,
|
||||
status: 503,
|
||||
models: [],
|
||||
});
|
||||
expect(tracked.wasCanceled()).toBe(true);
|
||||
});
|
||||
|
||||
it("cancels guarded non-ok discovery bodies before releasing the dispatcher", async () => {
|
||||
const tracked = cancelTrackedResponse("unavailable", { status: 503 });
|
||||
const release = vi.fn(async () => undefined);
|
||||
fetchWithSsrFGuardMock.mockResolvedValue({ response: tracked.response, release });
|
||||
|
||||
const result = await fetchLmstudioModels({
|
||||
baseUrl: "http://localhost:1234/v1",
|
||||
ssrfPolicy: {},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ reachable: true, status: 503, models: [] });
|
||||
expect(tracked.wasCanceled()).toBe(true);
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reports malformed model list JSON with an owned error", async () => {
|
||||
const fetchMock = vi.fn(async () => malformedJsonResponse());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user