mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 22:43:58 +00:00
fix(extensions/huggingface): bound model discovery JSON response read to prevent OOM (#101079)
* fix(extensions/huggingface): bound model discovery JSON response read to prevent OOM * test(huggingface): prove bounded discovery cleanup * test(huggingface): avoid unbound reader assertions --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -25,6 +25,15 @@ function stubAbortSignalTimeout() {
|
||||
return vi.spyOn(AbortSignal, "timeout").mockReturnValue(controller.signal);
|
||||
}
|
||||
|
||||
function responseFromReader(reader: ReadableStreamDefaultReader<Uint8Array>): Response {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Headers({ "Content-Type": "application/json" }),
|
||||
body: { getReader: () => reader },
|
||||
} as Response;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv("VITEST", ORIGINAL_VITEST);
|
||||
restoreEnv("NODE_ENV", ORIGINAL_NODE_ENV);
|
||||
@@ -108,6 +117,59 @@ describe("huggingface models", () => {
|
||||
expect(timeoutSpy).toHaveBeenCalledWith(MAX_TIMER_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
it("falls back to the static catalog when the discovery response exceeds the byte cap", async () => {
|
||||
process.env.VITEST = "false";
|
||||
process.env.NODE_ENV = "development";
|
||||
const chunk = new Uint8Array(1024 * 1024);
|
||||
const read = vi.fn(async () => ({ done: false as const, value: chunk }));
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const reader = {
|
||||
read,
|
||||
cancel,
|
||||
releaseLock,
|
||||
} as unknown as ReadableStreamDefaultReader<Uint8Array>;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => responseFromReader(reader)),
|
||||
);
|
||||
|
||||
const models = await discoverHuggingfaceModels("hf_test_token");
|
||||
|
||||
expect(models.map((m) => m.id)).toEqual(HUGGINGFACE_MODEL_CATALOG.map((m) => m.id));
|
||||
expect(cancel).toHaveBeenCalledTimes(1);
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
expect(read).toHaveBeenCalledTimes(17);
|
||||
});
|
||||
|
||||
it("parses a valid bounded discovery response", async () => {
|
||||
process.env.VITEST = "false";
|
||||
process.env.NODE_ENV = "development";
|
||||
const modelId = "test-org/test-model";
|
||||
const body = new TextEncoder().encode(JSON.stringify({ data: [{ id: modelId }] }));
|
||||
const read = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ done: false, value: body })
|
||||
.mockResolvedValueOnce({ done: true, value: undefined });
|
||||
const cancel = vi.fn(async () => undefined);
|
||||
const releaseLock = vi.fn();
|
||||
const reader = {
|
||||
read,
|
||||
cancel,
|
||||
releaseLock,
|
||||
} as unknown as ReadableStreamDefaultReader<Uint8Array>;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => responseFromReader(reader)),
|
||||
);
|
||||
|
||||
const models = await discoverHuggingfaceModels("hf_test_token");
|
||||
|
||||
expect(models.some((model) => model.id === modelId)).toBe(true);
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(releaseLock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("isHuggingfacePolicyLocked", () => {
|
||||
it("returns true for :cheapest and :fastest refs", () => {
|
||||
expect(isHuggingfacePolicyLocked("huggingface/deepseek-ai/DeepSeek-R1:cheapest")).toBe(true);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Huggingface plugin module implements models behavior.
|
||||
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types";
|
||||
import {
|
||||
fetchWithSsrFGuard,
|
||||
@@ -165,7 +166,10 @@ export async function discoverHuggingfaceModels(
|
||||
return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
|
||||
}
|
||||
|
||||
const body = (await response.json()) as OpenAIListModelsResponse;
|
||||
const body = await readProviderJsonResponse<OpenAIListModelsResponse>(
|
||||
response,
|
||||
"huggingface.model-discovery",
|
||||
);
|
||||
const data = body?.data;
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
|
||||
|
||||
Reference in New Issue
Block a user