ai 7.0.106 → 7.0.108

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +390 -95
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.d.ts +16 -2
  6. package/dist/internal/index.js +323 -53
  7. package/dist/internal/index.js.map +1 -1
  8. package/docs/00-introduction/index.mdx +1 -1
  9. package/docs/02-getting-started/03-nextjs-pages-router.mdx +1 -1
  10. package/docs/02-getting-started/05-nuxt.mdx +1 -1
  11. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +31 -0
  12. package/docs/03-ai-sdk-core/19-tool-search.mdx +4 -2
  13. package/docs/04-ai-sdk-ui/02-chatbot.mdx +26 -11
  14. package/docs/04-ai-sdk-ui/05-completion.mdx +1 -1
  15. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +7 -0
  16. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +7 -0
  17. package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +3 -2
  18. package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +1 -1
  19. package/package.json +11 -11
  20. package/src/generate-speech/generate-speech.ts +44 -4
  21. package/src/generate-text/generate-text.ts +29 -16
  22. package/src/generate-text/invoke-tool-callbacks-from-stream.ts +68 -16
  23. package/src/generate-text/parse-tool-call.ts +64 -12
  24. package/src/generate-text/stream-language-model-call.ts +32 -21
  25. package/src/generate-text/stream-text.ts +1 -1
  26. package/src/generate-text/tool-call-repair-function.ts +2 -0
  27. package/src/generate-video/generate-video.ts +61 -10
  28. package/src/middleware/wrap-provider.ts +2 -0
  29. package/src/model/as-language-model-v4.ts +166 -6
  30. package/src/test/mock-server-response.ts +19 -3
  31. package/src/text-stream/pipe-text-stream-to-response.ts +3 -5
  32. package/src/ui/http-chat-transport.ts +12 -2
  33. package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -1
  34. package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +2 -1
  35. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +1 -3
  36. package/src/util/data-url.ts +13 -1
  37. package/src/util/write-to-server-response.ts +7 -3
@@ -373,6 +373,7 @@ export async function streamLanguageModelCall<
373
373
  messages: standardizedPrompt.messages,
374
374
  repairToolCall,
375
375
  refineToolInput,
376
+ abortSignal,
376
377
  callId: effectiveCallId,
377
378
  provider: resolvedModel.provider,
378
379
  modelId: resolvedModel.modelId,
@@ -400,6 +401,7 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
400
401
  messages,
401
402
  repairToolCall,
402
403
  refineToolInput,
404
+ abortSignal,
403
405
  callId,
404
406
  provider,
405
407
  modelId,
@@ -414,6 +416,7 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
414
416
  messages: ModelMessage[];
415
417
  repairToolCall: ToolCallRepairFunction<TOOLS> | undefined;
416
418
  refineToolInput: ToolInputRefinement<TOOLS> | undefined;
419
+ abortSignal: AbortSignal | undefined;
417
420
  callId: string;
418
421
  provider: string;
419
422
  modelId: string;
@@ -637,40 +640,47 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
637
640
  callbacks: onLanguageModelCallEnd,
638
641
  });
639
642
 
640
- // Preserve the completed model call's usage, metadata, and
641
- // performance even when response validation below surfaces a
642
- // semantic error.
643
- controller.enqueue({
644
- type: 'model-call-end',
645
- finishReason: chunk.finishReason.unified,
646
- rawFinishReason: chunk.finishReason.raw,
647
- usage,
648
- providerMetadata: chunk.providerMetadata,
649
- performance,
650
- });
651
-
652
643
  const enforcedToolChoice =
653
644
  toolChoice.type === 'required' || toolChoice.type === 'tool'
654
645
  ? toolChoice
655
646
  : undefined;
656
647
 
