ai 7.0.120 → 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 +23 -0
- package/dist/index.d.ts +29 -3
- package/dist/index.js +106 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +1 -1
- package/docs/04-ai-sdk-ui/02-chatbot.mdx +9 -0
- 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/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 +13 -4
- 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 +51 -14
- 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/dist/internal/index.js
CHANGED
|
@@ -758,6 +758,15 @@ With `streamText`, you can control how error messages and usage information are
|
|
|
758
758
|
|
|
759
759
|
By default, the error message is masked for security reasons.
|
|
760
760
|
The default error message is "An error occurred."
|
|
761
|
+
|
|
762
|
+
Tool execution errors marked `providerExecuted: true` are an exception. They
|
|
763
|
+
bypass the UI stream's `onError` callback: strings are forwarded unchanged, and
|
|
764
|
+
other error values are serialized with `JSON.stringify`. This preserves provider
|
|
765
|
+
error codes when chat history is converted back into model messages, as well as
|
|
766
|
+
error messages from harness runtimes. Stream errors and invalid tool calls still
|
|
767
|
+
use `onError`, even for provider-executed tools. Do not rely on this callback to
|
|
768
|
+
redact provider-executed tool execution errors before they reach the client.
|
|
769
|
+
|
|
761
770
|
You can forward error messages or send your own error message by providing an `onError` function:
|
|
762
771
|
|
|
763
772
|
```ts filename="app/api/chat/route.ts" highlight="21-35"
|
|
@@ -92,6 +92,14 @@ export async function loadChat(id: string): Promise<UIMessage[]> {
|
|
|
92
92
|
|
|
93
93
|
When processing messages on the server that contain tool calls, custom metadata, or data parts, you should validate them using `validateUIMessages` before sending them to the model.
|
|
94
94
|
|
|
95
|
+
### Persisting streaming tool input
|
|
96
|
+
|
|
97
|
+
An `input-streaming` tool part includes `rawInput` while tool arguments are
|
|
98
|
+
still arriving. Persist the complete UI message, including this field, so a
|
|
99
|
+
resumed stream can append later tool-input deltas to the exact text received
|
|
100
|
+
before the interruption. AI SDK updates `rawInput` as each delta arrives and
|
|
101
|
+
removes it when the tool input becomes available.
|
|
102
|
+
|
|
95
103
|
### Migrating deprecated tool `rawInput`
|
|
96
104
|
|
|
97
105
|
Older persisted tool parts in the `output-error` state may contain `rawInput`.
|
|
@@ -90,6 +90,10 @@ export function Chat({
|
|
|
90
90
|
|
|
91
91
|
When you enable `resume`, the `useChat` hook makes a `GET` request to `/api/chat/[id]/stream` on mount to check for and resume any active streams.
|
|
92
92
|
|
|
93
|
+
The default transport URL-encodes the chat ID as a single path segment. IDs equal
|
|
94
|
+
to `.` or `..` are rejected with an `InvalidArgumentError` because URL parsers
|
|
95
|
+
interpret them as path traversal segments.
|
|
96
|
+
|
|
93
97
|
Let's start by creating the POST handler to create the resumable stream.
|
|
94
98
|
|
|
95
99
|
### 2. Create the POST handler
|
|
@@ -220,6 +224,9 @@ The diagram above shows the complete lifecycle of a resumable stream:
|
|
|
220
224
|
|
|
221
225
|
By default, the `useChat` hook makes a GET request to `/api/chat/[id]/stream` when resuming. Customize this endpoint, credentials, and headers, using the `prepareReconnectToStreamRequest` option in `DefaultChatTransport`:
|
|
222
226
|
|
|
227
|
+
The callback receives the original, unencoded ID. Its returned `api` URL is used
|
|
228
|
+
verbatim, so encode and validate any ID you include in a custom URL yourself.
|
|
229
|
+
|
|
223
230
|
```tsx filename="app/chat/[chatId]/chat.tsx"
|
|
224
231
|
import { useChat } from '@ai-sdk/react';
|
|
225
232
|
import { DefaultChatTransport } from 'ai';
|
|
@@ -233,10 +240,10 @@ export function Chat({ chatData, resume }) {
|
|
|
233
240
|
// Customize reconnect settings (optional)
|
|
234
241
|
prepareReconnectToStreamRequest: ({ id }) => {
|
|
235
242
|
return {
|
|
236
|
-
api: `/api/chat/${id}/stream`,
|
|
243
|
+
api: `/api/chat/${encodeURIComponent(id)}/stream`,
|
|
237
244
|
// Or use a different pattern:
|
|
238
|
-
// api: `/api/streams/${id}/resume`,
|
|
239
|
-
// api: `/api/resume-chat?id=${id}`,
|
|
245
|
+
// api: `/api/streams/${encodeURIComponent(id)}/resume`,
|
|
246
|
+
// api: `/api/resume-chat?id=${encodeURIComponent(id)}`,
|
|
240
247
|
credentials: 'include', // Include cookies/auth
|
|
241
248
|
headers: {
|
|
242
249
|
Authorization: 'Bearer token',
|
|
@@ -780,7 +780,16 @@ export async function POST(req: Request) {
|
|
|
780
780
|
Language models can make errors when calling tools.
|
|
781
781
|
By default, these errors are masked for security reasons, and show up as "An error occurred" in the UI.
|
|
782
782
|
|
|
783
|
-
|
|
783
|
+
Tool execution errors marked `providerExecuted: true` bypass this masking and the
|
|
784
|
+
UI stream's `onError` callback. String errors pass through unchanged; other error
|
|
785
|
+
values are serialized with `JSON.stringify`. The original error data is needed
|
|
786
|
+
when converting persisted UI messages back into model messages, for example to
|
|
787
|
+
preserve Anthropic tool error codes. Harness runtime tool errors also retain
|
|
788
|
+
their original messages. The callback still handles invalid tool calls and
|
|
789
|
+
stream errors, regardless of whether a tool is provider-executed.
|
|
790
|
+
|
|
791
|
+
To customize masked errors, pass `onError` to `toUIMessageStream`. This callback
|
|
792
|
+
does not redact provider-executed tool execution errors sent to the client.
|
|
784
793
|
|
|
785
794
|
```tsx
|
|
786
795
|
export function errorHandler(error: unknown) {
|
|
@@ -219,7 +219,7 @@ This page pulls in the current UI State using the `useUIState` hook, which is th
|
|
|
219
219
|
|
|
220
220
|
In this example, to call the next tool, the user must respond with plain text. **Given you are streaming a React component, you can add a button to trigger the next step in the conversation**.
|
|
221
221
|
|
|
222
|
-
To add user interaction, you will have to convert the component into a client component and use the `
|
|
222
|
+
To add user interaction, you will have to convert the component into a client component and use the `useActions` hook to trigger the next step in the conversation.
|
|
223
223
|
|
|
224
224
|
```tsx filename="components/flights.tsx"
|
|
225
225
|
'use client';
|
|
@@ -4133,7 +4133,7 @@ To see `streamText` in action, check out [these examples](#examples).
|
|
|
4133
4133
|
type: '(error: unknown) => string',
|
|
4134
4134
|
isOptional: true,
|
|
4135
4135
|
description:
|
|
4136
|
-
'Process an error, e.g. to log it. Returns error message to include in the data stream. Defaults to () => "An error occurred."',
|
|
4136
|
+
'Process an error, e.g. to log it. Returns the error message to include in the data stream. Defaults to () => "An error occurred." Tool execution errors marked providerExecuted: true bypass this callback: strings pass through unchanged and other values are JSON-stringified to preserve provider error data for model-message round trips and harness runtime error messages. Stream errors and invalid tool calls still use this callback.',
|
|
4137
4137
|
},
|
|
4138
4138
|
{
|
|
4139
4139
|
name: 'consumeSseStream',
|
|
@@ -27,8 +27,8 @@ description: Learn how to upgrade AI SDK 3.4 to 4.0.
|
|
|
27
27
|
You need to update the following packages to the following versions in your `package.json` file(s):
|
|
28
28
|
|
|
29
29
|
- `ai` package: `4.0.*`
|
|
30
|
-
-
|
|
31
|
-
-
|
|
30
|
+
- `@ai-sdk/provider-utils` package: `2.0.*`
|
|
31
|
+
- `@ai-sdk/*` packages: `1.0.*` (other `@ai-sdk/*` packages)
|
|
32
32
|
|
|
33
33
|
## Codemods
|
|
34
34
|
|
|
@@ -518,7 +518,7 @@ import { Provider } from 'ai';
|
|
|
518
518
|
providers](/docs/reference/ai-sdk-core/provider-registry#setup) instead.
|
|
519
519
|
</Note>
|
|
520
520
|
|
|
521
|
-
#### Removed `
|
|
521
|
+
#### Removed `experimental_createModelRegistry` function
|
|
522
522
|
|
|
523
523
|
The `experimental_createModelRegistry` function has been removed.
|
|
524
524
|
Please use the `experimental_createProviderRegistry` function instead.
|
|
@@ -621,7 +621,7 @@ const { text, response } = await generateText({
|
|
|
621
621
|
const responseMessages = response.messages;
|
|
622
622
|
```
|
|
623
623
|
|
|
624
|
-
### Removed `
|
|
624
|
+
### Removed `experimental_continuationSteps` option
|
|
625
625
|
|
|
626
626
|
The `experimental_continuationSteps` option has been removed from the `generateText` function.
|
|
627
627
|
Please use the `experimental_continueSteps` option instead.
|
|
@@ -965,7 +965,7 @@ return AssistantResponse(
|
|
|
965
965
|
);
|
|
966
966
|
```
|
|
967
967
|
|
|
968
|
-
#### Removed `
|
|
968
|
+
#### Removed `experimental_AssistantResponse` export
|
|
969
969
|
|
|
970
970
|
<Note type="warning">
|
|
971
971
|
There is no codemod available for this change. Please review and update your
|
|
@@ -18,7 +18,7 @@ introducing significant improvements and new features across the AI SDK and its
|
|
|
18
18
|
|
|
19
19
|
## New Features
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### OpenTelemetry Support
|
|
22
22
|
|
|
23
23
|
- Added experimental [OpenTelemetry support](/docs/ai-sdk-core/telemetry) for all [AI SDK Core functions](/docs/ai-sdk-core/overview#ai-sdk-core-functions), enabling better observability and tracing capabilities.
|
|
24
24
|
|
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
|
@@ -818,6 +818,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
818
818
|
responseMessageIndex === -1
|
|
819
819
|
? lastMessage
|
|
820
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;
|
|
821
829
|
const usesEarlierAssistantMessage =
|
|
822
830
|
responseMessageIndex !== -1 &&
|
|
823
831
|
responseMessageIndex < this.state.messages.length - 1 &&
|
|
@@ -835,10 +843,11 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
835
843
|
? this.resumableStreamState
|
|
836
844
|
: createStreamingUIMessageState({
|
|
837
845
|
lastMessage:
|
|
838
|
-
trigger === 'resume-stream'
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
846
|
+
trigger === 'resume-stream'
|
|
847
|
+
? resumableResponseMessage
|
|
848
|
+
: trigger === 'regenerate-message'
|
|
849
|
+
? undefined
|
|
850
|
+
: this.state.snapshot(responseMessage),
|
|
842
851
|
messageId: this.generateId(),
|
|
843
852
|
}),
|
|
844
853
|
abortController,
|
|
@@ -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
|
});
|
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(),
|