@@ -15,6 +15,28 @@ function jsonlBytes(value: string): number {
|
15 | 15 | return jsonlEncoder.encode(value).byteLength; |
16 | 16 | } |
17 | 17 | |
| 18 | +function cancelTrackedResponse( |
| 19 | +text: string, |
| 20 | +init: ResponseInit, |
| 21 | +): { |
| 22 | +response: Response; |
| 23 | +wasCanceled: () => boolean; |
| 24 | +} { |
| 25 | +let canceled = false; |
| 26 | +const stream = new ReadableStream<Uint8Array>({ |
| 27 | +start(controller) { |
| 28 | +controller.enqueue(new TextEncoder().encode(text)); |
| 29 | +}, |
| 30 | +cancel() { |
| 31 | +canceled = true; |
| 32 | +}, |
| 33 | +}); |
| 34 | +return { |
| 35 | +response: new Response(stream, init), |
| 36 | +wasCanceled: () => canceled, |
| 37 | +}; |
| 38 | +} |
| 39 | + |
18 | 40 | function fetchInputUrl(input: RequestInfo | URL): string { |
19 | 41 | if (typeof input === "string") { |
20 | 42 | return input; |
@@ -243,4 +265,56 @@ describe("OpenAI embedding batch output", () => {
|
243 | 265 | ["3", [4]], |
244 | 266 | ]); |
245 | 267 | }); |
| 268 | + |
| 269 | +it("bounds batch resource error bodies without using response.text()", async () => { |
| 270 | +const tracked = cancelTrackedResponse(`${"batch status unavailable ".repeat(1024)}tail`, { |
| 271 | +status: 400, |
| 272 | +headers: { "Content-Type": "text/plain" }, |
| 273 | +}); |
| 274 | +const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); |
| 275 | +let batchStatusReturned = false; |
| 276 | +const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { |
| 277 | +const url = fetchInputUrl(input); |
| 278 | +if (url.endsWith("/files") && init?.method === "POST") { |
| 279 | +return jsonResponse({ id: "file-0" }); |
| 280 | +} |
| 281 | +if (url.endsWith("/batches") && init?.method === "POST") { |
| 282 | +return jsonResponse({ id: "batch-0", status: "in_progress" }); |
| 283 | +} |
| 284 | +if (url.endsWith("/batches/batch-0") && !batchStatusReturned) { |
| 285 | +batchStatusReturned = true; |
| 286 | +return tracked.response; |
| 287 | +} |
| 288 | +return new Response("unexpected request", { status: 500 }); |
| 289 | +}); |
| 290 | + |
| 291 | +await expect( |
| 292 | +runOpenAiEmbeddingBatches({ |
| 293 | +openAi: { |
| 294 | +baseUrl: "https://openai-compatible.example/v1", |
| 295 | +headers: { Authorization: "Bearer test" }, |
| 296 | +model: "text-embedding-3-small", |
| 297 | + fetchImpl, |
| 298 | +}, |
| 299 | +agentId: "main", |
| 300 | +requests: [ |
| 301 | +{ |
| 302 | +custom_id: "0", |
| 303 | +method: "POST", |
| 304 | +url: "/v1/embeddings", |
| 305 | +body: { |
| 306 | +model: "text-embedding-3-small", |
| 307 | +input: "payload", |
| 308 | +}, |
| 309 | +}, |
| 310 | +], |
| 311 | +wait: true, |
| 312 | +concurrency: 1, |
| 313 | +pollIntervalMs: 1000, |
| 314 | +timeoutMs: 60_000, |
| 315 | +}), |
| 316 | +).rejects.toThrow(/openai batch status failed: 400 batch status unavailable/); |
| 317 | +expect(tracked.wasCanceled()).toBe(true); |
| 318 | +expect(textSpy).not.toHaveBeenCalled(); |
| 319 | +}); |
246 | 320 | }); |