ai 7.0.106 → 7.0.107

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.
@@ -10,8 +10,10 @@ definition into its initial context. Register tools with `deferLoading: true`;
10
10
  search matches their names and descriptions and makes them available on the
11
11
  **next model step**.
12
12
 
13
- Use it with `generateText`, `streamText`, or `ToolLoopAgent`. The factory takes no
14
- arguments; the model supplies a search query.
13
+ Use it with `generateText`, `streamText`, `ToolLoopAgent`, or `WorkflowAgent` from
14
+ `@ai-sdk/workflow`. The factory takes no arguments; the model supplies a search
15
+ query. `WorkflowAgent` supports direct tool calling; the other APIs also support
16
+ cache-preserving code mode.
15
17
 
16
18
  ## Direct Tool Calling
17
19
 
@@ -7,8 +7,9 @@ description: Search deferred tools and load their definitions on demand for dire
7
7
 
8
8
  Creates a tool that searches the surrounding generation's deferred tools by name
9
9
  and description. The factory takes no arguments. Use it with `generateText`,
10
- `streamText`, or `ToolLoopAgent`, either with direct tool calling or with code mode
11
- configured with `toolDiscovery: 'conversation'`.
10
+ `streamText`, `ToolLoopAgent`, or `WorkflowAgent` from `@ai-sdk/workflow`.
11
+ `WorkflowAgent` supports direct tool calling. The other APIs also support code
12
+ mode configured with `toolDiscovery: 'conversation'`.
12
13
 