657
- if (
648
+ const toolChoiceViolationError =
658
649
  enforcedToolChoice != null &&
659
650
  ![...toolCallsByToolCallId.values()].some(
660
651
  toolCall =>
661
652
  enforcedToolChoice.type === 'required' ||
662
653
  toolCall.toolName === enforcedToolChoice.toolName,
663
654
  )
664
- ) {
655
+ ? new ToolChoiceViolationError({
656
+ toolChoice: enforcedToolChoice,
657
+ finishReason: chunk.finishReason.unified,
658
+ provider,
659
+ modelId,
660
+ content: rawModelCallContent,
661
+ })
662
+ : undefined;
663
+
664
+ // Preserve the completed model call's usage, metadata, and
665
+ // performance even when response validation below surfaces a
666
+ // semantic error. Prevent invalid tool calls from being executed
667
+ // when the model-call-end event reaches the tool executor.
668
+ controller.enqueue({
669
+ type: 'model-call-end',
670
+ finishReason:
671
+ toolChoiceViolationError == null
672
+ ? chunk.finishReason.unified
673
+ : 'error',
674
+ rawFinishReason: chunk.finishReason.raw,
675
+ usage,
676
+ providerMetadata: chunk.providerMetadata,
677
+ performance,
678
+ });
679
+
680
+ if (toolChoiceViolationError != null) {
665
681
  controller.enqueue({
666
682
  type: 'error',
667
- error: new ToolChoiceViolationError({
668
- toolChoice: enforcedToolChoice,
669
- finishReason: chunk.finishReason.unified,
670
- provider,
671
- modelId,
672
- content: rawModelCallContent,
673
- }),
683
+ error: toolChoiceViolationError,
674
684
  });
675
685
  break;
676
686
  }
@@ -688,6 +698,7 @@ function createLanguageModelV4StreamPartToLanguageModelStreamPartTransform<
688
698
  refineToolInput,
689
699
  instructions,
690
700
  messages,
701
+ abortSignal,
691
702
  });
692
703
 
693
704
  toolCallsByToolCallId.set(toolCall.toolCallId, toolCall);
@@ -2675,7 +2675,7 @@ class DefaultStreamTextResult<
2675
2675
  tools: stepExecutionTools as TOOLS,
2676
2676
  stepInputMessages: stepMessages,
2677
2677
  abortSignal,
2678
- runtimeContext,
2678
+ toolsContext,
2679
2679
  });
2680
2680
 
2681
2681
  // Create child spans under the current step context.
@@ -17,6 +17,7 @@ import type { ToolSet } from '@ai-sdk/provider-utils';
17
17
  * @param options.tools - The tools that are available.
18
18
  * @param options.inputSchema - A function that returns the JSON Schema for a tool.
19
19
  * @param options.error - The error that occurred while parsing the tool call.
20
+ * @param options.abortSignal - An optional signal for cancelling the repair.
20
21
  */
