ai 7.0.105 → 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.
- package/CHANGELOG.md +42 -0
- package/dist/index.js +278 -88
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +15 -3
- package/dist/internal/index.js +153 -46
- package/dist/internal/index.js.map +1 -1
- package/docs/03-ai-sdk-core/19-tool-search.mdx +4 -2
- package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +3 -2
- package/package.json +11 -11
- package/src/batch/batch.ts +1 -0
- package/src/generate-object/generate-object.ts +1 -0
- package/src/generate-object/stream-object.ts +1 -0
- package/src/generate-speech/generate-speech.ts +44 -4
- package/src/generate-text/generate-text.ts +28 -15
- package/src/generate-text/invoke-tool-callbacks-from-stream.ts +68 -16
- package/src/generate-text/prune-messages.ts +13 -0
- package/src/generate-text/stream-language-model-call.ts +1 -0
- package/src/generate-text/stream-text.ts +29 -12
- package/src/generate-video/generate-video.ts +61 -10
- package/src/middleware/extract-json-middleware.ts +35 -9
- package/src/middleware/simulate-streaming-middleware.ts +7 -1
- package/src/middleware/wrap-provider.ts +2 -0
- package/src/prompt/convert-to-language-model-prompt.ts +4 -2
- package/src/prompt/create-tool-model-output.ts +20 -11
- package/src/test/mock-server-response.ts +19 -3
- package/src/text-stream/pipe-text-stream-to-response.ts +3 -5
- package/src/ui/http-chat-transport.ts +12 -2
- package/src/ui/last-assistant-message-is-complete-with-approval-responses.ts +1 -1
- package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +2 -1
- package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +1 -3
- package/src/ui-message-stream/read-ui-message-stream.ts +13 -4
- package/src/util/data-url.ts +13 -1
- package/src/util/download/download-function.ts +5 -2
- package/src/util/write-to-server-response.ts +7 -3
|
@@ -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
|
|
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
|
|
570
|
+
throw pollingTimeoutError;
|
|
564
571
|
}
|
|
565
572
|
}
|
|
566
573
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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);
|
|
@@ -19,6 +19,28 @@ function stripMarkdownCodeFenceSuffix(text: string): string {
|
|
|
19
19
|
return text.replace(/\n?```\s*$/, '').trimEnd();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function getPotentialSuffixStart(text: string): number {
|
|
23
|
+
let index = text.length;
|
|
24
|
+
|
|
25
|
+
while (index > 0 && /\s/.test(text[index - 1])) {
|
|
26
|
+
index--;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let backtickCount = 0;
|
|
30
|
+
while (index > 0 && backtickCount < 3 && text[index - 1] === '`') {
|
|
31
|
+
index--;
|
|
32
|
+
backtickCount++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (backtickCount > 0) {
|
|
36
|
+
while (index > 0 && /\s/.test(text[index - 1])) {
|
|
37
|
+
index--;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return index;
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
/**
|
|
23
45
|
* Middleware that extracts JSON from text content by stripping
|
|
24
46
|
* markdown code fences and other formatting.
|
|
@@ -75,8 +97,6 @@ export function extractJsonMiddleware(options?: {
|
|
|
75
97
|
}
|
|
76
98
|
> = createIdMap();
|
|
77
99
|
|
|
78
|
-
const SUFFIX_BUFFER_SIZE = 12;
|
|
79
|
-
|
|
80
100
|
return {
|
|
81
101
|
stream: stream.pipeThrough(
|
|
82
102
|
new TransformStream<
|
|
@@ -145,13 +165,19 @@ export function extractJsonMiddleware(options?: {
|
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
// Stream content
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
// Stream content while retaining anything that could still
|
|
169
|
+
// become trailing whitespace or a markdown fence suffix.
|
|
170
|
+
if (block.phase === 'streaming') {
|
|
171
|
+
const potentialSuffixStart = getPotentialSuffixStart(
|
|
172
|
+
block.buffer,
|
|
173
|
+
);
|
|
174
|
+
const toStream = block.buffer.slice(0, potentialSuffixStart);
|
|
175
|
+
block.buffer = block.buffer.slice(potentialSuffixStart);
|
|
176
|
+
|
|
177
|
+
if (toStream.length === 0) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
controller.enqueue({
|
|
156
182
|
type: 'text-delta',
|
|
157
183
|
id: chunk.id,
|
|
@@ -25,7 +25,13 @@ export function simulateStreamingMiddleware(): LanguageModelMiddleware {
|
|
|
25
25
|
switch (part.type) {
|
|
26
26
|
case 'text': {
|
|
27
27
|
if (part.text.length > 0) {
|
|
28
|
-
controller.enqueue({
|
|
28
|
+
controller.enqueue({
|
|
29
|
+
type: 'text-start',
|
|
30
|
+
id: String(id),
|
|
31
|
+
...(part.providerMetadata != null
|
|
32
|
+
? { providerMetadata: part.providerMetadata }
|
|
33
|
+
: {}),
|
|
34
|
+
});
|
|
29
35
|
controller.enqueue({
|
|
30
36
|
type: 'text-delta',
|
|
31
37
|
id: String(id),
|
|
@@ -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
|
}
|
|
@@ -36,7 +36,8 @@ import { MissingToolResultsError } from '../error/missing-tool-result-error';
|
|
|
36
36
|
export async function convertToLanguageModelPrompt({
|
|
37
37
|
prompt,
|
|
38
38
|
supportedUrls,
|
|
39
|
-
download
|
|
39
|
+
download,
|
|
40
|
+
abortSignal,
|
|
40
41
|
// `provider` is only needed here to convert legacy tool output types via `mapToolResultOutput`.
|
|
41
42
|
// TODO: remove in v8 when "file-id" and "image-file-id" types are removed
|
|
42
43
|
provider,
|
|
@@ -44,11 +45,12 @@ export async function convertToLanguageModelPrompt({
|
|
|
44
45
|
prompt: StandardizedPrompt;
|
|
45
46
|
supportedUrls: Record<string, RegExp[]>;
|
|
46
47
|
download: DownloadFunction | undefined;
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
47
49
|
provider?: string;
|
|
48
50
|
}): Promise<LanguageModelV4Prompt> {
|
|
49
51
|
const downloadedAssets = await downloadAssets(
|
|
50
52
|
prompt.messages,
|
|
51
|
-
download,
|
|
53
|
+
download ?? createDefaultDownloadFunction(undefined, abortSignal),
|
|
52
54
|
supportedUrls,
|
|
53
55
|
);
|
|
54
56
|
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { getErrorMessage, type JSONValue } from '@ai-sdk/provider';
|
|
2
|
-
import {
|
|
3
|
-
parseJSON,
|
|
4
|
-
type Tool,
|
|
5
|
-
type ToolResultOutput,
|
|
6
|
-
} from '@ai-sdk/provider-utils';
|
|
2
|
+
import type { Tool, ToolResultOutput } from '@ai-sdk/provider-utils';
|
|
7
3
|
|
|
8
4
|
export async function createToolModelOutput({
|
|
9
5
|
toolCallId,
|
|
@@ -21,7 +17,7 @@ export async function createToolModelOutput({
|
|
|
21
17
|
if (errorMode === 'text') {
|
|
22
18
|
return { type: 'error-text', value: getErrorMessage(output) };
|
|
23
19
|
} else if (errorMode === 'json') {
|
|
24
|
-
return { type: 'error-json', value:
|
|
20
|
+
return { type: 'error-json', value: toJSONValue(output) };
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
if (tool?.toModelOutput) {
|
|
@@ -30,16 +26,29 @@ export async function createToolModelOutput({
|
|
|
30
26
|
|
|
31
27
|
return typeof output === 'string'
|
|
32
28
|
? { type: 'text', value: output }
|
|
33
|
-
: { type: 'json', value:
|
|
29
|
+
: { type: 'json', value: toJSONValue(output) };
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes an in-process tool output to a plain JSON value
|
|
34
|
+
* (applies `toJSON`, converts `Date`, drops `undefined`, etc.)
|
|
35
|
+
* by round-tripping it through `JSON.stringify`.
|
|
36
|
+
*
|
|
37
|
+
* The parsed text is produced by `JSON.stringify` from a value that is
|
|
38
|
+
* already materialized in this process, so it is not untrusted input.
|
|
39
|
+
* `JSON.parse` is used deliberately instead of the secure parser from
|
|
40
|
+
* `@ai-sdk/provider-utils`: the secure parser rejects own `__proto__` and
|
|
41
|
+
* `constructor.prototype` keys, which are valid data in tool outputs
|
|
42
|
+
* (e.g. rows from an external API). `JSON.parse` defines `__proto__` as an
|
|
43
|
+
* own data property and never modifies the prototype chain, so preserving
|
|
44
|
+
* these keys here is safe. Do not reuse this pattern for text that comes
|
|
45
|
+
* from outside the process; use `parseJSON` / `safeParseJSON` instead.
|
|
46
|
+
*/
|
|
47
|
+
function toJSONValue(value: unknown): JSONValue {
|
|
37
48
|
if (value === undefined) {
|
|
38
49
|
return null;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
const serialized = JSON.stringify(value);
|
|
42
|
-
return serialized === undefined
|
|
43
|
-
? null
|
|
44
|
-
: await parseJSON({ text: serialized });
|
|
53
|
+
return serialized === undefined ? null : JSON.parse(serialized);
|
|
45
54
|
}
|
|
@@ -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
|
-
|
|
30
|
-
|
|
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:
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
'
|
|
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 =
|
|
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'
|
|
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:
|
|
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
|
}
|
|
@@ -65,11 +65,17 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
65
65
|
}): AsyncIterableStream<UI_MESSAGE> {
|
|
66
66
|
let controller: ReadableStreamDefaultController<UI_MESSAGE> | undefined;
|
|
67
67
|
let hasErrored = false;
|
|
68
|
+
let isCancelled = false;
|
|
69
|
+
const abortController = new AbortController();
|
|
68
70
|
|
|
69
71
|
const outputStream = new ReadableStream<UI_MESSAGE>({
|
|
70
72
|
start(controllerParam) {
|
|
71
73
|
controller = controllerParam;
|
|
72
74
|
},
|
|
75
|
+
cancel() {
|
|
76
|
+
isCancelled = true;
|
|
77
|
+
abortController.abort();
|
|
78
|
+
},
|
|
73
79
|
});
|
|
74
80
|
|
|
75
81
|
const state = createStreamingUIMessageState<UI_MESSAGE>({
|
|
@@ -98,17 +104,20 @@ export function readUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
98
104
|
return job({
|
|
99
105
|
state,
|
|
100
106
|
write: () => {
|
|
101
|
-
|
|
107
|
+
if (!isCancelled) {
|
|
108
|
+
controller?.enqueue(createUIMessageSnapshot(state.message));
|
|
109
|
+
}
|
|
102
110
|
},
|
|
103
111
|
});
|
|
104
112
|
},
|
|
105
113
|
onError: handleError,
|
|
106
114
|
}),
|
|
107
115
|
onError: handleError,
|
|
116
|
+
abortSignal: abortController.signal,
|
|
108
117
|
}).finally(() => {
|
|
109
|
-
// Only close if no error occurred.
|
|
110
|
-
//
|
|
111
|
-
if (!hasErrored) {
|
|
118
|
+
// Only close if no error or cancellation occurred. Both erroring and
|
|
119
|
+
// cancelling a stream close its controller.
|
|
120
|
+
if (!hasErrored && !isCancelled) {
|
|
112
121
|
controller?.close();
|
|
113
122
|
}
|
|
114
123
|
});
|
package/src/util/data-url.ts
CHANGED
|
@@ -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
|
-
|
|
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',
|
|
@@ -34,12 +34,15 @@ export type DownloadFunction = (
|
|
|
34
34
|
* Downloads the file if it is not supported by the model.
|
|
35
35
|
*/
|
|
36
36
|
export const createDefaultDownloadFunction =
|
|
37
|
-
(
|
|
37
|
+
(
|
|
38
|
+
download: typeof originalDownload = originalDownload,
|
|
39
|
+
abortSignal?: AbortSignal,
|
|
40
|
+
): DownloadFunction =>
|
|
38
41
|
requestedDownloads =>
|
|
39
42
|
Promise.all(
|
|
40
43
|
requestedDownloads.map(async requestedDownload =>
|
|
41
44
|
requestedDownload.isUrlSupportedByModel
|
|
42
45
|
? null
|
|
43
|
-
: await download(requestedDownload),
|
|
46
|
+
: await download({ ...requestedDownload, abortSignal }),
|
|
44
47
|
),
|
|
45
48
|
);
|
|
@@ -17,14 +17,18 @@ export function writeToServerResponse({
|
|
|
17
17
|
response: ServerResponse;
|
|
18
18
|
status?: number;
|
|
19
19
|
statusText?: string;
|
|
20
|
-
headers?:
|
|
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
|
|
29
|
+
response.writeHead(statusCode, statusText);
|
|
26
30
|
} else {
|
|
27
|
-
response.writeHead(statusCode
|
|
31
|
+
response.writeHead(statusCode);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
const reader = stream.getReader();
|