13
14
  ```ts
14
15
  import { toolSearch } from 'ai';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.106",
3
+ "version": "7.0.107",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.86",
45
+ "@ai-sdk/gateway": "4.0.87",
46
46
  "@ai-sdk/provider": "4.0.17",
47
- "@ai-sdk/provider-utils": "5.0.44"
47
+ "@ai-sdk/provider-utils": "5.0.45"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.87",
51
- "@ai-sdk/deepseek": "3.0.48",
52
- "@ai-sdk/google": "4.0.75",
53
- "@ai-sdk/groq": "4.0.45",
54
- "@ai-sdk/huggingface": "2.0.52",
55
- "@ai-sdk/moonshotai": "3.0.53",
56
- "@ai-sdk/openai": "4.0.70",
50
+ "@ai-sdk/amazon-bedrock": "5.0.88",
51
+ "@ai-sdk/deepseek": "3.0.49",
52
+ "@ai-sdk/google": "4.0.76",
53
+ "@ai-sdk/groq": "4.0.46",
54
+ "@ai-sdk/huggingface": "2.0.53",
55
+ "@ai-sdk/moonshotai": "3.0.54",
56
+ "@ai-sdk/openai": "4.0.71",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "5.0.3",
58
+ "@ai-sdk/xai": "5.0.4",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -155,14 +155,19 @@ export async function generateSpeech({
155
155
  model: resolvedModel.modelId,
156
156
  });
157
157
 
158
+ const detectedMediaType = detectMediaType({
159
+ data: result.audio,
160
+ topLevelType: 'audio',
161
+ });
162
+
158
163
  return new DefaultSpeechResult({
159
164
  audio: new DefaultGeneratedAudioFile({
160
165
  data: result.audio,
161
166
  mediaType:
162
- detectMediaType({
163
- data: result.audio,
164
- topLevelType: 'audio',
165
- }) ?? 'audio/mp3',
167
+ detectedMediaType ??
168
+ getResponseAudioMediaType(result.response.headers) ??
169
+ getOutputFormatMediaType(outputFormat) ??
170
+ 'audio/mp3',
166
171
  }),
167
172
  warnings: result.warnings,
168
173
  responses: [result.response],
@@ -170,6 +175,41 @@ export async function generateSpeech({
170
175
  });
171
176
  }
172
177
 
178
+ function getResponseAudioMediaType(
179
+ headers: Record<string, string> | undefined,
180
+ ): string | undefined {
181
+ const mediaType = Object.entries(headers ?? {}).find(
182
+ ([name]) => name.toLowerCase() === 'content-type',
183
+ )?.[1];
184
+
185
+ if (mediaType == null) {
186
+ return undefined;
187
+ }
188
+
189
+ const normalizedMediaType = mediaType.split(';', 1)[0].trim().toLowerCase();
190
+
191
+ if (normalizedMediaType.length === 0) {
192
+ return undefined;
193
+ }
194
+
195
+ return normalizedMediaType.startsWith('audio/')
196
+ ? normalizedMediaType
197
+ : undefined;
198
+ }
199
+
200
+ function getOutputFormatMediaType(outputFormat: string | undefined) {
201
+ if (outputFormat == null) {
202
+ return undefined;
203
+ }
204
+
205
+ const normalizedOutputFormat = outputFormat.trim().toLowerCase();
206
+
207
+ return normalizedOutputFormat === 'pcm' ||
208
+ normalizedOutputFormat === 'audio/pcm'
209
+ ? 'audio/pcm'
210
+ : undefined;
211
+ }
212
+
173
213
  class DefaultSpeechResult implements SpeechResult {
174
214
  readonly audio: GeneratedAudioFile;
175
215
  readonly warnings: Array<Warning>;
@@ -118,6 +118,7 @@ import type {
118
118
  OnToolExecutionEndCallback,
119
119
  OnToolExecutionStartCallback,
120
120
  } from './tool-execution-events';
121
+ import { validateToolContext } from './validate-tool-context';
121
122
  import type { ToolInputRefinement } from './tool-input-refinement';
122
123
  import type { ToolOrder } from './tool-order';
123
124
  import type { ToolOutput } from './tool-output';
@@ -1183,23 +1184,34 @@ export async function generateText<
1183
1184
  continue;
1184
1185
  }
1185
1186
 
1186
- if (tool.onInputStart != null) {
1187
- await tool.onInputStart({
1188
- toolCallId: toolCall.toolCallId,
1189
- messages: stepMessages,
1190
- abortSignal: mergedAbortSignal,
1191
- context: runtimeContext,
1187
+ if (
1188
+ tool.onInputStart != null ||
1189
+ tool.onInputAvailable != null
1190
+ ) {
1191
+ const context = await validateToolContext({
1192
+ toolName: toolCall.toolName,
1193
+ context: getOwn(toolsContext, toolCall.toolName),
1194
+ contextSchema: tool.contextSchema,
1192
1195
  });
1193
- }
1194
1196
 
1195
- if (tool?.onInputAvailable != null) {
1196
- await tool.onInputAvailable({
1197
- input: toolCall.input,
1198
- toolCallId: toolCall.toolCallId,
1199
- messages: stepMessages,
1200
- abortSignal: mergedAbortSignal,
1201
- context: runtimeContext,
1202
- });
1197
+ if (tool.onInputStart != null) {
1198
+ await tool.onInputStart({
1199
+ toolCallId: toolCall.toolCallId,
1200
+ messages: stepMessages,
1201
+ abortSignal: mergedAbortSignal,
1202
+ context,
1203
+ });
1204
+ }
1205
+
1206
+ if (tool.onInputAvailable != null) {
1207
+ await tool.onInputAvailable({
1208
+ input: toolCall.input,
1209
+ toolCallId: toolCall.toolCallId,
1210
+ messages: stepMessages,
1211
+ abortSignal: mergedAbortSignal,
1212
+ context,
1213
+ });
1214
+ }
1203
1215
  }
1204
1216
 
1205
1217
  const toolApprovalStatus = await resolveToolApproval({
@@ -1,4 +1,8 @@
1
- import type { Context, ModelMessage, ToolSet } from '@ai-sdk/provider-utils';
1
+ import type {
2
+ InferToolSetContext,
3
+ ModelMessage,
4
+ ToolSet,
5
+ } from '@ai-sdk/provider-utils';
2
6
  import { createIdMap } from '../util/create-id-map';
3
7
  import { getOwn } from '../util/get-own';
4
8
  import type { LanguageModelStreamPart } from './stream-language-model-call';
@@ -6,30 +10,61 @@ import {
6
10
  isStreamRetryAttemptBoundaryPart,
7
11
  type StreamRetryAttemptBoundaryPart,
8
12
  } from './stream-retry-attempt-boundary';
13
+ import { validateToolContext } from './validate-tool-context';
9
14
 
10
15
  type ToolCallbackStreamPart<TOOLS extends ToolSet> =
11
16
  | LanguageModelStreamPart<TOOLS>
12
17
  | StreamRetryAttemptBoundaryPart;
13
18
 
14
- export function invokeToolCallbacksFromStream<
15
- TOOLS extends ToolSet,
16
- RUNTIME_CONTEXT extends Context,
17
- >({
19
+ export function invokeToolCallbacksFromStream<TOOLS extends ToolSet>({
18
20
  stream,
19
21
  tools,
20
22
  stepInputMessages,
21
23
  abortSignal,
22
- runtimeContext,
24
+ toolsContext,
23
25
  }: {
24
26
  stream: ReadableStream<ToolCallbackStreamPart<TOOLS>>;
25
27
  tools: TOOLS | undefined;
26
28
  stepInputMessages: Array<ModelMessage>;
27
29
  abortSignal: AbortSignal | undefined;
28
- runtimeContext: RUNTIME_CONTEXT;
30
+ toolsContext: InferToolSetContext<TOOLS>;
29
31
  }): ReadableStream<ToolCallbackStreamPart<TOOLS>> {
30
32
  if (tools == null) return stream;
31
33
 
32
- const ongoingToolCallToolNames: Record<string, string> = createIdMap();
34
+ let ongoingToolCalls: Record<
35
+ string,
36
+ {
37
+ toolName: string;
38
+ validatedContext: Promise<unknown> | undefined;
39
+ }
40
+ > = createIdMap();
41
+
42
+ const getValidatedContext = ({
43
+ toolCallId,
44
+ toolName,
45
+ }: {
46
+ toolCallId: string;
47
+ toolName: string;
48
+ }): Promise<unknown> => {
49
+ const ongoingToolCall = ongoingToolCalls[toolCallId];
50
+
51
+ if (ongoingToolCall?.validatedContext != null) {
52
+ return ongoingToolCall.validatedContext;
53
+ }
54
+
55
+ const tool = getOwn(tools, toolName);
56
+ const validatedContext = validateToolContext({
57
+ toolName,
58
+ context: getOwn(toolsContext, toolName),
59
+ contextSchema: tool?.contextSchema,
60
+ });
61
+
62
+ if (ongoingToolCall != null) {
63
+ ongoingToolCall.validatedContext = validatedContext;
64
+ }
65
+
66
+ return validatedContext;
67
+ };
33
68
 
34
69
  return stream.pipeThrough(
35
70
  new TransformStream({
@@ -37,12 +72,16 @@ export function invokeToolCallbacksFromStream<
37
72
  controller.enqueue(chunk);
38
73
 
39
74
  if (isStreamRetryAttemptBoundaryPart(chunk)) {
75
+ ongoingToolCalls = createIdMap();
40
76
  return;
41
77
  }
42
78
 
43
79
  switch (chunk.type) {
44
80
  case 'tool-input-start': {
45
- ongoingToolCallToolNames[chunk.id] = chunk.toolName;
81
+ ongoingToolCalls[chunk.id] = {
82
+ toolName: chunk.toolName,
83
+ validatedContext: undefined,
84
+ };
46
85
 
47
86
  const tool = getOwn(tools, chunk.toolName);
48
87
  if (tool?.onInputStart != null) {
@@ -50,7 +89,10 @@ export function invokeToolCallbacksFromStream<
50
89
  toolCallId: chunk.id,
51
90
  messages: stepInputMessages,
52
91
  abortSignal,
53
- context: runtimeContext,
92
+ context: await getValidatedContext({
93
+ toolCallId: chunk.id,
94
+ toolName: chunk.toolName,
95
+ }),
54
96
  });
55
97
  }
56
98
 
@@ -58,7 +100,7 @@ export function invokeToolCallbacksFromStream<
58
100
  }
59
101
 
60
102
  case 'tool-input-delta': {
61
- const toolName = ongoingToolCallToolNames[chunk.id];
103
+ const toolName = ongoingToolCalls[chunk.id]?.toolName;
62
104
  const tool = getOwn(tools, toolName);
63
105
 
64
106
  if (tool?.onInputDelta != null) {
@@ -67,7 +109,10 @@ export function invokeToolCallbacksFromStream<
67
109
  toolCallId: chunk.id,
68
110
  messages: stepInputMessages,
69
111
  abortSignal,
70
- context: runtimeContext,
112
+ context: await getValidatedContext({
113
+ toolCallId: chunk.id,
114
+ toolName,
115
+ }),
71
116
  });
72
117
  }
73
118
 
@@ -75,19 +120,26 @@ export function invokeToolCallbacksFromStream<
75
120
  }
76
121
 
77
122
  case 'tool-call': {
78
- const toolName = ongoingToolCallToolNames[chunk.toolCallId];
123
+ const toolName = ongoingToolCalls[chunk.toolCallId]?.toolName;
79
124
  const tool = getOwn(tools, toolName);
80
125
 
81
- delete ongoingToolCallToolNames[chunk.toolCallId];
82
-
83
126
  if (!chunk.invalid && tool?.onInputAvailable != null) {
127
+ const validatedContext = getValidatedContext({
128
+ toolCallId: chunk.toolCallId,
129
+ toolName,
130
+ });
131
+
132
+ delete ongoingToolCalls[chunk.toolCallId];
133
+
84
134
  await tool.onInputAvailable({
85
135
  input: chunk.input,
86
136
  toolCallId: chunk.toolCallId,
87
137
  messages: stepInputMessages,
88
138
  abortSignal,
89
- context: runtimeContext,
139
+ context: await validatedContext,
90
140
  });
141
+ } else {
142
+ delete ongoingToolCalls[chunk.toolCallId];
91
143
  }
92
144
  }
93
145
  }
@@ -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.
@@ -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
  }
@@ -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();