ai 6.0.296 → 6.0.298
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 +25 -0
- package/dist/index.d.mts +36 -5
- package/dist/index.d.ts +36 -5
- package/dist/index.js +187 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +187 -32
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +1 -1
- 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/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
- package/docs/07-reference/02-ai-sdk-ui/41-create-ui-message-stream-response.mdx +7 -0
- package/docs/07-reference/02-ai-sdk-ui/42-pipe-ui-message-stream-to-response.mdx +7 -0
- package/package.json +4 -4
- package/src/agent/create-agent-ui-stream-response.ts +3 -0
- package/src/agent/pipe-agent-ui-stream-to-response.ts +3 -0
- package/src/embed/embed-many.ts +5 -3
- package/src/generate-image/generate-image.ts +7 -2
- package/src/generate-text/stream-text-result.ts +5 -0
- package/src/generate-text/stream-text.ts +4 -1
- 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 +27 -1
- 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/create-sse-stream-with-keep-alive.ts +89 -0
- package/src/ui-message-stream/create-ui-message-stream-response.ts +7 -1
- package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +7 -1
- package/src/ui-message-stream/ui-message-stream-response-init.ts +7 -0
- package/src/util/get-own.ts +18 -0
- package/src/util/set-own.ts +16 -0
package/dist/internal/index.js
CHANGED
|
@@ -164,7 +164,7 @@ function detectMediaType({
|
|
|
164
164
|
var import_provider_utils2 = require("@ai-sdk/provider-utils");
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = true ? "6.0.
|
|
167
|
+
var VERSION = true ? "6.0.298" : "0.0.0-test";
|
|
168
168
|
|
|
169
169
|
// src/util/download/download.ts
|
|
170
170
|
var download = async ({
|
package/dist/internal/index.mjs
CHANGED
|
@@ -728,7 +728,16 @@ With `streamText`, you can control how error messages and usage information are
|
|
|
728
728
|
|
|
729
729
|
By default, the error message is masked for security reasons.
|
|
730
730
|
The default error message is "An error occurred."
|
|
731
|
-
|
|
731
|
+
|
|
732
|
+
Tool execution errors marked `providerExecuted: true` are an exception. They
|
|
733
|
+
bypass the UI stream's `onError` callback: strings are forwarded unchanged, and
|
|
734
|
+
other error values are serialized with `JSON.stringify`. This preserves provider
|
|
735
|
+
error codes when chat history is converted back into model messages. Stream
|
|
736
|
+
errors and invalid tool calls still use `onError`, even for provider-executed
|
|
737
|
+
tools. Do not rely on this callback to redact provider-executed tool execution
|
|
738
|
+
errors before they reach the client.
|
|
739
|
+
|
|
740
|
+
You can forward error messages or send your own error message by providing an `onError` function:
|
|
732
741
|
|
|
733
742
|
```ts filename="app/api/chat/route.ts" highlight="13-27"
|
|
734
743
|
import { convertToModelMessages, streamText, UIMessage } from 'ai';
|
|
@@ -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
|
### Validation with tools
|
|
96
104
|
|
|
97
105
|
When your messages include tool calls, validate them against your tool definitions:
|
|
@@ -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
|
|
@@ -215,6 +219,9 @@ The diagram above shows the complete lifecycle of a resumable stream:
|
|
|
215
219
|
|
|
216
220
|
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`:
|
|
217
221
|
|
|
222
|
+
The callback receives the original, unencoded ID. Its returned `api` URL is used
|
|
223
|
+
verbatim, so encode and validate any ID you include in a custom URL yourself.
|
|
224
|
+
|
|
218
225
|
```tsx filename="app/chat/[chatId]/chat.tsx"
|
|
219
226
|
import { useChat } from '@ai-sdk/react';
|
|
220
227
|
import { DefaultChatTransport } from 'ai';
|
|
@@ -228,10 +235,10 @@ export function Chat({ chatData, resume }) {
|
|
|
228
235
|
// Customize reconnect settings (optional)
|
|
229
236
|
prepareReconnectToStreamRequest: ({ id }) => {
|
|
230
237
|
return {
|
|
231
|
-
api: `/api/chat/${id}/stream`,
|
|
238
|
+
api: `/api/chat/${encodeURIComponent(id)}/stream`,
|
|
232
239
|
// Or use a different pattern:
|
|
233
|
-
// api: `/api/streams/${id}/resume`,
|
|
234
|
-
// api: `/api/resume-chat?id=${id}`,
|
|
240
|
+
// api: `/api/streams/${encodeURIComponent(id)}/resume`,
|
|
241
|
+
// api: `/api/resume-chat?id=${encodeURIComponent(id)}`,
|
|
235
242
|
credentials: 'include', // Include cookies/auth
|
|
236
243
|
headers: {
|
|
237
244
|
Authorization: 'Bearer token',
|
|
@@ -682,7 +682,16 @@ export async function POST(req: Request) {
|
|
|
682
682
|
Language models can make errors when calling tools.
|
|
683
683
|
By default, these errors are masked for security reasons, and show up as "An error occurred" in the UI.
|
|
684
684
|
|
|
685
|
-
|
|
685
|
+
Tool execution errors marked `providerExecuted: true` bypass this masking and the
|
|
686
|
+
UI stream's `onError` callback. String errors pass through unchanged; other error
|
|
687
|
+
values are serialized with `JSON.stringify`. The original error data is needed
|
|
688
|
+
when converting persisted UI messages back into model messages, for example to
|
|
689
|
+
preserve Anthropic tool error codes. The callback still handles invalid tool
|
|
690
|
+
calls and stream errors, regardless of whether a tool is provider-executed.
|
|
691
|
+
|
|
692
|
+
To customize masked errors, pass `onError` to `result.toUIMessageStream()` or
|
|
693
|
+
`result.toUIMessageStreamResponse()`. This callback does not redact
|
|
694
|
+
provider-executed tool execution errors sent to the client.
|
|
686
695
|
|
|
687
696
|
```tsx
|
|
688
697
|
export function errorHandler(error: unknown) {
|
|
@@ -3559,7 +3559,7 @@ To see `streamText` in action, check out [these examples](#examples).
|
|
|
3559
3559
|
type: '(error: unknown) => string',
|
|
3560
3560
|
isOptional: true,
|
|
3561
3561
|
description:
|
|
3562
|
-
'Process an error, e.g. to log it. Returns error message to include in the data stream. Defaults to () => "An error occurred."',
|
|
3562
|
+
'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. Stream errors and invalid tool calls still use this callback.',
|
|
3563
3563
|
},
|
|
3564
3564
|
{
|
|
3565
3565
|
name: 'consumeSseStream',
|
|
@@ -102,6 +102,13 @@ const response = createUIMessageStreamResponse({
|
|
|
102
102
|
isOptional: true,
|
|
103
103
|
description: 'Additional headers for the response.',
|
|
104
104
|
},
|
|
105
|
+
{
|
|
106
|
+
name: 'keepAliveMs',
|
|
107
|
+
type: 'number',
|
|
108
|
+
isOptional: true,
|
|
109
|
+
description:
|
|
110
|
+
'Interval in milliseconds for sending SSE keep-alive comments. When set, an opening comment is sent immediately and additional comments are sent after the stream has been idle for the configured interval.',
|
|
111
|
+
},
|
|
105
112
|
{
|
|
106
113
|
name: 'consumeSseStream',
|
|
107
114
|
type: '(options: { stream: ReadableStream<string> }) => PromiseLike<void> | void',
|
|
@@ -66,6 +66,13 @@ await pipeUIMessageStreamToResponse({
|
|
|
66
66
|
isOptional: true,
|
|
67
67
|
description: 'Additional headers for the response.',
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: 'keepAliveMs',
|
|
71
|
+
type: 'number',
|
|
72
|
+
isOptional: true,
|
|
73
|
+
description:
|
|
74
|
+
'Interval in milliseconds for sending SSE keep-alive comments. When set, an opening comment is sent immediately and additional comments are sent after the stream has been idle for the configured interval.',
|
|
75
|
+
},
|
|
69
76
|
{
|
|
70
77
|
name: 'consumeSseStream',
|
|
71
78
|
type: '({ stream }: { stream: ReadableStream<string> }) => PromiseLike<void> | void',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.298",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
-
"@ai-sdk/gateway": "3.0.
|
|
48
|
+
"@ai-sdk/gateway": "3.0.207",
|
|
49
49
|
"@ai-sdk/provider": "3.0.18",
|
|
50
|
-
"@ai-sdk/provider-utils": "4.0.
|
|
50
|
+
"@ai-sdk/provider-utils": "4.0.57"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"tsx": "^4.19.2",
|
|
59
59
|
"typescript": "5.8.3",
|
|
60
60
|
"zod": "3.25.76",
|
|
61
|
-
"@ai-sdk/google": "3.0.
|
|
61
|
+
"@ai-sdk/google": "3.0.130",
|
|
62
62
|
"@ai-sdk/test-server": "1.0.6",
|
|
63
63
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
64
64
|
},
|
|
@@ -25,6 +25,7 @@ import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settin
|
|
|
25
25
|
* @param headers - Additional headers for the response. Optional.
|
|
26
26
|
* @param status - The status code for the response. Optional.
|
|
27
27
|
* @param statusText - The status text for the response. Optional.
|
|
28
|
+
* @param keepAliveMs - Optional interval for sending SSE keep-alive comments.
|
|
28
29
|
* @param consumeSseStream - Whether to consume the SSE stream. Optional.
|
|
29
30
|
*
|
|
30
31
|
* @returns The response object.
|
|
@@ -38,6 +39,7 @@ export async function createAgentUIStreamResponse<
|
|
|
38
39
|
headers,
|
|
39
40
|
status,
|
|
40
41
|
statusText,
|
|
42
|
+
keepAliveMs,
|
|
41
43
|
consumeSseStream,
|
|
42
44
|
...options
|
|
43
45
|
}: {
|
|
@@ -58,6 +60,7 @@ export async function createAgentUIStreamResponse<
|
|
|
58
60
|
headers,
|
|
59
61
|
status,
|
|
60
62
|
statusText,
|
|
63
|
+
keepAliveMs,
|
|
61
64
|
consumeSseStream,
|
|
62
65
|
stream: await createAgentUIStream(options),
|
|
63
66
|
});
|
|
@@ -27,6 +27,7 @@ import type { ToolLoopAgentOnStepFinishCallback } from './tool-loop-agent-settin
|
|
|
27
27
|
* @param headers - Additional headers for the response. Optional.
|
|
28
28
|
* @param status - The status code for the response. Optional.
|
|
29
29
|
* @param statusText - The status text for the response. Optional.
|
|
30
|
+
* @param keepAliveMs - Optional interval for sending SSE keep-alive comments.
|
|
30
31
|
* @param consumeSseStream - Whether to consume the SSE stream. Optional.
|
|
31
32
|
*/
|
|
32
33
|
export async function pipeAgentUIStreamToResponse<
|
|
@@ -39,6 +40,7 @@ export async function pipeAgentUIStreamToResponse<
|
|
|
39
40
|
headers,
|
|
40
41
|
status,
|
|
41
42
|
statusText,
|
|
43
|
+
keepAliveMs,
|
|
42
44
|
consumeSseStream,
|
|
43
45
|
...options
|
|
44
46
|
}: {
|
|
@@ -61,6 +63,7 @@ export async function pipeAgentUIStreamToResponse<
|
|
|
61
63
|
headers,
|
|
62
64
|
status,
|
|
63
65
|
statusText,
|
|
66
|
+
keepAliveMs,
|
|
64
67
|
consumeSseStream,
|
|
65
68
|
stream: await createAgentUIStream(options),
|
|
66
69
|
});
|
package/src/embed/embed-many.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getOwn } from '../util/get-own';
|
|
2
|
+
import { setOwn } from '../util/set-own';
|
|
1
3
|
import {
|
|
2
4
|
withUserAgentSuffix,
|
|
3
5
|
type ProviderOptions,
|
|
@@ -358,10 +360,10 @@ export async function embedMany({
|
|
|
358
360
|
for (const [providerName, metadata] of Object.entries(
|
|
359
361
|
result.providerMetadata,
|
|
360
362
|
)) {
|
|
361
|
-
providerMetadata
|
|
362
|
-
...providerMetadata
|
|
363
|
+
setOwn(providerMetadata, providerName, {
|
|
364
|
+
...getOwn(providerMetadata, providerName),
|
|
363
365
|
...metadata,
|
|
364
|
-
};
|
|
366
|
+
});
|
|
365
367
|
}
|
|
366
368
|
}
|
|
367
369
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getOwn } from '../util/get-own';
|
|
2
|
+
import { setOwn } from '../util/set-own';
|
|
1
3
|
import type {
|
|
2
4
|
ImageModelV3,
|
|
3
5
|
ImageModelV3CallOptions,
|
|
@@ -284,10 +286,13 @@ export async function generateImage({
|
|
|
284
286
|
.images;
|
|
285
287
|
}
|
|
286
288
|
} else {
|
|
287
|
-
providerMetadata
|
|
288
|
-
|
|
289
|
+
const currentEntry = getOwn(providerMetadata, providerName) ?? {
|
|
290
|
+
images: [],
|
|
291
|
+
};
|
|
292
|
+
currentEntry.images.push(
|
|
289
293
|
...result.providerMetadata[providerName].images,
|
|
290
294
|
);
|
|
295
|
+
setOwn(providerMetadata, providerName, currentEntry);
|
|
291
296
|
}
|
|
292
297
|
}
|
|
293
298
|
}
|
|
@@ -99,6 +99,11 @@ export type UIMessageStreamOptions<UI_MESSAGE extends UIMessage> = {
|
|
|
99
99
|
/**
|
|
100
100
|
* Process an error, e.g. to log it. Default to `() => 'An error occurred.'`.
|
|
101
101
|
*
|
|
102
|
+
* Tool execution errors marked `providerExecuted: true` bypass this callback.
|
|
103
|
+
* Their strings pass through unchanged; other values are JSON-stringified to
|
|
104
|
+
* preserve provider error data for model-message round trips. Stream errors
|
|
105
|
+
* and invalid tool calls still use it.
|
|
106
|
+
*
|
|
102
107
|
* @returns error message to include in the data stream.
|
|
103
108
|
*/
|
|
104
109
|
onError?: (error: unknown) => string;
|
|
@@ -2698,7 +2698,7 @@ class DefaultStreamTextResult<
|
|
|
2698
2698
|
sendSources = false,
|
|
2699
2699
|
sendStart = true,
|
|
2700
2700
|
sendFinish = true,
|
|
2701
|
-
onError = () => 'An error occurred.', //
|
|
2701
|
+
onError = () => 'An error occurred.', // masks errors except provider-executed tool execution errors
|
|
2702
2702
|
}: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream<
|
|
2703
2703
|
InferUIMessageChunk<UI_MESSAGE>
|
|
2704
2704
|
> {
|
|
@@ -3032,6 +3032,9 @@ class DefaultStreamTextResult<
|
|
|
3032
3032
|
case 'tool-error': {
|
|
3033
3033
|
const dynamic = isDynamic(part);
|
|
3034
3034
|
|
|
3035
|
+
// Preserve provider error codes for model-message round trips.
|
|
3036
|
+
// These execution errors intentionally bypass onError; invalid
|
|
3037
|
+
// tool calls and stream errors still go through it.
|
|
3035
3038
|
controller.enqueue({
|
|
3036
3039
|
type: 'tool-output-error',
|
|
3037
3040
|
toolCallId: part.toolCallId,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getOwn } from '../util/get-own';
|
|
2
|
+
import { setOwn } from '../util/set-own';
|
|
1
3
|
import type {
|
|
2
4
|
Experimental_VideoModelV3,
|
|
3
5
|
Experimental_VideoModelV3CallOptions,
|
|
@@ -385,12 +387,12 @@ export async function experimental_generateVideo({
|
|
|
385
387
|
for (const [providerName, metadata] of Object.entries(
|
|
386
388
|
result.providerMetadata,
|
|
387
389
|
)) {
|
|
388
|
-
const existingMetadata = providerMetadata
|
|
390
|
+
const existingMetadata = getOwn(providerMetadata, providerName);
|
|
389
391
|
if (existingMetadata != null && typeof existingMetadata === 'object') {
|
|
390
|
-
providerMetadata
|
|
392
|
+
setOwn(providerMetadata, providerName, {
|
|
391
393
|
...existingMetadata,
|
|
392
394
|
...metadata,
|
|
393
|
-
};
|
|
395
|
+
});
|
|
394
396
|
|
|
395
397
|
// Merge videos arrays if both exist
|
|
396
398
|
if (
|
|
@@ -405,7 +407,7 @@ export async function experimental_generateVideo({
|
|
|
405
407
|
];
|
|
406
408
|
}
|
|
407
409
|
} else {
|
|
408
|
-
providerMetadata
|
|
410
|
+
setOwn(providerMetadata, providerName, metadata);
|
|
409
411
|
}
|
|
410
412
|
}
|
|
411
413
|
}
|
|
@@ -133,7 +133,7 @@ class DefaultProviderRegistry<
|
|
|
133
133
|
PROVIDERS extends Record<string, ProviderV3>,
|
|
134
134
|
SEPARATOR extends string,
|
|
135
135
|
> implements ProviderRegistryProvider<PROVIDERS, SEPARATOR> {
|
|
136
|
-
private providers: PROVIDERS =
|
|
136
|
+
private providers: PROVIDERS = Object.create(null);
|
|
137
137
|
private separator: SEPARATOR;
|
|
138
138
|
private languageModelMiddleware?:
|
|
139
139
|
| LanguageModelMiddleware
|
package/src/ui/chat.ts
CHANGED
|
@@ -792,6 +792,14 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
792
792
|
responseMessageIndex === -1
|
|
793
793
|
? lastMessage
|
|
794
794
|
: this.state.messages[responseMessageIndex];
|
|
795
|
+
const resumableResponseMessage =
|
|
796
|
+
trigger === 'resume-stream' &&
|
|
797
|
+
responseMessage?.role === 'assistant' &&
|
|
798
|
+
responseMessage.parts.some(
|
|
799
|
+
part => isToolUIPart(part) && part.state === 'input-streaming',
|
|
800
|
+
)
|
|
801
|
+
? this.state.snapshot(responseMessage)
|
|
802
|
+
: undefined;
|
|
795
803
|
const usesEarlierAssistantMessage =
|
|
796
804
|
responseMessageIndex !== -1 &&
|
|
797
805
|
responseMessageIndex < this.state.messages.length - 1 &&
|
|
@@ -809,10 +817,11 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
809
817
|
? this.resumableStreamState
|
|
810
818
|
: createStreamingUIMessageState({
|
|
811
819
|
lastMessage:
|
|
812
|
-
trigger === 'resume-stream'
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
820
|
+
trigger === 'resume-stream'
|
|
821
|
+
? resumableResponseMessage
|
|
822
|
+
: trigger === 'regenerate-message'
|
|
823
|
+
? undefined
|
|
824
|
+
: this.state.snapshot(responseMessage),
|
|
816
825
|
messageId: this.generateId(),
|
|
817
826
|
}),
|
|
818
827
|
abortController,
|
|
@@ -4,10 +4,19 @@ import {
|
|
|
4
4
|
type FetchFunction,
|
|
5
5
|
type Resolvable,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
|
+
import { InvalidArgumentError } from '../error/invalid-argument-error';
|
|
7
8
|
import type { UIMessageChunk } from '../ui-message-stream/ui-message-chunks';
|
|
8
9
|
import type { ChatTransport } from './chat-transport';
|
|
9
10
|
import type { UIMessage } from './ui-messages';
|
|
10
11
|
|
|
12
|
+
function appendPathToUrl(url: string, path: string): string {
|
|
13
|
+
const queryOrFragmentStart = url.search(/[?#]/);
|
|
14
|
+
|
|
15
|
+
return queryOrFragmentStart === -1
|
|
16
|
+
? `${url}${path}`
|
|
17
|
+
: `${url.slice(0, queryOrFragmentStart)}${path}${url.slice(queryOrFragmentStart)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export type PrepareSendMessagesRequest<UI_MESSAGE extends UIMessage> = (
|
|
12
21
|
options: {
|
|
13
22
|
id: string;
|
|
@@ -233,7 +242,24 @@ export abstract class HttpChatTransport<
|
|
|
233
242
|
requestMetadata: options.metadata,
|
|
234
243
|
});
|
|
235
244
|
|
|
236
|
-
|
|
245
|
+
let api = preparedRequest?.api;
|
|
246
|
+
if (api == null) {
|
|
247
|
+
// encodeURIComponent leaves dot segments unchanged, and URL parsers
|
|
248
|
+
// normalize them even when their dots are percent-encoded.
|
|
249
|
+
if (options.chatId === '.' || options.chatId === '..') {
|
|
250
|
+
throw new InvalidArgumentError({
|
|
251
|
+
parameter: 'chatId',
|
|
252
|
+
value: options.chatId,
|
|
253
|
+
message:
|
|
254
|
+
'Chat IDs must not be "." or ".." when using the default reconnect URL.',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
api = appendPathToUrl(
|
|
259
|
+
this.api,
|
|
260
|
+
`/${encodeURIComponent(options.chatId)}/stream`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
237
263
|
const headers =
|
|
238
264
|
preparedRequest?.headers !== undefined
|
|
239
265
|
? normalizeHeaders(preparedRequest.headers)
|
|
@@ -3,7 +3,9 @@ import { isToolUIPart, type UIMessage } from './ui-messages';
|
|
|
3
3
|
/**
|
|
4
4
|
* Check if the last message is an assistant message with completed tool calls.
|
|
5
5
|
* The last step of the message must have at least one tool invocation and
|
|
6
|
-
* all tool invocations must have a result.
|
|
6
|
+
* all tool invocations must have a result. Completed model text may follow
|
|
7
|
+
* the tool invocation because client-side tool results update parts in place,
|
|
8
|
+
* but trailing text without a completed stream state prevents resubmission.
|
|
7
9
|
*/
|
|
8
10
|
export function lastAssistantMessageIsCompleteWithToolCalls({
|
|
9
11
|
messages,
|
|
@@ -24,13 +26,23 @@ export function lastAssistantMessageIsCompleteWithToolCalls({
|
|
|
24
26
|
return part.type === 'step-start' ? index : lastIndex;
|
|
25
27
|
}, -1);
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
+
const lastStepParts = message.parts.slice(lastStepStartIndex + 1);
|
|
30
|
+
|
|
31
|
+
const lastStepToolInvocations = lastStepParts
|
|
29
32
|
.filter(isToolUIPart)
|
|
30
33
|
.filter(part => !part.providerExecuted);
|
|
31
34
|
|
|
35
|
+
const lastToolInvocationIndex = lastStepParts.reduce(
|
|
36
|
+
(lastIndex, part, index) =>
|
|
37
|
+
isToolUIPart(part) && !part.providerExecuted ? index : lastIndex,
|
|
38
|
+
-1,
|
|
39
|
+
);
|
|
40
|
+
|
|
32
41
|
return (
|
|
33
42
|
lastStepToolInvocations.length > 0 &&
|
|
43
|
+
!lastStepParts
|
|
44
|
+
.slice(lastToolInvocationIndex + 1)
|
|
45
|
+
.some(part => part.type === 'text' && part.state !== 'done') &&
|
|
34
46
|
lastStepToolInvocations.every(
|
|
35
47
|
part =>
|
|
36
48
|
(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
|
|
|
@@ -167,6 +200,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
167
200
|
| {
|
|
168
201
|
state: 'input-streaming';
|
|
169
202
|
input: unknown;
|
|
203
|
+
rawInput?: string;
|
|
170
204
|
providerExecuted?: boolean;
|
|
171
205
|
providerMetadata?: ProviderMetadata;
|
|
172
206
|
}
|
|
@@ -282,6 +316,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
282
316
|
| {
|
|
283
317
|
state: 'input-streaming';
|
|
284
318
|
input: unknown;
|
|
319
|
+
rawInput?: string;
|
|
285
320
|
providerMetadata?: ProviderMetadata;
|
|
286
321
|
}
|
|
287
322
|
| {
|
|
@@ -322,7 +357,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
322
357
|
anyPart.input = anyOptions.input;
|
|
323
358
|
anyPart.output = anyOptions.output;
|
|
324
359
|
anyPart.errorText = anyOptions.errorText;
|
|
325
|
-
anyPart.rawInput = anyOptions.rawInput
|
|
360
|
+
anyPart.rawInput = anyOptions.rawInput;
|
|
326
361
|
anyPart.preliminary = anyOptions.preliminary;
|
|
327
362
|
if (options.title !== undefined) {
|
|
328
363
|
anyPart.title = options.title;
|
|
@@ -615,6 +650,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
615
650
|
toolName: partialToolCall.toolName,
|
|
616
651
|
state: 'input-streaming',
|
|
617
652
|
input: partialArgs,
|
|
653
|
+
rawInput: partialToolCall.text,
|
|
618
654
|
title: partialToolCall.title,
|
|
619
655
|
toolMetadata: partialToolCall.toolMetadata,
|
|
620
656
|
});
|
|
@@ -624,6 +660,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
624
660
|
toolName: partialToolCall.toolName,
|
|
625
661
|
state: 'input-streaming',
|
|
626
662
|
input: partialArgs,
|
|
663
|
+
rawInput: partialToolCall.text,
|
|
627
664
|
title: partialToolCall.title,
|
|
628
665
|
toolMetadata: partialToolCall.toolMetadata,
|
|
629
666
|
});
|
package/src/ui/ui-messages.ts
CHANGED
|
@@ -238,6 +238,13 @@ export type UIToolInvocation<TOOL extends UITool | Tool> = {
|
|
|
238
238
|
| {
|
|
239
239
|
state: 'input-streaming';
|
|
240
240
|
input?: DeepPartial<asUITool<TOOL>['input']> | undefined;
|
|
241
|
+
/**
|
|
242
|
+
* The raw tool input text received so far.
|
|
243
|
+
*
|
|
244
|
+
* This is used to continue input streaming when a message is persisted
|
|
245
|
+
* and later resumed.
|
|
246
|
+
*/
|
|
247
|
+
rawInput?: string;
|
|
241
248
|
output?: never;
|
|
242
249
|
errorText?: never;
|
|
243
250
|
callProviderMetadata?: ProviderMetadata;
|
|
@@ -361,6 +368,13 @@ export type DynamicToolUIPart = {
|
|
|
361
368
|
| {
|
|
362
369
|
state: 'input-streaming';
|
|
363
370
|
input?: unknown;
|
|
371
|
+
/**
|
|
372
|
+
* The raw tool input text received so far.
|
|
373
|
+
*
|
|
374
|
+
* This is used to continue input streaming when a message is persisted
|
|
375
|
+
* and later resumed.
|
|
376
|
+
*/
|
|
377
|
+
rawInput?: string;
|
|
364
378
|
output?: never;
|
|
365
379
|
errorText?: never;
|
|
366
380
|
callProviderMetadata?: ProviderMetadata;
|
|
@@ -139,6 +139,7 @@ const uiMessagesSchema = lazySchema(() => {
|
|
|
139
139
|
toolMetadata: toolMetadataSchema.optional(),
|
|
140
140
|
state: z.literal('input-streaming'),
|
|
141
141
|
input: z.unknown().optional(),
|
|
142
|
+
rawInput: z.string().optional(),
|
|
142
143
|
providerExecuted: z.boolean().optional(),
|
|
143
144
|
callProviderMetadata: providerMetadataSchema.optional(),
|
|
144
145
|
output: z.never().optional(),
|
|
@@ -235,6 +236,7 @@ const uiMessagesSchema = lazySchema(() => {
|
|
|
235
236
|
providerExecuted: z.boolean().optional(),
|
|
236
237
|
callProviderMetadata: providerMetadataSchema.optional(),
|
|
237
238
|
input: z.unknown().optional(),
|
|
239
|
+
rawInput: z.string().optional(),
|
|
238
240
|
output: z.never().optional(),
|
|
239
241
|
errorText: z.never().optional(),
|
|
240
242
|
approval: z.never().optional(),
|