21
22
  export type ToolCallRepairFunction<TOOLS extends ToolSet> = (options: {
22
23
  instructions: Instructions | undefined;
@@ -29,4 +30,5 @@ export type ToolCallRepairFunction<TOOLS extends ToolSet> = (options: {
29
30
  tools: TOOLS;
30
31
  inputSchema: (options: { toolName: string }) => PromiseLike<JSONSchema7>;
31
32
  error: NoSuchToolError | InvalidToolInputError;
33
+ abortSignal?: AbortSignal;
32
34
  }) => Promise<LanguageModelV4ToolCall | null>;
@@ -3,6 +3,7 @@ import type {
3
3
  Experimental_VideoModelV4CallOptions,
4
4
  Experimental_VideoModelV4File,
5
5
  Experimental_VideoModelV4Result,
6
+ Experimental_VideoModelV4OperationStatusResult,
6
7
  Experimental_VideoModelV4OperationWebhook,
7
8
  Experimental_VideoModelV4FrameImage,
8
9
  Experimental_VideoModelV4FrameType,
@@ -287,7 +288,7 @@ export async function experimental_generateVideo({
287
288
  `ai/${VERSION}`,
288
289
  );
289
290
 
290
- const { retry } = prepareRetries({
291
+ const { maxRetries, retry } = prepareRetries({
291
292
  maxRetries: maxRetriesArg,
292
293
  abortSignal,
293
294
  });
@@ -363,6 +364,7 @@ export async function experimental_generateVideo({
363
364
  callOptions,
364
365
  poll,
365
366
  webhook,
367
+ maxRetries,
366
368
  retry,
367
369
  });
368
370
  }
@@ -477,12 +479,14 @@ async function executeStartStatusFlow({
477
479
  callOptions,
478
480
  poll: pollConfig,
479
481
  webhook: webhookFactory,
482
+ maxRetries,
480
483
  retry,
481
484
  }: {
482
485
  model: Experimental_VideoModelV4;
483
486
  callOptions: Experimental_VideoModelV4CallOptions;
484
487
  poll?: GenerateVideoPollOptions;
485
488
  webhook?: GenerateVideoWebhookFactory;
489
+ maxRetries: number;
486
490
  retry: <OUTPUT>(fn: () => PromiseLike<OUTPUT>) => PromiseLike<OUTPUT>;
487
491
  }): Promise<Experimental_VideoModelV4Result> {
488
492
  // 1. If webhook and provider supports it, set up the webhook
@@ -538,6 +542,9 @@ async function executeStartStatusFlow({
538
542
  const timeoutMs = pollConfig?.timeoutMs ?? 600_000;
539
543
  const delay = pollConfig?.delay ?? defaultDelay;
540
544
  const startTime = Date.now();
545
+ const pollingTimeoutError = new Error(
546
+ `Video generation timed out after ${timeoutMs}ms.`,
547
+ );
541
548
 
542
549
  if (webhookReceived != null) {
543
550
  // 3a. Webhook flow: wait for webhook, then get final status
@@ -554,23 +561,67 @@ async function executeStartStatusFlow({
554
561
  // 3b. Polling flow (also used when webhooks are not supported)
555
562
  const elapsedMs = Date.now() - startTime;
556
563
  if (elapsedMs >= timeoutMs) {
557
- throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
564
+ throw pollingTimeoutError;
558
565
  }
559
566
  await delay(Math.min(intervalMs, timeoutMs - elapsedMs), {
560
567
  abortSignal: callOptions.abortSignal,
561
568
  });
562
569
  if (Date.now() - startTime >= timeoutMs) {
563
- throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
570
+ throw pollingTimeoutError;
564
571
  }
565
572
  }
566
573
 
567
- const statusResult = await retry(() =>
568
- model.doStatus!({
569
- operation: startResult.operation,
570
- abortSignal: callOptions.abortSignal,
571
- headers: callOptions.headers,
572
- }),
573
- );
574
+ let statusResult: Experimental_VideoModelV4OperationStatusResult;
575
+ if (webhookReceived != null) {
576
+ statusResult = await retry(() =>
577
+ model.doStatus!({
578
+ operation: startResult.operation,
579
+ abortSignal: callOptions.abortSignal,
580
+ headers: callOptions.headers,
581
+ }),
582
+ );
583
+ } else {
584
+ const statusTimeoutController = new AbortController();
585
+ const statusAbortSignal = mergeAbortSignals(
586
+ callOptions.abortSignal,
587
+ statusTimeoutController.signal,
588
+ );
589
+ const statusTimeoutId = setTimeout(
590
+ () => statusTimeoutController.abort(pollingTimeoutError),
591
+ timeoutMs - (Date.now() - startTime),
592
+ );
593
+ const statusTimeoutPromise = new Promise<never>((_, reject) => {
594
+ statusTimeoutController.signal.addEventListener(
595
+ 'abort',
596
+ () => reject(pollingTimeoutError),
597
+ { once: true },
598
+ );
599
+ });
600
+ const { retry: statusRetry } = prepareRetries({
601
+ maxRetries,
602
+ abortSignal: statusAbortSignal,
603
+ });
604
+
605
+ try {
606
+ statusResult = await Promise.race([
607
+ statusRetry(() =>
608
+ model.doStatus!({
609
+ operation: startResult.operation,
610
+ abortSignal: statusAbortSignal,
611
+ headers: callOptions.headers,
612
+ }),
613
+ ),
614
+ statusTimeoutPromise,
615
+ ]);
616
+ } catch (error) {
617
+ if (statusTimeoutController.signal.aborted) {
618
+ throw pollingTimeoutError;
619
+ }
620
+ throw error;
621
+ } finally {
622
+ clearTimeout(statusTimeoutId);
623
+ }
624
+ }
574
625
 
575
626
  if (statusResult.status === 'error') {
576
627
  throw new Error(statusResult.error);
@@ -47,5 +47,7 @@ export function wrapProvider({
47
47
  transcriptionModel: providerV4.transcriptionModel,
48
48
  speechModel: providerV4.speechModel,
49
49
  rerankingModel: providerV4.rerankingModel,
50
+ ...(providerV4.files != null ? { files: providerV4.files } : {}),
51
+ ...(providerV4.skills != null ? { skills: providerV4.skills } : {}),
50
52
  };
51
53
  }
@@ -1,8 +1,20 @@
1
- import type {
2
- LanguageModelV2,
3
- LanguageModelV3,
4
- LanguageModelV4,
1
+ import {
2
+ type LanguageModelV2,
3
+ type LanguageModelV3,
4
+ type LanguageModelV3Content,
5
+ type LanguageModelV3DataContent,
6
+ type LanguageModelV3Prompt,
7
+ type LanguageModelV3StreamPart,
8
+ type LanguageModelV3ToolResultOutput,
9
+ type LanguageModelV4,
10
+ type LanguageModelV4CallOptions,
11
+ type LanguageModelV4Content,
12
+ type LanguageModelV4Prompt,
13
+ type LanguageModelV4StreamPart,
14
+ type LanguageModelV4ToolResultOutput,
15
+ type SharedV4FileData,
5
16
  } from '@ai-sdk/provider';
17
+ import { convertUint8ArrayToBase64 } from '@ai-sdk/provider-utils';
6
18
  import { asLanguageModelV3 } from './as-language-model-v3';
7
19
 
8
20
  export function asLanguageModelV4(
@@ -18,8 +30,156 @@ export function asLanguageModelV4(
18
30
 
19
31
  return new Proxy(v3Model, {
20
32
  get(target, prop: keyof LanguageModelV3) {
21
- if (prop === 'specificationVersion') return 'v4';
22
- return target[prop];
33
+ switch (prop) {
34
+ case 'specificationVersion':
35
+ return 'v4';
36
+ case 'doGenerate':
37
+ return async (options: LanguageModelV4CallOptions) => {
38
+ const result = await target.doGenerate({
39
+ ...options,
40
+ prompt: convertV4PromptToV3(options.prompt),
41
+ });
42
+
43
+ return {
44
+ ...result,
45
+ content: result.content.map(convertV3ContentToV4),
46
+ };
47
+ };
48
+ case 'doStream':
49
+ return async (options: LanguageModelV4CallOptions) => {
50
+ const result = await target.doStream({
51
+ ...options,
52
+ prompt: convertV4PromptToV3(options.prompt),
53
+ });
54
+
55
+ return {
56
+ ...result,
57
+ stream: convertV3StreamToV4(result.stream),
58
+ };
59
+ };
60
+ default:
61
+ return target[prop];
62
+ }
23
63
  },
24
64
  }) as unknown as LanguageModelV4;
25
65
  }
66
+
67
+ function convertV4PromptToV3(
68
+ prompt: LanguageModelV4Prompt,
69
+ ): LanguageModelV3Prompt {
70
+ return prompt.map(message => {
71
+ if (message.role === 'system') {
72
+ return message;
73
+ }
74
+
75
+ return {
76
+ ...message,
77
+ content: message.content.map(part => {
78
+ switch (part.type) {
79
+ case 'file':
80
+ return {
81
+ ...part,
82
+ data: convertV4FileDataToV3(part.data),
83
+ };
84
+ case 'tool-result':
85
+ return {
86
+ ...part,
87
+ output: convertV4ToolResultOutputToV3(part.output),
88
+ };
89
+ default:
90
+ return part;
91
+ }
92
+ }),
93
+ };
94
+ }) as LanguageModelV3Prompt;
95
+ }
96
+
97
+ function convertV4FileDataToV3(
98
+ data: SharedV4FileData,
99
+ ): LanguageModelV3DataContent {
100
+ switch (data.type) {
101
+ case 'data':
102
+ return data.data;
103
+ case 'url':
104
+ return data.url;
105
+ case 'reference':
106
+ case 'text':
107
+ // pass through unsupported types as is
108
+ return data as unknown as LanguageModelV3DataContent;
109
+ }
110
+ }
111
+
112
+ function convertV4ToolResultOutputToV3(
113
+ output: LanguageModelV4ToolResultOutput,
114
+ ): LanguageModelV3ToolResultOutput {
115
+ if (output.type !== 'content') {
116
+ return output;
117
+ }
118
+
119
+ return {
120
+ ...output,
121
+ value: output.value.map(part => {
122
+ if (part.type !== 'file') {
123
+ return part;
124
+ }
125
+
126
+ switch (part.data.type) {
127
+ case 'data':
128
+ return {
129
+ type: 'file-data' as const,
130
+ data:
131
+ typeof part.data.data === 'string'
132
+ ? part.data.data
133
+ : convertUint8ArrayToBase64(part.data.data),
134
+ mediaType: part.mediaType,
135
+ filename: part.filename,
136
+ providerOptions: part.providerOptions,
137
+ };
138
+ case 'url':
139
+ return {
140
+ type: 'file-url' as const,
141
+ url: part.data.url.toString(),
142
+ providerOptions: part.providerOptions,
143
+ };
144
+ case 'reference':
145
+ return {
146
+ type: 'file-id' as const,
147
+ fileId: part.data.reference,
148
+ providerOptions: part.providerOptions,
149
+ };
150
+ case 'text':
151
+ return part;
152
+ }
153
+ }),
154
+ } as LanguageModelV3ToolResultOutput;
155
+ }
156
+
157
+ function convertV3ContentToV4(
158
+ content: LanguageModelV3Content,
159
+ ): LanguageModelV4Content {
160
+ return content.type === 'file'
161
+ ? {
162
+ ...content,
163
+ data: { type: 'data', data: content.data },
164
+ }
165
+ : content;
166
+ }
167
+
168
+ function convertV3StreamToV4(
169
+ stream: ReadableStream<LanguageModelV3StreamPart>,
170
+ ): ReadableStream<LanguageModelV4StreamPart> {
171
+ return stream.pipeThrough(
172
+ new TransformStream<LanguageModelV3StreamPart, LanguageModelV4StreamPart>({
173
+ transform(chunk, controller) {
174
+ controller.enqueue(
175
+ chunk.type === 'file'
176
+ ? {
177
+ ...chunk,
178
+ data: { type: 'data', data: chunk.data },
179
+ }
180
+ : chunk,
181
+ );
182
+ },
183
+ }),
184
+ );
185
+ }
@@ -3,7 +3,7 @@ import type { ServerResponse } from 'node:http';
3
3
 
4
4
  class MockServerResponse extends EventEmitter {
5
5
  writtenChunks: any[] = [];
6
- headers: Record<string, string> = {};
6
+ headers: Record<string, string | string[]> = {};
7
7
  statusCode = 0;
8
8
  statusMessage = '';
9
9
  ended = false;
@@ -17,6 +17,20 @@ class MockServerResponse extends EventEmitter {
17
17
  this.ended = true;
18
18
  }
19
19
 
20
+ setHeaders(headers: Headers): void {
21
+ this.headers = {};
22
+
23
+ for (const [key, value] of headers.entries()) {
24
+ const existingValue = this.headers[key];
25
+ this.headers[key] =
26
+ existingValue == null
27
+ ? value
28
+ : Array.isArray(existingValue)
29
+ ? [...existingValue, value]
30
+ : [existingValue, value];
31
+ }
32
+ }
33
+
20
34
  writeHead(
21
35
  statusCode: number,
22
36
  arg2: string | Record<string, string>,
@@ -26,8 +40,10 @@ class MockServerResponse extends EventEmitter {
26
40
 
27
41
  if (typeof arg2 === 'string') {
28
42
  this.statusMessage = arg2;
29
- this.headers = arg3 ?? {};
30
- } else {
43
+ if (arg3 != null) {
44
+ this.headers = arg3;
45
+ }
46
+ } else if (arg2 != null) {
31
47
  this.statusMessage = '';
32
48
  this.headers = arg2;
33
49
  }
@@ -29,11 +29,9 @@ export function pipeTextStreamToResponse({
29
29
  response,
30
30
  status,
31
31
  statusText,
32
- headers: Object.fromEntries(
33
- prepareHeaders(headers, {
34
- 'content-type': 'text/plain; charset=utf-8',
35
- }).entries(),
36
- ),
32
+ headers: prepareHeaders(headers, {
33
+ 'content-type': 'text/plain; charset=utf-8',
34
+ }),
37
35
  stream: stream.pipeThrough(new TextEncoderStream()),
38
36
  });
39
37
  }
@@ -10,6 +10,14 @@ import type { ChatTransport } from './chat-transport';
10
10
  import { createUIApiCallError } from './create-ui-api-call-error';
11
11
  import type { UIMessage } from './ui-messages';
12
12
 
13
+ function appendPathToUrl(url: string, path: string): string {
14
+ const queryOrFragmentStart = url.search(/[?#]/);
15
+
16
+ return queryOrFragmentStart === -1
17
+ ? `${url}${path}`
18
+ : `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
19
+ }
20
+
13
21
  export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
14
22
  options: {
15
23
  id: string;
@@ -193,7 +201,7 @@ export abstract class HttpChatTransport<
193
201
  const response = await fetch(api, {
194
202
  method: 'POST',
195
203
  headers: {
196
- 'Content-Type': 'application/json',
204
+ 'content-type': 'application/json',
197
205
  ...headers,
198
206
  },
199
207
  body: JSON.stringify(body),
@@ -239,7 +247,9 @@ export abstract class HttpChatTransport<
239
247
  requestMetadata: options.metadata,
240
248
  });
241
249
 
242
- const api = preparedRequest?.api ?? `${this.api}/${options.chatId}/stream`;
250
+ const api =
251
+ preparedRequest?.api ??
252
+ appendPathToUrl(this.api, `/${options.chatId}/stream`);
243
253
  const headers =
244
254
  preparedRequest?.headers !== undefined
245
255
  ? normalizeHeaders(preparedRequest.headers)
@@ -34,7 +34,7 @@ export function lastAssistantMessageIsCompleteWithApprovalResponses({
34
34
  // all tool approvals must have a response
35
35
  lastStepToolInvocations.every(
36
36
  part =>
37
- part.state === 'output-available' ||
37
+ (part.state === 'output-available' && part.preliminary !== true) ||
38
38
  part.state === 'output-error' ||
39
39
  part.state === 'output-denied' ||
40
40
  part.state === 'approval-responded',
@@ -32,7 +32,8 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
32
32
  lastStepToolInvocations.length > 0 &&
33
33
  lastStepToolInvocations.every(
34
34
  part =>
35
- part.state === 'output-available' || part.state === 'output-error',
35
+ (part.state === 'output-available' && part.preliminary !== true) ||
36
+ part.state === 'output-error',
36
37
  )
37
38
  );
38
39
  }
@@ -44,9 +44,7 @@ export function pipeUIMessageStreamToResponse({
44
44
  response,
45
45
  status,
46
46
  statusText,
47
- headers: Object.fromEntries(
48
- prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS).entries(),
49
- ),
47
+ headers: prepareHeaders(headers, UI_MESSAGE_STREAM_HEADERS),
50
48
  stream: sseStream.pipeThrough(new TextEncoderStream()),
51
49
  });
52
50
  }
@@ -13,6 +13,10 @@ const { atob } = globalThis;
13
13
  export function getTextFromDataUrl(dataUrl: string): string {
14
14
  const [header, base64Content] = dataUrl.split(',');
15
15
  const mediaType = header.split(';')[0].split(':')[1];
16
+ const charsetMatch = /(?:^|;)\s*charset\s*=\s*(?:"([^"]+)"|([^;\s]+))/i.exec(
17
+ header,
18
+ );
19
+ const charset = charsetMatch?.[1] ?? charsetMatch?.[2];
16
20
 
17
21
  if (mediaType == null || base64Content == null) {
18
22
  throw new InvalidArgumentError({
@@ -23,7 +27,15 @@ export function getTextFromDataUrl(dataUrl: string): string {
23
27
  }
24
28
 
25
29
  try {
26
- return atob(base64Content);
30
+ const byteString = atob(base64Content);
31
+
32
+ if (charset == null) {
33
+ return byteString;
34
+ }
35
+
36
+ return new TextDecoder(charset).decode(
37
+ Uint8Array.from(byteString, byte => byte.codePointAt(0)!),
38
+ );
27
39
  } catch {
28
40
  throw new InvalidArgumentError({
29
41
  parameter: 'dataUrl',
@@ -17,14 +17,18 @@ export function writeToServerResponse({
17
17
  response: ServerResponse;
18
18
  status?: number;
19
19
  statusText?: string;
20
- headers?: Record<string, string | number | string[]>;
20
+ headers?: Headers;
21
21
  stream: ReadableStream<Uint8Array>;
22
22
  }): Promise<void> {
23
23
  const statusCode = status ?? 200;
24
+ if (headers != null) {
25
+ response.setHeaders(headers);
26
+ }
27
+
24
28
  if (statusText !== undefined) {
25
- response.writeHead(statusCode, statusText, headers);
29
+ response.writeHead(statusCode, statusText);
26
30
  } else {
27
- response.writeHead(statusCode, headers);
31
+ response.writeHead(statusCode);
28
32
  }
29
33
 
30
34
  const reader = stream.getReader();