Skip to content

Commit 3f147ae

Browse files
committed
test(provider-transport-fetch): cover split large SSE event
1 parent 81d60ca commit 3f147ae

2 files changed

Lines changed: 41 additions & 10 deletions

File tree

src/agents/provider-transport-fetch.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,42 @@ describe("buildGuardedModelFetch", () => {
13691369
expect(refreshTimeout).toHaveBeenCalledTimes(2);
13701370
});
13711371

1372+
it("handles a valid large SSE event split before its boundary", async () => {
1373+
const payload = { text: "x".repeat(70 * 1024) };
1374+
const encoder = new TextEncoder();
1375+
fetchWithSsrFGuardMock.mockResolvedValue({
1376+
response: new Response(
1377+
new ReadableStream({
1378+
start(controller) {
1379+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}`));
1380+
controller.enqueue(encoder.encode("\n\n"));
1381+
controller.close();
1382+
},
1383+
}),
1384+
{ headers: { "content-type": "text/event-stream" } },
1385+
),
1386+
finalUrl: "https://openrouter.ai/api/v1/chat/completions",
1387+
release: vi.fn(async () => undefined),
1388+
});
1389+
const model = {
1390+
id: "gpt-5.4",
1391+
provider: "openrouter",
1392+
api: "openai-completions",
1393+
baseUrl: "https://openrouter.ai/api/v1",
1394+
} as unknown as Model<"openai-completions">;
1395+
1396+
const response = await buildGuardedModelFetch(model)(
1397+
"https://openrouter.ai/api/v1/chat/completions",
1398+
{ method: "POST" },
1399+
);
1400+
const items = [];
1401+
for await (const item of Stream.fromSSEResponse(response, new AbortController())) {
1402+
items.push(item);
1403+
}
1404+
1405+
expect(items).toEqual([payload]);
1406+
});
1407+
13721408
it("errors on oversized SSE body without event boundary in sanitizer", async () => {
13731409
const oversized = "x".repeat(16 * 1024 * 1024 + 1024);
13741410
const encoder = new TextEncoder();

src/agents/provider-transport-fetch.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,11 @@ const log = createSubsystemLogger("provider-transport-fetch");
5151
* without Content-Length. */
5252
const SSE_SYNTHESIZE_JSON_MAX_BYTES = 16 * 1024 * 1024;
5353

54-
/** Max bytes read from a non-OK (error) response body before truncation. Error
55-
* payloads are small, so keep this tight to bound memory on a hostile error stream. */
54+
/** Max bytes read from a non-OK response body before truncation. */
5655
const SSE_NONOK_BODY_MAX_BYTES = 64 * 1024;
5756

58-
/** Max bytes for the internal SSE sanitization buffer between event boundaries.
59-
* A single legitimate event (e.g. a large reasoning summary on the chatgpt-responses
60-
* API) can far exceed 64 KiB, so bound this at the same 16 MiB ceiling as the
61-
* JSON-synthesis path: only a genuinely boundary-less (hostile/broken) stream trips
62-
* the guard, not a real large event. */
63-
const SSE_SANITIZE_BUFFER_MAX_BYTES = 16 * 1024 * 1024;
57+
/** Max decoded characters buffered while waiting for the next SSE event boundary. */
58+
const SSE_SANITIZE_BUFFER_MAX_CHARS = 16 * 1024 * 1024;
6459

6560
const BLOCKED_EXACT_ORIGIN_TRUST_HOSTNAME_LABELS = new Set(["instance-data"]);
6661
const PLAIN_DECIMAL_NUMBER_RE = /^\d+(?:\.\d+)?$/;
@@ -213,9 +208,9 @@ function sanitizeOpenAISdkSseResponse(
213208
for (;;) {
214209
const boundary = findSseEventBoundary(buffer);
215210
if (!boundary) {
216-
if (buffer.length > SSE_SANITIZE_BUFFER_MAX_BYTES) {
211+
if (buffer.length > SSE_SANITIZE_BUFFER_MAX_CHARS) {
217212
throw new Error(
218-
`SSE response exceeded max buffer size (${SSE_SANITIZE_BUFFER_MAX_BYTES} bytes) without event boundary`,
213+
`SSE response exceeded max buffer size (${SSE_SANITIZE_BUFFER_MAX_CHARS} chars) without event boundary`,
219214
);
220215
}
221216
return enqueued;

0 commit comments

Comments
 (0)