


















@@ -23,6 +23,42 @@ beforeEach(() => {
2323vi.clearAllMocks();
2424});
252526+// Mirrors SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES in the source under test. Kept in
27+// sync deliberately so the regression asserts the body is capped, not drained.
28+const SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES = 16 * 1024 * 1024;
29+const CHUNK_BYTES = 1024 * 1024;
30+31+/**
32+ * Builds a Response body that would never terminate on its own: each pull emits
33+ * a 1 MiB chunk forever. A bounded reader must cancel it after the byte cap.
34+ */
35+function createUnboundedJsonStream(): {
36+body: ReadableStream<Uint8Array>;
37+cancelCount: number;
38+bytesPulled: number;
39+} {
40+const state = { cancelCount: 0, bytesPulled: 0 };
41+const chunk = new Uint8Array(CHUNK_BYTES).fill(0x20); // ASCII spaces: valid stream, never closes
42+const body = new ReadableStream<Uint8Array>({
43+pull(controller) {
44+state.bytesPulled += chunk.byteLength;
45+controller.enqueue(chunk);
46+},
47+cancel() {
48+state.cancelCount += 1;
49+},
50+});
51+return {
52+ body,
53+get cancelCount() {
54+return state.cancelCount;
55+},
56+get bytesPulled() {
57+return state.bytesPulled;
58+},
59+};
60+}
61+2662function createRuntime() {
2763return {
2864error: vi.fn(),
@@ -437,6 +473,75 @@ describe("discoverOpenAICompatibleLocalModels", () => {
437473});
438474expect(release).toHaveBeenCalledOnce();
439475});
476+477+it("bounds an unbounded /models discovery stream instead of buffering it", async () => {
478+const release = vi.fn(async () => undefined);
479+const oversized = createUnboundedJsonStream();
480+fetchWithSsrFGuardMock.mockResolvedValueOnce({
481+response: new Response(oversized.body, { status: 200 }),
482+finalUrl: "http://127.0.0.1:8000/v1/models",
483+ release,
484+});
485+486+const models = await discoverOpenAICompatibleLocalModels({
487+baseUrl: "http://127.0.0.1:8000/v1",
488+label: "vLLM",
489+env: {},
490+});
491+492+// The reader cancels the body once the byte cap is exceeded; without the
493+// cap the stream would never finish and the discovery would buffer it all.
494+expect(models).toEqual([]);
495+expect(oversized.cancelCount).toBe(1);
496+expect(oversized.bytesPulled).toBeLessThanOrEqual(
497+SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES + 2 * CHUNK_BYTES,
498+);
499+expect(release).toHaveBeenCalledOnce();
500+});
501+502+it("bounds an unbounded llama.cpp /props discovery stream instead of buffering it", async () => {
503+const modelsRelease = vi.fn(async () => undefined);
504+const propsRelease = vi.fn(async () => undefined);
505+const oversized = createUnboundedJsonStream();
506+fetchWithSsrFGuardMock.mockResolvedValueOnce({
507+response: new Response(JSON.stringify({ data: [{ id: "qwen3.6-mxfp4-moe" }] }), {
508+status: 200,
509+}),
510+finalUrl: "http://127.0.0.1:8080/v1/models",
511+release: modelsRelease,
512+});
513+fetchWithSsrFGuardMock.mockResolvedValueOnce({
514+response: new Response(oversized.body, { status: 200 }),
515+finalUrl: "http://127.0.0.1:8080/props",
516+release: propsRelease,
517+});
518+519+const models = await discoverOpenAICompatibleLocalModels({
520+baseUrl: "http://127.0.0.1:8080/v1",
521+label: "llama.cpp",
522+env: {},
523+});
524+525+// /props overflow is swallowed so discovery still succeeds, but the body is
526+// capped: the runtime context token probe is skipped, not OOM'd.
527+expect(models).toEqual([
528+{
529+id: "qwen3.6-mxfp4-moe",
530+name: "qwen3.6-mxfp4-moe",
531+reasoning: false,
532+input: ["text"],
533+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
534+contextWindow: 128000,
535+maxTokens: 8192,
536+},
537+]);
538+expect(oversized.cancelCount).toBe(1);
539+expect(oversized.bytesPulled).toBeLessThanOrEqual(
540+SELF_HOSTED_DISCOVERY_JSON_MAX_BYTES + 2 * CHUNK_BYTES,
541+);
542+expect(modelsRelease).toHaveBeenCalledOnce();
543+expect(propsRelease).toHaveBeenCalledOnce();
544+});
440545});
441546442547describe("configureOpenAICompatibleSelfHostedProviderNonInteractive", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。