ai 7.0.119 → 7.0.122
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 +32 -0
- package/dist/index.d.ts +30 -3
- package/dist/index.js +132 -36
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/02-foundations/02-providers-and-models.mdx +1 -0
- package/docs/02-getting-started/00-choosing-a-provider.mdx +4 -4
- package/docs/02-getting-started/02-nextjs-app-router.mdx +2 -2
- package/docs/02-getting-started/03-nextjs-pages-router.mdx +2 -2
- package/docs/02-getting-started/04-svelte.mdx +6 -6
- package/docs/02-getting-started/05-nuxt.mdx +6 -6
- package/docs/02-getting-started/06-nodejs.mdx +2 -2
- package/docs/02-getting-started/07-expo.mdx +2 -2
- package/docs/02-getting-started/08-tanstack-start.mdx +2 -2
- package/docs/03-agents/06-policy-tool-approvals.mdx +2 -2
- package/docs/03-agents/07-workflow-agent.mdx +10 -10
- package/docs/03-ai-sdk-core/20-prompt-engineering.mdx +1 -1
- package/docs/03-ai-sdk-core/26-reasoning.mdx +1 -1
- package/docs/03-ai-sdk-core/45-provider-management.mdx +4 -4
- package/docs/04-ai-sdk-ui/02-chatbot.mdx +10 -1
- package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +8 -0
- package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +10 -3
- package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +10 -1
- package/docs/05-ai-sdk-rsc/04-multistep-interfaces.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
- package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +8 -8
- package/docs/08-migration-guides/29-migration-guide-4-0.mdx +5 -5
- package/docs/08-migration-guides/37-migration-guide-3-3.mdx +1 -1
- package/package.json +11 -11
- package/src/embed/embed-many.ts +5 -3
- package/src/generate-image/generate-image.ts +8 -3
- package/src/generate-text/smooth-stream.ts +24 -9
- package/src/generate-text/stream-text-result.ts +5 -0
- package/src/generate-video/generate-video.ts +6 -4
- package/src/registry/provider-registry.ts +1 -1
- package/src/ui/chat.ts +45 -7
- package/src/ui/http-chat-transport.ts +19 -3
- package/src/ui/last-assistant-message-is-complete-with-tool-calls.ts +15 -3
- package/src/ui/process-ui-message-stream.ts +59 -18
- package/src/ui/ui-messages.ts +14 -0
- package/src/ui/validate-ui-messages.ts +2 -0
- package/src/ui-message-stream/to-ui-message-chunk.ts +9 -1
- package/src/ui-message-stream/to-ui-message-stream.ts +1 -1
- package/src/util/set-own.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.122",
|
|
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.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.100",
|
|
46
46
|
"@ai-sdk/provider": "4.0.19",
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.51"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@ai-sdk/amazon-bedrock": "5.0.
|
|
51
|
-
"@ai-sdk/deepseek": "3.0.
|
|
52
|
-
"@ai-sdk/google": "4.0.
|
|
53
|
-
"@ai-sdk/groq": "4.0.
|
|
54
|
-
"@ai-sdk/huggingface": "2.0.
|
|
55
|
-
"@ai-sdk/moonshotai": "3.0.
|
|
56
|
-
"@ai-sdk/openai": "4.0.
|
|
50
|
+
"@ai-sdk/amazon-bedrock": "5.0.100",
|
|
51
|
+
"@ai-sdk/deepseek": "3.0.56",
|
|
52
|
+
"@ai-sdk/google": "4.0.85",
|
|
53
|
+
"@ai-sdk/groq": "4.0.52",
|
|
54
|
+
"@ai-sdk/huggingface": "2.0.59",
|
|
55
|
+
"@ai-sdk/moonshotai": "3.0.60",
|
|
56
|
+
"@ai-sdk/openai": "4.0.80",
|
|
57
57
|
"@ai-sdk/test-server": "2.0.2",
|
|
58
|
-
"@ai-sdk/xai": "5.0.
|
|
58
|
+
"@ai-sdk/xai": "5.0.12",
|
|
59
59
|
"@edge-runtime/vm": "^5.0.0",
|
|
60
60
|
"@smithy/eventstream-codec": "^4.3.3",
|
|
61
61
|
"@smithy/util-utf8": "^4.3.3",
|
package/src/embed/embed-many.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { TelemetryOptions } from '../telemetry/telemetry-options';
|
|
|
14
14
|
import type { Embedding, EmbeddingModel, ProviderMetadata } from '../types';
|
|
15
15
|
import type { Warning } from '../types/warning';
|
|
16
16
|
import type { Callback } from '../util/callback';
|
|
17
|
+
import { getOwn } from '../util/get-own';
|
|
18
|
+
import { setOwn } from '../util/set-own';
|
|
17
19
|
import { notify } from '../util/notify';
|
|
18
20
|
import { prepareRetries } from '../util/prepare-retries';
|
|
19
21
|
import { splitArray } from '../util/split-array';
|
|
@@ -427,10 +429,10 @@ export async function embedMany<RUNTIME_CONTEXT extends Context = Context>({
|
|
|
427
429
|
for (const [providerName, metadata] of Object.entries(
|
|
428
430
|
result.providerMetadata,
|
|
429
431
|
)) {
|
|
430
|
-
providerMetadata
|
|
431
|
-
...providerMetadata
|
|
432
|
+
setOwn(providerMetadata, providerName, {
|
|
433
|
+
...getOwn(providerMetadata, providerName),
|
|
432
434
|
...metadata,
|
|
433
|
-
};
|
|
435
|
+
});
|
|
434
436
|
}
|
|
435
437
|
}
|
|
436
438
|
}
|
|
@@ -25,6 +25,8 @@ import type { ImageModel } from '../types/image-model';
|
|
|
25
25
|
import type { ImageModelResponseMetadata } from '../types/image-model-response-metadata';
|
|
26
26
|
import { addImageModelUsage, type ImageModelUsage } from '../types/usage';
|
|
27
27
|
import type { Warning } from '../types/warning';
|
|
28
|
+
import { getOwn } from '../util/get-own';
|
|
29
|
+
import { setOwn } from '../util/set-own';
|
|
28
30
|
import { prepareRetries } from '../util/prepare-retries';
|
|
29
31
|
import { RetryError } from '../util/retry-error';
|
|
30
32
|
import { VERSION } from '../version';
|
|
@@ -324,8 +326,11 @@ export async function generateImage({
|
|
|
324
326
|
.images;
|
|
325
327
|
}
|
|
326
328
|
} else {
|
|
327
|
-
providerMetadata
|
|
328
|
-
|
|
329
|
+
const currentEntry = getOwn(providerMetadata, providerName) ?? {
|
|
330
|
+
images: [],
|
|
331
|
+
};
|
|
332
|
+
currentEntry.images.push(...metadata.images);
|
|
333
|
+
setOwn(providerMetadata, providerName, currentEntry);
|
|
329
334
|
}
|
|
330
335
|
}
|
|
331
336
|
}
|
|
@@ -395,7 +400,7 @@ export function getImageProviderMetadata(
|
|
|
395
400
|
const value = metadata.images?.[imageIndex];
|
|
396
401
|
|
|
397
402
|
if (isJSONObject(value) && !Array.isArray(value)) {
|
|
398
|
-
(imageMetadata ??= {})
|
|
403
|
+
setOwn((imageMetadata ??= {}), providerName, value);
|
|
399
404
|
}
|
|
400
405
|
}
|
|
401
406
|
|
|
@@ -159,10 +159,20 @@ export function smoothStream<TOOLS extends ToolSet>({
|
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
if (chunk.text.length === 0 && chunk.providerMetadata != null) {
|
|
163
|
+
flushBuffer(controller);
|
|
164
|
+
controller.enqueue(chunk);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Flush at metadata boundaries because one output part cannot preserve
|
|
169
|
+
// metadata from multiple input deltas.
|
|
163
170
|
if (
|
|
164
|
-
|
|
165
|
-
(
|
|
171
|
+
buffer.length > 0 &&
|
|
172
|
+
(chunk.type !== type ||
|
|
173
|
+
chunk.id !== id ||
|
|
174
|
+
providerMetadata != null ||
|
|
175
|
+
chunk.providerMetadata != null)
|
|
166
176
|
) {
|
|
167
177
|
flushBuffer(controller);
|
|
168
178
|
}
|
|
@@ -170,20 +180,25 @@ export function smoothStream<TOOLS extends ToolSet>({
|
|
|
170
180
|
buffer += chunk.text;
|
|
171
181
|
id = chunk.id;
|
|
172
182
|
type = chunk.type;
|
|
173
|
-
|
|
174
|
-
// Preserve providerMetadata (e.g., Anthropic thinking signatures)
|
|
175
|
-
if (chunk.providerMetadata != null) {
|
|
176
|
-
providerMetadata = chunk.providerMetadata;
|
|
177
|
-
}
|
|
183
|
+
providerMetadata = chunk.providerMetadata;
|
|
178
184
|
|
|
179
185
|
let match;
|
|
180
186
|
|
|
181
187
|
while ((match = detectChunk(buffer)) != null) {
|
|
182
|
-
controller.enqueue({
|
|
188
|
+
controller.enqueue({
|
|
189
|
+
type,
|
|
190
|
+
text: match,
|
|
191
|
+
id,
|
|
192
|
+
...(providerMetadata != null ? { providerMetadata } : {}),
|
|
193
|
+
});
|
|
183
194
|
buffer = buffer.slice(match.length);
|
|
184
195
|
|
|
185
196
|
await delay(isDocumentHidden() ? null : delayInMs);
|
|
186
197
|
}
|
|
198
|
+
|
|
199
|
+
if (buffer.length === 0) {
|
|
200
|
+
providerMetadata = undefined;
|
|
201
|
+
}
|
|
187
202
|
},
|
|
188
203
|
});
|
|
189
204
|
};
|
|
@@ -104,6 +104,11 @@ export type UIMessageStreamOptions<UI_MESSAGE extends UIMessage> = {
|
|
|
104
104
|
/**
|
|
105
105
|
* Process an error, e.g. to log it. Default to `() => 'An error occurred.'`.
|
|
106
106
|
*
|
|
107
|
+
* Tool execution errors marked `providerExecuted: true` bypass this callback.
|
|
108
|
+
* Their strings pass through unchanged; other values are JSON-stringified to
|
|
109
|
+
* preserve provider error data for model-message round trips and harness
|
|
110
|
+
* runtime error messages. Stream errors and invalid tool calls still use it.
|
|
111
|
+
*
|
|
107
112
|
* @returns error message to include in the data stream.
|
|
108
113
|
*/
|
|
109
114
|
onError?: (error: unknown) => string;
|
|
@@ -29,6 +29,8 @@ import { resolveVideoModel } from '../model/resolve-model';
|
|
|
29
29
|
import type { VideoModel } from '../types/video-model';
|
|
30
30
|
import type { VideoModelResponseMetadata } from '../types/video-model-response-metadata';
|
|
31
31
|
import type { Warning } from '../types/warning';
|
|
32
|
+
import { getOwn } from '../util/get-own';
|
|
33
|
+
import { setOwn } from '../util/set-own';
|
|
32
34
|
import { createDownload } from '../util/download/create-download';
|
|
33
35
|
import { prepareRetries } from '../util/prepare-retries';
|
|
34
36
|
import { VERSION } from '../version';
|
|
@@ -697,17 +699,17 @@ function mergeProviderMetadata(
|
|
|
697
699
|
source: SharedV4ProviderMetadata,
|
|
698
700
|
): void {
|
|
699
701
|
for (const [providerName, metadataValue] of Object.entries(source)) {
|
|
700
|
-
const existingMetadata = target
|
|
702
|
+
const existingMetadata = getOwn(target, providerName);
|
|
701
703
|
if (
|
|
702
704
|
existingMetadata != null &&
|
|
703
705
|
typeof existingMetadata === 'object' &&
|
|
704
706
|
metadataValue != null &&
|
|
705
707
|
typeof metadataValue === 'object'
|
|
706
708
|
) {
|
|
707
|
-
target
|
|
709
|
+
setOwn(target, providerName, {
|
|
708
710
|
...existingMetadata,
|
|
709
711
|
...metadataValue,
|
|
710
|
-
};
|
|
712
|
+
});
|
|
711
713
|
|
|
712
714
|
if (
|
|
713
715
|
'videos' in existingMetadata &&
|
|
@@ -721,7 +723,7 @@ function mergeProviderMetadata(
|
|
|
721
723
|
];
|
|
722
724
|
}
|
|
723
725
|
} else {
|
|
724
|
-
target
|
|
726
|
+
setOwn(target, providerName, metadataValue);
|
|
725
727
|
}
|
|
726
728
|
}
|
|
727
729
|
}
|
|
@@ -213,7 +213,7 @@ class DefaultProviderRegistry<
|
|
|
213
213
|
keyof PROVIDERS,
|
|
214
214
|
ProviderV4 & ProviderWithOptionalVideoModel & EvaluationProvider
|
|
215
215
|
>
|
|
216
|
-
> =
|
|
216
|
+
> = Object.create(null);
|
|
217
217
|
private separator: SEPARATOR;
|
|
218
218
|
private languageModelMiddleware?:
|
|
219
219
|
| LanguageModelMiddleware
|
package/src/ui/chat.ts
CHANGED
|
@@ -263,6 +263,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
263
263
|
private pendingApprovalMessageId: string | undefined;
|
|
264
264
|
private activeResponse: ActiveResponse<UI_MESSAGE> | undefined = undefined;
|
|
265
265
|
private activeResumeRequest: ActiveResumeRequest | undefined = undefined;
|
|
266
|
+
private resumableStreamState:
|
|
267
|
+
| StreamingUIMessageState<UI_MESSAGE>
|
|
268
|
+
| undefined = undefined;
|
|
266
269
|
private jobExecutor = new SerialJobExecutor();
|
|
267
270
|
|
|
268
271
|
constructor({
|
|
@@ -723,6 +726,10 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
723
726
|
trigger: 'submit-message' | 'resume-stream' | 'regenerate-message';
|
|
724
727
|
messageId?: string;
|
|
725
728
|
} & ChatRequestOptions) {
|
|
729
|
+
if (trigger !== 'resume-stream') {
|
|
730
|
+
this.resumableStreamState = undefined;
|
|
731
|
+
}
|
|
732
|
+
|
|
726
733
|
const abortController = new AbortController();
|
|
727
734
|
const activeResumeRequest =
|
|
728
735
|
trigger === 'resume-stream' ? { abortController } : undefined;
|
|
@@ -760,6 +767,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
760
767
|
await reconnect?.cancel().catch(() => {});
|
|
761
768
|
if (isCurrentRequest()) {
|
|
762
769
|
this.setStatus({ status: 'ready' });
|
|
770
|
+
this.resumableStreamState = undefined;
|
|
763
771
|
}
|
|
764
772
|
clearActiveResumeRequest();
|
|
765
773
|
return;
|
|
@@ -767,6 +775,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
767
775
|
|
|
768
776
|
if (reconnect == null) {
|
|
769
777
|
this.setStatus({ status: 'ready' });
|
|
778
|
+
this.resumableStreamState = undefined;
|
|
770
779
|
clearActiveResumeRequest();
|
|
771
780
|
return; // no active stream found, so we do not resume
|
|
772
781
|
}
|
|
@@ -779,6 +788,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
779
788
|
) {
|
|
780
789
|
if (isCurrentRequest()) {
|
|
781
790
|
this.setStatus({ status: 'ready' });
|
|
791
|
+
this.resumableStreamState = undefined;
|
|
782
792
|
}
|
|
783
793
|
clearActiveResumeRequest();
|
|
784
794
|
return;
|
|
@@ -808,6 +818,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
808
818
|
responseMessageIndex === -1
|
|
809
819
|
? lastMessage
|
|
810
820
|
: this.state.messages[responseMessageIndex];
|
|
821
|
+
const resumableResponseMessage =
|
|
822
|
+
trigger === 'resume-stream' &&
|
|
823
|
+
responseMessage?.role === 'assistant' &&
|
|
824
|
+
responseMessage.parts.some(
|
|
825
|
+
part => isToolUIPart(part) && part.state === 'input-streaming',
|
|
826
|
+
)
|
|
827
|
+
? this.state.snapshot(responseMessage)
|
|
828
|
+
: undefined;
|
|
811
829
|
const usesEarlierAssistantMessage =
|
|
812
830
|
responseMessageIndex !== -1 &&
|
|
813
831
|
responseMessageIndex < this.state.messages.length - 1 &&
|
|
@@ -820,13 +838,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
820
838
|
|
|
821
839
|
try {
|
|
822
840
|
const response = {
|
|
823
|
-
state:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
841
|
+
state:
|
|
842
|
+
trigger === 'resume-stream' && this.resumableStreamState != null
|
|
843
|
+
? this.resumableStreamState
|
|
844
|
+
: createStreamingUIMessageState({
|
|
845
|
+
lastMessage:
|
|
846
|
+
trigger === 'resume-stream'
|
|
847
|
+
? resumableResponseMessage
|
|
848
|
+
: trigger === 'regenerate-message'
|
|
849
|
+
? undefined
|
|
850
|
+
: this.state.snapshot(responseMessage),
|
|
851
|
+
messageId: this.generateId(),
|
|
852
|
+
}),
|
|
830
853
|
abortController,
|
|
831
854
|
} as ActiveResponse<UI_MESSAGE>;
|
|
832
855
|
|
|
@@ -916,12 +939,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
916
939
|
if (isAbort) {
|
|
917
940
|
if (isCurrentRequest()) {
|
|
918
941
|
this.setStatus({ status: 'ready' });
|
|
942
|
+
if (this.resumableStreamState === response.state) {
|
|
943
|
+
this.resumableStreamState = undefined;
|
|
944
|
+
}
|
|
919
945
|
}
|
|
920
946
|
return null;
|
|
921
947
|
}
|
|
922
948
|
|
|
923
949
|
if (isCurrentRequest()) {
|
|
924
950
|
this.setStatus({ status: 'ready' });
|
|
951
|
+
if (this.resumableStreamState === response.state) {
|
|
952
|
+
this.resumableStreamState = undefined;
|
|
953
|
+
}
|
|
925
954
|
}
|
|
926
955
|
} catch (err) {
|
|
927
956
|
// Ignore abort errors as they are expected.
|
|
@@ -929,6 +958,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
929
958
|
isAbort = true;
|
|
930
959
|
if (isCurrentRequest()) {
|
|
931
960
|
this.setStatus({ status: 'ready' });
|
|
961
|
+
if (this.resumableStreamState === activeResponse?.state) {
|
|
962
|
+
this.resumableStreamState = undefined;
|
|
963
|
+
}
|
|
932
964
|
}
|
|
933
965
|
return null;
|
|
934
966
|
}
|
|
@@ -948,6 +980,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
948
980
|
isDisconnect = true;
|
|
949
981
|
}
|
|
950
982
|
|
|
983
|
+
if (isDisconnect) {
|
|
984
|
+
this.resumableStreamState = activeResponse?.state;
|
|
985
|
+
} else if (this.resumableStreamState === activeResponse?.state) {
|
|
986
|
+
this.resumableStreamState = undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
951
989
|
if (this.onError && err instanceof Error) {
|
|
952
990
|
this.onError(err);
|
|
953
991
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type Resolvable,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
7
|
import { EmptyResponseBodyError } from '@ai-sdk/provider';
|
|
8
|
+
import { InvalidArgumentError } from '../error/invalid-argument-error';
|
|
8
9
|
import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
|
|
9
10
|
import type { ChatTransport } from './chat-transport';
|
|
10
11
|
import { createUIApiCallError } from './create-ui-api-call-error';
|
|
@@ -247,9 +248,24 @@ export abstract class HttpChatTransport<
|
|
|
247
248
|
requestMetadata: options.metadata,
|
|
248
249
|
});
|
|
249
250
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
let api = preparedRequest?.api;
|
|
252
|
+
if (api == null) {
|
|
253
|
+
// encodeURIComponent leaves dot segments unchanged, and URL parsers
|
|
254
|
+
// normalize them even when their dots are percent-encoded.
|
|
255
|
+
if (options.chatId === '.' || options.chatId === '..') {
|
|
256
|
+
throw new InvalidArgumentError({
|
|
257
|
+
parameter: 'chatId',
|
|
258
|
+
value: options.chatId,
|
|
259
|
+
message:
|
|
260
|
+
'Chat IDs must not be "." or ".." when using the default reconnect URL.',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
api = appendPathToUrl(
|
|
265
|
+
this.api,
|
|
266
|
+
`/${encodeURIComponent(options.chatId)}/stream`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
253
269
|
const headers =
|
|
254
270
|
preparedRequest?.headers !== undefined
|
|
255
271
|
? normalizeHeaders(preparedRequest.headers)
|
|
@@ -2,7 +2,9 @@ import { isToolUIPart, type UIMessage } from './ui-messages';
|
|
|
2
2
|
/**
|
|
3
3
|
* Check if the last message is an assistant message with completed tool calls.
|
|
4
4
|
* The last step of the message must have at least one tool invocation and
|
|
5
|
-
* all tool invocations must have a result.
|
|
5
|
+
* all tool invocations must have a result. Completed model text may follow
|
|
6
|
+
* the tool invocation because client-side tool results update parts in place,
|
|
7
|
+
* but trailing text without a completed stream state prevents resubmission.
|
|
6
8
|
*/
|
|
7
9
|
export function lastAssistantMessageIsCompleteWithToolCalls({
|
|
8
10
|
messages,
|
|
@@ -23,13 +25,23 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
|
|
|
23
25
|
return part.type === 'step-start' ? index : lastIndex;
|
|
24
26
|
}, -1);
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
const lastStepParts = message.parts.slice(lastStepStartIndex + 1);
|
|
29
|
+
|
|
30
|
+
const lastStepToolInvocations = lastStepParts
|
|
28
31
|
.filter(isToolUIPart)
|
|
29
32
|
.filter(part => !part.providerExecuted);
|
|
30
33
|
|
|
34
|
+
const lastToolInvocationIndex = lastStepParts.reduce(
|
|
35
|
+
(lastIndex, part, index) =>
|
|
36
|
+
isToolUIPart(part) && !part.providerExecuted ? index : lastIndex,
|
|
37
|
+
-1,
|
|
38
|
+
);
|
|
39
|
+
|
|
31
40
|
return (
|
|
32
41
|
lastStepToolInvocations.length > 0 &&
|
|
42
|
+
!lastStepParts
|
|
43
|
+
.slice(lastToolInvocationIndex + 1)
|
|
44
|
+
.some(part => part.type === 'text' && part.state !== 'done') &&
|
|
33
45
|
lastStepToolInvocations.every(
|
|
34
46
|
part =>
|
|
35
47
|
(part.state === 'output-available' && part.preliminary !== true) ||
|
|
@@ -60,22 +60,55 @@ export function createStreamingUIMessageState<UI_MESSAGE extends UIMessage>({
|
|
|
60
60
|
lastMessage: UI_MESSAGE | undefined;
|
|
61
61
|
messageId: string;
|
|
62
62
|
}): StreamingUIMessageState<UI_MESSAGE> {
|
|
63
|
+
const message =
|
|
64
|
+
lastMessage?.role === 'assistant'
|
|
65
|
+
? lastMessage
|
|
66
|
+
: ({
|
|
67
|
+
id: messageId,
|
|
68
|
+
metadata: undefined,
|
|
69
|
+
role: 'assistant',
|
|
70
|
+
parts: [] as UIMessagePart<
|
|
71
|
+
InferUIMessageData<UI_MESSAGE>,
|
|
72
|
+
InferUIMessageTools<UI_MESSAGE>
|
|
73
|
+
>[],
|
|
74
|
+
} as UI_MESSAGE);
|
|
75
|
+
const partialToolCalls: StreamingUIMessageState<UI_MESSAGE>['partialToolCalls'] =
|
|
76
|
+
createIdMap();
|
|
77
|
+
const lastStepStartIndex = message.parts.findLastIndex(
|
|
78
|
+
part => part.type === 'step-start',
|
|
79
|
+
);
|
|
80
|
+
let staticToolIndex = 0;
|
|
81
|
+
|
|
82
|
+
for (const part of message.parts.slice(lastStepStartIndex + 1)) {
|
|
83
|
+
if (!isToolUIPart(part)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const index = staticToolIndex;
|
|
88
|
+
if (isStaticToolUIPart(part)) {
|
|
89
|
+
staticToolIndex++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (part.state !== 'input-streaming') {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
partialToolCalls[part.toolCallId] = {
|
|
97
|
+
text: part.rawInput ?? '',
|
|
98
|
+
index,
|
|
99
|
+
toolName:
|
|
100
|
+
part.type === 'dynamic-tool' ? part.toolName : getStaticToolName(part),
|
|
101
|
+
dynamic: part.type === 'dynamic-tool',
|
|
102
|
+
title: part.title,
|
|
103
|
+
toolMetadata: part.toolMetadata,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
63
107
|
return {
|
|
64
|
-
message
|
|
65
|
-
lastMessage?.role === 'assistant'
|
|
66
|
-
? lastMessage
|
|
67
|
-
: ({
|
|
68
|
-
id: messageId,
|
|
69
|
-
metadata: undefined,
|
|
70
|
-
role: 'assistant',
|
|
71
|
-
parts: [] as UIMessagePart<
|
|
72
|
-
InferUIMessageData<UI_MESSAGE>,
|
|
73
|
-
InferUIMessageTools<UI_MESSAGE>
|
|
74
|
-
>[],
|
|
75
|
-
} as UI_MESSAGE),
|
|
108
|
+
message,
|
|
76
109
|
activeTextParts: createIdMap(),
|
|
77
110
|
activeReasoningParts: createIdMap(),
|
|
78
|
-
partialToolCalls
|
|
111
|
+
partialToolCalls,
|
|
79
112
|
};
|
|
80
113
|
}
|
|
81
114
|
|
|
@@ -185,6 +218,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
185
218
|
| {
|
|
186
219
|
state: 'input-streaming';
|
|
187
220
|
input: unknown;
|
|
221
|
+
rawInput?: string;
|
|
188
222
|
providerExecuted?: boolean;
|
|
189
223
|
providerMetadata?: ProviderMetadata;
|
|
190
224
|
}
|
|
@@ -300,6 +334,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
300
334
|
| {
|
|
301
335
|
state: 'input-streaming';
|
|
302
336
|
input: unknown;
|
|
337
|
+
rawInput?: string;
|
|
303
338
|
providerMetadata?: ProviderMetadata;
|
|
304
339
|
}
|
|
305
340
|
| {
|
|
@@ -340,7 +375,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
340
375
|
anyPart.input = anyOptions.input;
|
|
341
376
|
anyPart.output = anyOptions.output;
|
|
342
377
|
anyPart.errorText = anyOptions.errorText;
|
|
343
|
-
anyPart.rawInput = anyOptions.rawInput
|
|
378
|
+
anyPart.rawInput = anyOptions.rawInput;
|
|
344
379
|
anyPart.preliminary = anyOptions.preliminary;
|
|
345
380
|
if (options.title !== undefined) {
|
|
346
381
|
anyPart.title = options.title;
|
|
@@ -645,6 +680,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
645
680
|
toolName: partialToolCall.toolName,
|
|
646
681
|
state: 'input-streaming',
|
|
647
682
|
input: partialArgs,
|
|
683
|
+
rawInput: partialToolCall.text,
|
|
648
684
|
title: partialToolCall.title,
|
|
649
685
|
toolMetadata: partialToolCall.toolMetadata,
|
|
650
686
|
});
|
|
@@ -654,6 +690,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
654
690
|
toolName: partialToolCall.toolName,
|
|
655
691
|
state: 'input-streaming',
|
|
656
692
|
input: partialArgs,
|
|
693
|
+
rawInput: partialToolCall.text,
|
|
657
694
|
title: partialToolCall.title,
|
|
658
695
|
toolMetadata: partialToolCall.toolMetadata,
|
|
659
696
|
});
|
|
@@ -816,7 +853,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
816
853
|
providerExecuted: chunk.providerExecuted,
|
|
817
854
|
providerMetadata: chunk.providerMetadata,
|
|
818
855
|
title: toolInvocation.title,
|
|
819
|
-
toolMetadata:
|
|
856
|
+
toolMetadata:
|
|
857
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
820
858
|
},
|
|
821
859
|
toolInvocation,
|
|
822
860
|
);
|
|
@@ -832,7 +870,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
832
870
|
preliminary: chunk.preliminary,
|
|
833
871
|
providerMetadata: chunk.providerMetadata,
|
|
834
872
|
title: toolInvocation.title,
|
|
835
|
-
toolMetadata:
|
|
873
|
+
toolMetadata:
|
|
874
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
836
875
|
},
|
|
837
876
|
toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
|
|
838
877
|
);
|
|
@@ -856,7 +895,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
856
895
|
providerExecuted: chunk.providerExecuted,
|
|
857
896
|
providerMetadata: chunk.providerMetadata,
|
|
858
897
|
title: toolInvocation.title,
|
|
859
|
-
toolMetadata:
|
|
898
|
+
toolMetadata:
|
|
899
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
860
900
|
},
|
|
861
901
|
toolInvocation,
|
|
862
902
|
);
|
|
@@ -872,7 +912,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
872
912
|
providerExecuted: chunk.providerExecuted,
|
|
873
913
|
providerMetadata: chunk.providerMetadata,
|
|
874
914
|
title: toolInvocation.title,
|
|
875
|
-
toolMetadata:
|
|
915
|
+
toolMetadata:
|
|
916
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
876
917
|
},
|
|
877
918
|
toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
|
|
878
919
|
);
|
package/src/ui/ui-messages.ts
CHANGED
|
@@ -297,6 +297,13 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
|
|
|
297
297
|
| {
|
|
298
298
|
state: 'input-streaming';
|
|
299
299
|
input?: DeepPartial<asUITool<TOOL>['input']> | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* The raw tool input text received so far.
|
|
302
|
+
*
|
|
303
|
+
* This is used to continue input streaming when a message is persisted
|
|
304
|
+
* and later resumed.
|
|
305
|
+
*/
|
|
306
|
+
rawInput?: string;
|
|
300
307
|
output?: never;
|
|
301
308
|
errorText?: never;
|
|
302
309
|
callProviderMetadata?: ProviderMetadata;
|
|
@@ -434,6 +441,13 @@ export type DynamicToolUIPart = {
|
|
|
434
441
|
| {
|
|
435
442
|
state: 'input-streaming';
|
|
436
443
|
input?: unknown;
|
|
444
|
+
/**
|
|
445
|
+
* The raw tool input text received so far.
|
|
446
|
+
*
|
|
447
|
+
* This is used to continue input streaming when a message is persisted
|
|
448
|
+
* and later resumed.
|
|
449
|
+
*/
|
|
450
|
+
rawInput?: string;
|
|
437
451
|
output?: never;
|
|
438
452
|
errorText?: never;
|
|
439
453
|
callProviderMetadata?: ProviderMetadata;
|
|
@@ -154,6 +154,7 @@ const uiMessagesSchema = lazySchema(() => {
|
|
|
154
154
|
toolMetadata: toolMetadataSchema.optional(),
|
|
155
155
|
state: z.literal('input-streaming'),
|
|
156
156
|
input: z.unknown().optional(),
|
|
157
|
+
rawInput: z.string().optional(),
|
|
157
158
|
providerExecuted: z.boolean().optional(),
|
|
158
159
|
callProviderMetadata: providerMetadataSchema.optional(),
|
|
159
160
|
output: z.never().optional(),
|
|
@@ -257,6 +258,7 @@ const uiMessagesSchema = lazySchema(() => {
|
|
|
257
258
|
providerExecuted: z.boolean().optional(),
|
|
258
259
|
callProviderMetadata: providerMetadataSchema.optional(),
|
|
259
260
|
input: z.unknown().optional(),
|
|
261
|
+
rawInput: z.string().optional(),
|
|
260
262
|
output: z.never().optional(),
|
|
261
263
|
errorText: z.never().optional(),
|
|
262
264
|
approval: z.never().optional(),
|
|
@@ -18,6 +18,11 @@ export type ToUIMessageChunkOptions<
|
|
|
18
18
|
sendSources?: boolean;
|
|
19
19
|
sendStart?: boolean;
|
|
20
20
|
sendFinish?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Formats stream errors, invalid tool calls, and application-executed tool
|
|
23
|
+
* errors. Provider-executed tool execution errors bypass this callback so
|
|
24
|
+
* their original error data survives model-message round trips.
|
|
25
|
+
*/
|
|
21
26
|
onError?: (error: unknown) => string;
|
|
22
27
|
messageMetadata?: InferUIMessageMetadata<UI_MESSAGE>;
|
|
23
28
|
responseMessageId?: string;
|
|
@@ -40,7 +45,7 @@ export function toUIMessageChunk<
|
|
|
40
45
|
sendSources = false,
|
|
41
46
|
sendStart = true,
|
|
42
47
|
sendFinish = true,
|
|
43
|
-
onError = () => 'An error occurred.', //
|
|
48
|
+
onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
|
|
44
49
|
messageMetadata,
|
|
45
50
|
responseMessageId,
|
|
46
51
|
}: ToUIMessageChunkOptions<TOOLS, UI_MESSAGE> = {},
|
|
@@ -305,6 +310,9 @@ export function toUIMessageChunk<
|
|
|
305
310
|
case 'tool-error': {
|
|
306
311
|
const dynamic = isDynamic(part);
|
|
307
312
|
|
|
313
|
+
// Preserve provider error codes for model-message round trips and harness
|
|
314
|
+
// runtime error messages. These execution errors intentionally bypass
|
|
315
|
+
// onError; invalid tool calls and stream errors still go through it.
|
|
308
316
|
return {
|
|
309
317
|
type: 'tool-output-error',
|
|
310
318
|
toolCallId: part.toolCallId,
|
|
@@ -26,7 +26,7 @@ export function toUIMessageStream<
|
|
|
26
26
|
sendSources = false,
|
|
27
27
|
sendStart = true,
|
|
28
28
|
sendFinish = true,
|
|
29
|
-
onError = () => 'An error occurred.', //
|
|
29
|
+
onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
|
|
30
30
|
messageMetadata,
|
|
31
31
|
originalMessages,
|
|
32
32
|
generateMessageId,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes an enumerable own property without invoking inherited setters such as
|
|
3
|
+
* `Object.prototype.__proto__`. Keeps the target object's prototype unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export function setOwn<T>(
|
|
6
|
+
object: Record<string, T>,
|
|
7
|
+
key: string,
|
|
8
|
+
value: T,
|
|
9
|
+
): void {
|
|
10
|
+
Object.defineProperty(object, key, {
|
|
11
|
+
value,
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
});
|
|
16
|
+
}
|