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.
- package/CHANGELOG.md +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +390 -95
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +16 -2
- package/dist/internal/index.js +323 -53
- package/dist/internal/index.js.map +1 -1
- package/docs/00-introduction/index.mdx +1 -1
- package/docs/02-getting-started/03-nextjs-pages-router.mdx +1 -1
- package/docs/02-getting-started/05-nuxt.mdx +1 -1
- package/docs/03-ai-sdk-core/16-mcp-tools.mdx +31 -0
- package/docs/03-ai-sdk-core/19-tool-search.mdx +4 -2
- package/docs/04-ai-sdk-ui/02-chatbot.mdx +26 -11
- package/docs/04-ai-sdk-ui/05-completion.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +7 -0
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +7 -0
- package/docs/07-reference/01-ai-sdk-core/23-tool-search.mdx +3 -2
- package/docs/07-reference/02-ai-sdk-ui/01-use-chat.mdx +1 -1
- package/package.json +11 -11
- package/src/generate-speech/generate-speech.ts +44 -4
- package/src/generate-text/generate-text.ts +29 -16
- package/src/generate-text/invoke-tool-callbacks-from-stream.ts +68 -16
- package/src/generate-text/parse-tool-call.ts +64 -12
- package/src/generate-text/stream-language-model-call.ts +32 -21
- package/src/generate-text/stream-text.ts +1 -1
- package/src/generate-text/tool-call-repair-function.ts +2 -0
- package/src/generate-video/generate-video.ts +61 -10
- package/src/middleware/wrap-provider.ts +2 -0
- package/src/model/as-language-model-v4.ts +166 -6
- 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/util/data-url.ts +13 -1
- 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
|
-
|
|
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:
|
|
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);
|
|
@@ -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
|
|
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);
|
|
@@ -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
|
|
2
|
-
LanguageModelV2,
|
|
3
|
-
LanguageModelV3,
|
|
4
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
}
|
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',
|
|
@@ -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();
|