@ai-sdk/anthropic 4.0.32 → 4.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,756 @@
1
+ import {
2
+ EmptyResponseBodyError,
3
+ InvalidArgumentError,
4
+ UnsupportedFunctionalityError,
5
+ type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
6
+ type Experimental_BatchV4ItemResult as BatchV4ItemResult,
7
+ type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
8
+ type Experimental_BatchV4StartResult as BatchV4StartResult,
9
+ type Experimental_BatchV4Status as BatchV4Status,
10
+ type JSONObject,
11
+ type LanguageModelV4GenerateResult,
12
+ type SharedV4ProviderMetadata,
13
+ } from '@ai-sdk/provider';
14
+ import {
15
+ combineHeaders,
16
+ convertAsyncIteratorToReadableStream,
17
+ createJsonResponseHandler,
18
+ getFromApi,
19
+ lazySchema,
20
+ normalizeHeaders,
21
+ parseJSON,
22
+ parseProviderOptions,
23
+ postJsonToApi,
24
+ resolve,
25
+ safeValidateTypes,
26
+ WORKFLOW_DESERIALIZE,
27
+ WORKFLOW_SERIALIZE,
28
+ zodSchema,
29
+ type InferSchema,
30
+ type ResponseHandler,
31
+ } from '@ai-sdk/provider-utils';
32
+ import { z } from 'zod/v4';
33
+ import {
34
+ anthropicResponseSchema,
35
+ type AnthropicReasoningMetadata,
36
+ type AnthropicResponseContextManagement,
37
+ type AnthropicStopDetails,
38
+ } from './anthropic-api';
39
+ import { anthropicFailedResponseHandler } from './anthropic-error';
40
+ import {
41
+ AnthropicLanguageModel,
42
+ type AnthropicLanguageModelConfig,
43
+ } from './anthropic-language-model';
44
+ import {
45
+ anthropicLanguageModelOptions,
46
+ type AnthropicModelId,
47
+ } from './anthropic-language-model-options';
48
+ import type {
49
+ AnthropicMessageMetadata,
50
+ AnthropicUsageIteration,
51
+ } from './anthropic-message-metadata';
52
+ import { convertAnthropicUsage } from './convert-anthropic-usage';
53
+ import { mapAnthropicStopReason } from './map-anthropic-stop-reason';
54
+
55
+ const anthropicBatchRequestIdPattern = /^[A-Za-z0-9_-]{1,64}$/;
56
+
57
+ const anthropicBatchProviderOptionsSchema = anthropicLanguageModelOptions.pick({
58
+ anthropicBeta: true,
59
+ });
60
+
61
+ type AnthropicBatchRequest = Parameters<
62
+ BatchLanguageModelV4['experimental_doStartBatch']
63
+ >[0]['requests'][number];
64
+
65
+ const anthropicBatchResponseSchema = lazySchema(() =>
66
+ zodSchema(
67
+ z.object({
68
+ id: z.string(),
69
+ type: z.literal('message_batch'),
70
+ processing_status: z.string(),
71
+ request_counts: z.object({
72
+ processing: z.number(),
73
+ succeeded: z.number(),
74
+ errored: z.number(),
75
+ canceled: z.number(),
76
+ expired: z.number(),
77
+ }),
78
+ created_at: z.string(),
79
+ expires_at: z.string(),
80
+ archived_at: z.string().nullish(),
81
+ results_url: z.string().nullish(),
82
+ }),
83
+ ),
84
+ );
85
+
86
+ type AnthropicBatchResponse = InferSchema<typeof anthropicBatchResponseSchema>;
87
+
88
+ const anthropicBatchResultLineSchema = lazySchema(() =>
89
+ zodSchema(
90
+ z.object({
91
+ custom_id: z.string(),
92
+ result: z.discriminatedUnion('type', [
93
+ z.object({
94
+ type: z.literal('succeeded'),
95
+ message: z.unknown(),
96
+ }),
97
+ z.object({
98
+ type: z.literal('errored'),
99
+ error: z.object({
100
+ type: z.literal('error'),
101
+ error: z.object({
102
+ type: z.string(),
103
+ message: z.string(),
104
+ }),
105
+ request_id: z.string().nullish(),
106
+ }),
107
+ }),
108
+ z.object({ type: z.literal('canceled') }),
109
+ z.object({ type: z.literal('expired') }),
110
+ ]),
111
+ }),
112
+ ),
113
+ );
114
+
115
+ type AnthropicBatchResultLine = InferSchema<
116
+ typeof anthropicBatchResultLineSchema
117
+ >;
118
+
119
+ type AnthropicResponse = InferSchema<typeof anthropicResponseSchema>;
120
+
121
+ export class AnthropicMessagesBatchLanguageModel
122
+ extends AnthropicLanguageModel
123
+ implements BatchLanguageModelV4
124
+ {
125
+ static [WORKFLOW_SERIALIZE](model: AnthropicMessagesBatchLanguageModel) {
126
+ return AnthropicLanguageModel[WORKFLOW_SERIALIZE](model);
127
+ }
128
+
129
+ static [WORKFLOW_DESERIALIZE](options: {
130
+ modelId: AnthropicModelId;
131
+ config: AnthropicLanguageModelConfig;
132
+ }) {
133
+ return new AnthropicMessagesBatchLanguageModel(
134
+ options.modelId,
135
+ options.config,
136
+ );
137
+ }
138
+
139
+ constructor(modelId: AnthropicModelId, config: AnthropicLanguageModelConfig) {
140
+ super(modelId, config);
141
+ }
142
+
143
+ async experimental_doStartBatch({
144
+ requests,
145
+ providerOptions,
146
+ headers,
147
+ abortSignal,
148
+ }: Parameters<
149
+ BatchLanguageModelV4['experimental_doStartBatch']
150
+ >[0]): Promise<BatchV4StartResult> {
151
+ validateRequestIds(requests);
152
+
153
+ const explicitBatchBetas = new Set(
154
+ await getAnthropicBatchProviderBetas({
155
+ provider: this.config.provider,
156
+ providerOptions,
157
+ }),
158
+ );
159
+ const batchBetas = new Set(explicitBatchBetas);
160
+ const preparedRequests: Array<{
161
+ custom_id: string;
162
+ params: Record<string, unknown>;
163
+ }> = [];
164
+ const batchWarnings: BatchV4StartResult['warnings'] = [];
165
+
166
+ for (const request of requests) {
167
+ const requestBetas = await getAnthropicBatchProviderBetas({
168
+ provider: this.config.provider,
169
+ providerOptions: request.options.providerOptions,
170
+ });
171
+ if (requestBetas.length > 0) {
172
+ throw new UnsupportedFunctionalityError({
173
+ functionality: 'per-request providerOptions.anthropic.anthropicBeta',
174
+ message:
175
+ `Anthropic Message Batches do not support per-request betas ` +
176
+ `(request "${request.id}"). Set providerOptions.anthropic.anthropicBeta ` +
177
+ `on startTextBatch instead.`,
178
+ });
179
+ }
180
+
181
+ const prepared = await this.getArgs({
182
+ ...request.options,
183
+ stream: false,
184
+ userSuppliedBetas: new Set(explicitBatchBetas),
185
+ });
186
+ const body = this.transformRequestBody(prepared.args, prepared.betas);
187
+ validateAnthropicBatchBody({
188
+ body,
189
+ requestId: request.id,
190
+ });
191
+
192
+ preparedRequests.push({ custom_id: request.id, params: body });
193
+ for (const beta of prepared.betas) {
194
+ batchBetas.add(beta);
195
+ }
196
+ for (const warning of prepared.warnings) {
197
+ batchWarnings.push({ requestId: request.id, warning });
198
+ }
199
+ }
200
+
201
+ const { value: batch } = await postJsonToApi({
202
+ url: this.getBatchUrl(''),
203
+ headers: await this.getStartBatchHeaders({ betas: batchBetas, headers }),
204
+ body: { requests: preparedRequests },
205
+ failedResponseHandler: anthropicFailedResponseHandler,
206
+ successfulResponseHandler: createJsonResponseHandler(
207
+ anthropicBatchResponseSchema,
208
+ ),
209
+ abortSignal,
210
+ fetch: this.config.fetch,
211
+ });
212
+
213
+ return {
214
+ batchId: batch.id,
215
+ ...convertAnthropicBatchStatus(batch),
216
+ warnings: batchWarnings,
217
+ };
218
+ }
219
+
220
+ async experimental_doGetBatchStatus(
221
+ options: BatchV4OperationOptions,
222
+ ): Promise<BatchV4Status> {
223
+ return convertAnthropicBatchStatus(await this.retrieveBatch(options));
224
+ }
225
+
226
+ async experimental_doGetBatchResults(
227
+ options: BatchV4OperationOptions,
228
+ ): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
229
+ const batch = await this.retrieveBatch(options);
230
+
231
+ if (convertAnthropicBatchStatus(batch).status === 'pending') {
232
+ throw new InvalidArgumentError({
233
+ argument: 'batchId',
234
+ message: `Anthropic batch "${options.batchId}" is not complete.`,
235
+ });
236
+ }
237
+
238
+ if (batch.archived_at != null) {
239
+ throw new InvalidArgumentError({
240
+ argument: 'batchId',
241
+ message: `Anthropic batch "${options.batchId}" results are no longer available.`,
242
+ });
243
+ }
244
+
245
+ if (batch.results_url == null) {
246
+ throw new InvalidArgumentError({
247
+ argument: 'batchId',
248
+ message: `Anthropic batch "${options.batchId}" does not have a results URL.`,
249
+ });
250
+ }
251
+
252
+ const { value: stream } = await getFromApi({
253
+ url: batch.results_url,
254
+ validateUrl: true,
255
+ credentialedOrigin: this.config.baseURL,
256
+ trustedOrigin: this.config.baseURL,
257
+ headers: await this.getBatchHeaders(options.headers),
258
+ failedResponseHandler: anthropicFailedResponseHandler,
259
+ successfulResponseHandler: rawStreamResponseHandler,
260
+ abortSignal: options.abortSignal,
261
+ fetch: this.config.fetch,
262
+ });
263
+
264
+ return convertAsyncIteratorToReadableStream(
265
+ this.iterateBatchResults(stream),
266
+ );
267
+ }
268
+
269
+ private async retrieveBatch(
270
+ options: BatchV4OperationOptions,
271
+ ): Promise<AnthropicBatchResponse> {
272
+ const { value: batch } = await getFromApi({
273
+ url: this.getBatchUrl(`/${encodeURIComponent(options.batchId)}`),
274
+ validateUrl: false,
275
+ headers: await this.getBatchHeaders(options.headers),
276
+ failedResponseHandler: anthropicFailedResponseHandler,
277
+ successfulResponseHandler: createJsonResponseHandler(
278
+ anthropicBatchResponseSchema,
279
+ ),
280
+ abortSignal: options.abortSignal,
281
+ fetch: this.config.fetch,
282
+ });
283
+
284
+ return batch;
285
+ }
286
+
287
+ private async *iterateBatchResults(
288
+ stream: ReadableStream<Uint8Array>,
289
+ ): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
290
+ for await (const line of parseJsonLines(stream)) {
291
+ yield await convertAnthropicBatchResult(line);
292
+ }
293
+ }
294
+
295
+ private getBatchUrl(path: string) {
296
+ return `${this.config.baseURL}/messages/batches${path}`;
297
+ }
298
+
299
+ private async getStartBatchHeaders({
300
+ betas,
301
+ headers,
302
+ }: {
303
+ betas: Set<string>;
304
+ headers: Record<string, string | undefined> | undefined;
305
+ }) {
306
+ return combineHeaders(
307
+ normalizeHeaders(await this.getBatchHeaders(headers)),
308
+ {
309
+ 'anthropic-beta':
310
+ betas.size > 0 ? Array.from(betas).join(',') : undefined,
311
+ },
312
+ );
313
+ }
314
+
315
+ private async getBatchHeaders(
316
+ headers: Record<string, string | undefined> | undefined,
317
+ ) {
318
+ return combineHeaders(
319
+ this.config.headers ? await resolve(this.config.headers) : undefined,
320
+ headers,
321
+ );
322
+ }
323
+ }
324
+
325
+ async function getAnthropicBatchProviderBetas({
326
+ provider,
327
+ providerOptions,
328
+ }: {
329
+ provider: string;
330
+ providerOptions: BatchV4OperationOptions['providerOptions'];
331
+ }) {
332
+ const providerOptionsName = provider.split('.')[0];
333
+ const canonicalOptions = await parseProviderOptions({
334
+ provider: 'anthropic',
335
+ providerOptions,
336
+ schema: anthropicBatchProviderOptionsSchema,
337
+ });
338
+ const customOptions =
339
+ providerOptionsName !== 'anthropic'
340
+ ? await parseProviderOptions({
341
+ provider: providerOptionsName,
342
+ providerOptions,
343
+ schema: anthropicBatchProviderOptionsSchema,
344
+ })
345
+ : undefined;
346
+
347
+ const anthropicOptions = Object.assign(
348
+ {},
349
+ canonicalOptions ?? {},
350
+ customOptions ?? {},
351
+ );
352
+
353
+ return anthropicOptions.anthropicBeta ?? [];
354
+ }
355
+
356
+ function validateRequestIds(requests: ReadonlyArray<AnthropicBatchRequest>) {
357
+ const ids = new Set<string>();
358
+
359
+ for (const request of requests) {
360
+ if (!anthropicBatchRequestIdPattern.test(request.id)) {
361
+ throw new InvalidArgumentError({
362
+ argument: 'requests',
363
+ message:
364
+ `Anthropic batch request ID "${request.id}" must match ` +
365
+ '^[A-Za-z0-9_-]{1,64}$.',
366
+ });
367
+ }
368
+
369
+ if (ids.has(request.id)) {
370
+ throw new InvalidArgumentError({
371
+ argument: 'requests',
372
+ message: `Anthropic batch request IDs must be unique; duplicate ID "${request.id}".`,
373
+ });
374
+ }
375
+
376
+ ids.add(request.id);
377
+ }
378
+ }
379
+
380
+ function validateAnthropicBatchBody({
381
+ body,
382
+ requestId,
383
+ }: {
384
+ body: Record<string, unknown>;
385
+ requestId: string;
386
+ }) {
387
+ if (body.speed != null) {
388
+ throw new UnsupportedFunctionalityError({
389
+ functionality: 'providerOptions.anthropic.speed',
390
+ message:
391
+ `Anthropic Message Batches do not support speed ` +
392
+ `(request "${requestId}").`,
393
+ });
394
+ }
395
+
396
+ if (
397
+ Array.isArray(body.fallbacks) &&
398
+ body.fallbacks.some(
399
+ fallback =>
400
+ fallback != null &&
401
+ typeof fallback === 'object' &&
402
+ 'speed' in fallback &&
403
+ fallback.speed != null,
404
+ )
405
+ ) {
406
+ throw new UnsupportedFunctionalityError({
407
+ functionality: 'providerOptions.anthropic.fallbacks[].speed',
408
+ message:
409
+ `Anthropic Message Batches do not support fallback speed ` +
410
+ `(request "${requestId}").`,
411
+ });
412
+ }
413
+ }
414
+
415
+ function convertAnthropicBatchStatus(
416
+ batch: AnthropicBatchResponse,
417
+ ): BatchV4Status {
418
+ const requestCounts = convertAnthropicRequestCounts(batch.request_counts);
419
+
420
+ return {
421
+ status: mapAnthropicBatchStatus(batch.processing_status),
422
+ rawStatus: batch.processing_status,
423
+ ...(requestCounts != null ? { requestCounts } : {}),
424
+ createdAt: batch.created_at,
425
+ expiresAt: batch.expires_at,
426
+ };
427
+ }
428
+
429
+ function mapAnthropicBatchStatus(rawStatus: string): BatchV4Status['status'] {
430
+ switch (rawStatus) {
431
+ case 'ended':
432
+ return 'completed';
433
+ case 'in_progress':
434
+ case 'canceling':
435
+ default:
436
+ return 'pending';
437
+ }
438
+ }
439
+
440
+ function convertAnthropicRequestCounts(
441
+ counts: AnthropicBatchResponse['request_counts'],
442
+ ): BatchV4Status['requestCounts'] | undefined {
443
+ const values = [
444
+ counts.processing,
445
+ counts.succeeded,
446
+ counts.errored,
447
+ counts.canceled,
448
+ counts.expired,
449
+ ];
450
+
451
+ if (values.some(value => !Number.isFinite(value) || value < 0)) {
452
+ return undefined;
453
+ }
454
+
455
+ return {
456
+ total: values.reduce((total, value) => total + value, 0),
457
+ pending: counts.processing,
458
+ completed: counts.succeeded,
459
+ failed: counts.errored + counts.canceled + counts.expired,
460
+ };
461
+ }
462
+
463
+ async function convertAnthropicBatchResult(
464
+ line: AnthropicBatchResultLine,
465
+ ): Promise<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
466
+ switch (line.result.type) {
467
+ case 'canceled':
468
+ return { id: line.custom_id, status: 'cancelled' };
469
+ case 'expired':
470
+ return { id: line.custom_id, status: 'expired' };
471
+ case 'errored': {
472
+ const requestId = line.result.error.request_id;
473
+ return {
474
+ id: line.custom_id,
475
+ status: 'failed',
476
+ error: {
477
+ message: line.result.error.error.message,
478
+ type: line.result.error.error.type,
479
+ },
480
+ ...(requestId != null
481
+ ? {
482
+ providerMetadata: {
483
+ anthropic: { requestId },
484
+ } satisfies SharedV4ProviderMetadata,
485
+ }
486
+ : {}),
487
+ };
488
+ }
489
+ case 'succeeded': {
490
+ const validation = await safeValidateTypes({
491
+ value: line.result.message,
492
+ schema: anthropicResponseSchema,
493
+ });
494
+
495
+ if (!validation.success) {
496
+ return {
497
+ id: line.custom_id,
498
+ status: 'failed',
499
+ error: {
500
+ message: 'Anthropic returned an invalid Message batch result.',
501
+ code: 'invalid_response',
502
+ },
503
+ };
504
+ }
505
+
506
+ const response = validation.value;
507
+
508
+ const unsupportedPart = response.content.find(
509
+ part => !supportedBatchContentTypes.has(part.type),
510
+ );
511
+
512
+ if (unsupportedPart != null) {
513
+ return {
514
+ id: line.custom_id,
515
+ status: 'failed',
516
+ error: {
517
+ message:
518
+ `Anthropic returned a "${unsupportedPart.type}" content block, ` +
519
+ 'but tool content is not supported in AI SDK text batches.',
520
+ code: 'unsupported_tool_content',
521
+ },
522
+ };
523
+ }
524
+
525
+ return {
526
+ id: line.custom_id,
527
+ status: 'succeeded',
528
+ result: convertAnthropicBatchResponse(response),
529
+ };
530
+ }
531
+ }
532
+ }
533
+
534
+ const supportedBatchContentTypes = new Set([
535
+ 'text',
536
+ 'thinking',
537
+ 'redacted_thinking',
538
+ 'compaction',
539
+ // The normal Anthropic response conversion intentionally drops this marker;
540
+ // the fallback hop remains available through usage.iterations metadata.
541
+ 'fallback',
542
+ ]);
543
+
544
+ function convertAnthropicBatchResponse(
545
+ response: AnthropicResponse,
546
+ ): LanguageModelV4GenerateResult {
547
+ const content: LanguageModelV4GenerateResult['content'] = [];
548
+
549
+ for (const part of response.content) {
550
+ switch (part.type) {
551
+ case 'text':
552
+ content.push({ type: 'text', text: part.text });
553
+ break;
554
+ case 'thinking':
555
+ content.push({
556
+ type: 'reasoning',
557
+ text: part.thinking,
558
+ providerMetadata: {
559
+ anthropic: {
560
+ signature: part.signature,
561
+ } satisfies AnthropicReasoningMetadata,
562
+ },
563
+ });
564
+ break;
565
+ case 'redacted_thinking':
566
+ content.push({
567
+ type: 'reasoning',
568
+ text: '',
569
+ providerMetadata: {
570
+ anthropic: {
571
+ redactedData: part.data,
572
+ } satisfies AnthropicReasoningMetadata,
573
+ },
574
+ });
575
+ break;
576
+ case 'compaction':
577
+ content.push({
578
+ type: 'text',
579
+ text: part.content,
580
+ providerMetadata: { anthropic: { type: 'compaction' } },
581
+ });
582
+ break;
583
+ }
584
+ }
585
+
586
+ return {
587
+ content,
588
+ finishReason: {
589
+ unified: mapAnthropicStopReason({
590
+ finishReason: response.stop_reason,
591
+ }),
592
+ raw: response.stop_reason ?? undefined,
593
+ },
594
+ usage: convertAnthropicUsage({ usage: response.usage }),
595
+ response: {
596
+ id: response.id ?? undefined,
597
+ modelId: response.model ?? undefined,
598
+ },
599
+ warnings: [],
600
+ providerMetadata: {
601
+ anthropic: convertAnthropicMessageMetadata(response),
602
+ },
603
+ };
604
+ }
605
+
606
+ function convertAnthropicMessageMetadata(response: AnthropicResponse) {
607
+ const stopDetails = mapAnthropicStopDetails(response.stop_details);
608
+
609
+ return {
610
+ usage: response.usage as JSONObject,
611
+ stopSequence: response.stop_sequence ?? null,
612
+ ...(stopDetails != null ? { stopDetails } : {}),
613
+ iterations: response.usage.iterations
614
+ ? response.usage.iterations.map(
615
+ iteration =>
616
+ ({
617
+ type: iteration.type,
618
+ ...(iteration.model != null ? { model: iteration.model } : {}),
619
+ inputTokens: iteration.input_tokens,
620
+ outputTokens: iteration.output_tokens,
621
+ ...(iteration.cache_creation_input_tokens
622
+ ? {
623
+ cacheCreationInputTokens:
624
+ iteration.cache_creation_input_tokens,
625
+ }
626
+ : {}),
627
+ ...(iteration.cache_read_input_tokens
628
+ ? { cacheReadInputTokens: iteration.cache_read_input_tokens }
629
+ : {}),
630
+ }) satisfies AnthropicUsageIteration,
631
+ )
632
+ : null,
633
+ container: response.container
634
+ ? {
635
+ expiresAt: response.container.expires_at,
636
+ id: response.container.id,
637
+ skills:
638
+ response.container.skills?.map(skill => ({
639
+ type: skill.type,
640
+ skillId: skill.skill_id,
641
+ version: skill.version,
642
+ })) ?? null,
643
+ }
644
+ : null,
645
+ contextManagement: mapAnthropicResponseContextManagement(
646
+ response.context_management,
647
+ ),
648
+ } satisfies AnthropicMessageMetadata;
649
+ }
650
+
651
+ function mapAnthropicResponseContextManagement(
652
+ contextManagement: AnthropicResponseContextManagement | null | undefined,
653
+ ): AnthropicMessageMetadata['contextManagement'] {
654
+ return contextManagement
655
+ ? {
656
+ appliedEdits: contextManagement.applied_edits.map(edit => {
657
+ switch (edit.type) {
658
+ case 'clear_tool_uses_20250919':
659
+ return {
660
+ type: edit.type,
661
+ clearedToolUses: edit.cleared_tool_uses,
662
+ clearedInputTokens: edit.cleared_input_tokens,
663
+ };
664
+ case 'clear_thinking_20251015':
665
+ return {
666
+ type: edit.type,
667
+ clearedThinkingTurns: edit.cleared_thinking_turns,
668
+ clearedInputTokens: edit.cleared_input_tokens,
669
+ };
670
+ case 'compact_20260112':
671
+ return { type: edit.type };
672
+ }
673
+ }),
674
+ }
675
+ : null;
676
+ }
677
+
678
+ function mapAnthropicStopDetails(
679
+ stopDetails: AnthropicStopDetails | null | undefined,
680
+ ): AnthropicMessageMetadata['stopDetails'] | undefined {
681
+ if (stopDetails == null) {
682
+ return undefined;
683
+ }
684
+
685
+ return {
686
+ type: stopDetails.type,
687
+ ...(stopDetails.category != null ? { category: stopDetails.category } : {}),
688
+ ...(stopDetails.explanation != null
689
+ ? { explanation: stopDetails.explanation }
690
+ : {}),
691
+ ...(stopDetails.recommended_model != null
692
+ ? { recommendedModel: stopDetails.recommended_model }
693
+ : {}),
694
+ };
695
+ }
696
+
697
+ const rawStreamResponseHandler: ResponseHandler<
698
+ ReadableStream<Uint8Array>
699
+ > = async ({ response }) => {
700
+ if (response.body == null) {
701
+ throw new EmptyResponseBodyError();
702
+ }
703
+
704
+ return { value: response.body };
705
+ };
706
+
707
+ async function* parseJsonLines(
708
+ stream: ReadableStream<Uint8Array>,
709
+ ): AsyncGenerator<AnthropicBatchResultLine> {
710
+ const reader = stream.getReader();
711
+ const decoder = new TextDecoder();
712
+ let buffer = '';
713
+ let finished = false;
714
+
715
+ try {
716
+ while (true) {
717
+ const { done, value } = await reader.read();
718
+
719
+ if (done) {
720
+ finished = true;
721
+ buffer += decoder.decode();
722
+ break;
723
+ }
724
+
725
+ buffer += decoder.decode(value, { stream: true });
726
+
727
+ let lineEnd = buffer.indexOf('\n');
728
+ while (lineEnd !== -1) {
729
+ const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
730
+ buffer = buffer.slice(lineEnd + 1);
731
+
732
+ if (line.trim().length > 0) {
733
+ yield await parseJSON({
734
+ text: line,
735
+ schema: anthropicBatchResultLineSchema,
736
+ });
737
+ }
738
+
739
+ lineEnd = buffer.indexOf('\n');
740
+ }
741
+ }
742
+
743
+ const finalLine = buffer.replace(/\r$/, '');
744
+ if (finalLine.trim().length > 0) {
745
+ yield await parseJSON({
746
+ text: finalLine,
747
+ schema: anthropicBatchResultLineSchema,
748
+ });
749
+ }
750
+ } finally {
751
+ if (!finished) {
752
+ await reader.cancel().catch(() => {});
753
+ }
754
+ reader.releaseLock();
755
+ }
756
+ }