ai 7.0.118 → 7.0.120
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 +1 -0
- package/dist/index.js +83 -31
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +3 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/test/index.d.ts +5 -1
- package/dist/test/index.js +4 -0
- package/dist/test/index.js.map +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/35-image-generation.mdx +38 -0
- package/docs/03-ai-sdk-core/36-transcription.mdx +1 -0
- package/docs/03-ai-sdk-core/45-provider-management.mdx +4 -4
- package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +51 -9
- package/docs/04-ai-sdk-ui/02-chatbot.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/61-wrap-image-model.mdx +16 -1
- package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +8 -8
- package/package.json +12 -12
- package/src/middleware/wrap-image-model.ts +13 -0
- package/src/telemetry/tracing-channel-publisher.ts +1 -0
- package/src/test/mock-image-model-v4.ts +8 -0
- package/src/ui/chat.ts +36 -7
- package/src/ui/process-ui-message-stream.ts +9 -10
- package/src/util/write-to-server-response.ts +41 -2
|
@@ -79,7 +79,7 @@ import { customProvider, gateway } from 'ai';
|
|
|
79
79
|
export const anthropic = customProvider({
|
|
80
80
|
languageModels: {
|
|
81
81
|
opus: gateway('anthropic/claude-opus-5.5'),
|
|
82
|
-
sonnet: gateway('anthropic/claude-sonnet-5'),
|
|
82
|
+
sonnet: gateway('anthropic/claude-sonnet-5.5'),
|
|
83
83
|
haiku: gateway('anthropic/claude-haiku-4.5'),
|
|
84
84
|
},
|
|
85
85
|
fallbackProvider: gateway,
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
|
|
101
101
|
export const myProvider = customProvider({
|
|
102
102
|
languageModels: {
|
|
103
|
-
'text-medium': gateway('anthropic/claude-sonnet-5'),
|
|
103
|
+
'text-medium': gateway('anthropic/claude-sonnet-5.5'),
|
|
104
104
|
'text-small': gateway('openai/gpt-6-luna'),
|
|
105
105
|
'reasoning-medium': wrapLanguageModel({
|
|
106
106
|
model: gateway('openai/gpt-6-astra'),
|
|
@@ -154,7 +154,7 @@ const myOpenAI = customProvider({
|
|
|
154
154
|
// custom provider with skills interface:
|
|
155
155
|
const myAnthropic = customProvider({
|
|
156
156
|
languageModels: {
|
|
157
|
-
sonnet: anthropic('claude-sonnet-5'),
|
|
157
|
+
sonnet: anthropic('claude-sonnet-5-5'),
|
|
158
158
|
},
|
|
159
159
|
skills: anthropic.skills(),
|
|
160
160
|
});
|
|
@@ -330,7 +330,7 @@ import { createProviderRegistry, customProvider, uploadSkill } from 'ai';
|
|
|
330
330
|
|
|
331
331
|
const registry = createProviderRegistry({
|
|
332
332
|
anthropic: customProvider({
|
|
333
|
-
languageModels: { sonnet: anthropic('claude-sonnet-5') },
|
|
333
|
+
languageModels: { sonnet: anthropic('claude-sonnet-5-5') },
|
|
334
334
|
skills: anthropic.skills(),
|
|
335
335
|
}),
|
|
336
336
|
});
|
|
@@ -138,6 +138,39 @@ Harness runtimes execute their built-in tools internally. For those tools,
|
|
|
138
138
|
lifecycle after the runtime reports the result. The callbacks are still
|
|
139
139
|
delivered before the tool result is published to the result stream.
|
|
140
140
|
|
|
141
|
+
## Runtime Context
|
|
142
|
+
|
|
143
|
+
Set `runtimeContext` on `HarnessAgent` to make application context available to
|
|
144
|
+
every turn. Use `prepareCall` to replace it for each new prompt turn. The
|
|
145
|
+
prepared context is available through `generate()` and `stream()` and remains
|
|
146
|
+
the same during in-memory continuations with `continueGenerate()` and
|
|
147
|
+
`continueStream()`. Lifecycle callbacks receive the full context, and it is
|
|
148
|
+
also available on each result's `finalStep.runtimeContext`.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const agent = new HarnessAgent({
|
|
152
|
+
harness: claudeCode,
|
|
153
|
+
runtimeContext: {
|
|
154
|
+
conversationId: 'conversation-123',
|
|
155
|
+
userId: 'user-456',
|
|
156
|
+
},
|
|
157
|
+
telemetry: {
|
|
158
|
+
includeRuntimeContext: {
|
|
159
|
+
conversationId: true,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
onEnd: ({ runtimeContext }) => {
|
|
163
|
+
console.log(runtimeContext.userId);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Runtime context is excluded from telemetry by default. Use
|
|
169
|
+
`telemetry.includeRuntimeContext` to allowlist individual top-level properties;
|
|
170
|
+
only properties set to `true` are sent to telemetry integrations. Lifecycle
|
|
171
|
+
callbacks and results still receive the full context, so avoid storing secrets
|
|
172
|
+
that those consumers should not access.
|
|
173
|
+
|
|
141
174
|
## Generate Structured Output
|
|
142
175
|
|
|
143
176
|
Set `output` when constructing `HarnessAgent` to require the same typed output
|
|
@@ -227,7 +260,7 @@ HTTP routes that need multi-turn continuity.
|
|
|
227
260
|
## Change Settings Between Turns
|
|
228
261
|
|
|
229
262
|
Use `callOptionsSchema` and `prepareCall` to derive `model`, `skills`,
|
|
230
|
-
`instructions`, and `
|
|
263
|
+
`instructions`, `tools`, and `runtimeContext` for each new turn. This follows the same
|
|
231
264
|
call-options pattern as `ToolLoopAgent`:
|
|
232
265
|
|
|
233
266
|
```ts
|
|
@@ -253,6 +286,7 @@ const agent = new HarnessAgent({
|
|
|
253
286
|
...call,
|
|
254
287
|
model: options.useCheaperModel ? 'claude-haiku-4-5' : undefined,
|
|
255
288
|
instructions: `Work as the ${options.area} specialist.`,
|
|
289
|
+
runtimeContext: { area: options.area },
|
|
256
290
|
skills: [options.area === 'frontend' ? frontendSkill : backendSkill],
|
|
257
291
|
tools: options.enablePolicyTool ? { getPolicy } : undefined,
|
|
258
292
|
}),
|
|
@@ -390,6 +424,7 @@ const session = await agent.createSession({
|
|
|
390
424
|
sandboxSession,
|
|
391
425
|
// Rebind this when the suspended turn used host-only tool context:
|
|
392
426
|
toolsContext,
|
|
427
|
+
runtimeContext,
|
|
393
428
|
});
|
|
394
429
|
|
|
395
430
|
const result = await agent.continueStream({ session });
|
|
@@ -398,10 +433,12 @@ const result = await agent.continueStream({ session });
|
|
|
398
433
|
Use `continueStream()` for incremental output, or `continueGenerate()` to drain
|
|
399
434
|
the continued turn and return a `GenerateTextResult`.
|
|
400
435
|
|
|
401
|
-
`toolsContext`
|
|
402
|
-
contain credentials or non-serializable
|
|
403
|
-
|
|
404
|
-
`createSession` to rebind
|
|
436
|
+
`toolsContext` and `runtimeContext` are intentionally not stored in
|
|
437
|
+
continuation state because they can contain credentials or non-serializable
|
|
438
|
+
host objects. When a suspended turn used context derived by `prepareCall`,
|
|
439
|
+
pass the same values to `createSession` to rebind them in the process that
|
|
440
|
+
continues the turn. After a process resume without rebinding, the continuation
|
|
441
|
+
uses the agent's constructor-level `runtimeContext` and `toolsContext`.
|
|
405
442
|
|
|
406
443
|
## Stop After a Harness Step
|
|
407
444
|
|
|
@@ -481,9 +518,10 @@ const agent = new HarnessAgent({
|
|
|
481
518
|
```
|
|
482
519
|
|
|
483
520
|
`workDir` is optional. When provided, it must be relative to the sandbox's
|
|
484
|
-
default working directory and is used as the session working directory.
|
|
485
|
-
|
|
486
|
-
|
|
521
|
+
default working directory and is used as the session working directory. Set it
|
|
522
|
+
to `'.'` to use the sandbox's default working directory itself. When omitted,
|
|
523
|
+
regular sessions use the default `<harnessId>-<sessionId>` directory, while
|
|
524
|
+
`onBootstrap` receives the sandbox's default working directory.
|
|
487
525
|
|
|
488
526
|
## Prepare Reusable Sandboxes
|
|
489
527
|
|
|
@@ -555,6 +593,8 @@ supplied, their values must match.
|
|
|
555
593
|
- `stopWhen`: condition(s) for finishing a result slice after a completed
|
|
556
594
|
harness tool step that can continue into another model step.
|
|
557
595
|
- `tools`: AI SDK tools executed by the host when the harness calls them.
|
|
596
|
+
- `runtimeContext`: application context forwarded to lifecycle callbacks and
|
|
597
|
+
results for every turn; `prepareCall` can replace it for a new prompt turn.
|
|
558
598
|
- `activeTools`: allowlist of built-in and host-executed tools the harness can
|
|
559
599
|
call.
|
|
560
600
|
- `inactiveTools`: denylist of built-in and host-executed tools the harness
|
|
@@ -563,7 +603,9 @@ supplied, their values must match.
|
|
|
563
603
|
- `permissionMode`: built-in tool permission mode.
|
|
564
604
|
- `toolApproval`: approval status map for host-executed tools.
|
|
565
605
|
- `sandboxConfig`: sandbox working-directory and lifecycle hook configuration.
|
|
566
|
-
- `telemetry`, `debug`, and `onLog`: observability and diagnostics.
|
|
606
|
+
- `telemetry`, `debug`, and `onLog`: observability and diagnostics. Use
|
|
607
|
+
`telemetry.includeRuntimeContext` to allowlist runtime context properties for
|
|
608
|
+
telemetry.
|
|
567
609
|
|
|
568
610
|
Telemetry reports each turn's resolved model, instructions, and active
|
|
569
611
|
host-defined tools. Skills are adapter context and do not have a corresponding
|
|
@@ -947,7 +947,7 @@ Check out the [stream protocol guide](/docs/ai-sdk-ui/stream-protocol) for more
|
|
|
947
947
|
## Reasoning
|
|
948
948
|
|
|
949
949
|
Some models such as DeepSeek `deepseek-r1`
|
|
950
|
-
and Anthropic `claude-sonnet-5` support reasoning tokens.
|
|
950
|
+
and Anthropic `claude-sonnet-5-5` support reasoning tokens.
|
|
951
951
|
These tokens are typically sent before the message content.
|
|
952
952
|
You can forward them to the client with the `sendReasoning` option:
|
|
953
953
|
|
|
@@ -61,4 +61,19 @@ const { image } = await generateImage({
|
|
|
61
61
|
|
|
62
62
|
### Returns
|
|
63
63
|
|
|
64
|
-
A new `ImageModelV4` instance with middleware applied.
|
|
64
|
+
A new `ImageModelV4` instance with middleware applied. The wrapped model
|
|
65
|
+
preserves the original model's `supportsFileInputs` and `supportsMaskInputs`
|
|
66
|
+
capabilities by default. Middleware can override them with
|
|
67
|
+
`overrideSupportsFileInputs` and `overrideSupportsMaskInputs`. Returning
|
|
68
|
+
`undefined` from an override intentionally marks that capability as unknown.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const model = wrapImageModel({
|
|
72
|
+
model: originalModel,
|
|
73
|
+
middleware: {
|
|
74
|
+
specificationVersion: 'v4',
|
|
75
|
+
overrideSupportsFileInputs: () => true,
|
|
76
|
+
overrideSupportsMaskInputs: () => false,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
@@ -17,7 +17,7 @@ import { tool } from 'ai';
|
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
|
|
19
19
|
const agent = new WorkflowAgent({
|
|
20
|
-
model: 'anthropic/claude-sonnet-5',
|
|
20
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
21
21
|
instructions: 'You are a helpful assistant.',
|
|
22
22
|
tools: {
|
|
23
23
|
weather: tool({
|
|
@@ -71,7 +71,7 @@ To see `WorkflowAgent` in action, check out [these examples](#examples).
|
|
|
71
71
|
type: 'LanguageModel',
|
|
72
72
|
isRequired: true,
|
|
73
73
|
description:
|
|
74
|
-
"The language model to use. A string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-sonnet-5') or a provider instance (e.g., `openai('gpt-6-astra')`).",
|
|
74
|
+
"The language model to use. A string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-sonnet-5.5') or a provider instance (e.g., `openai('gpt-6-astra')`).",
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
name: 'instructions',
|
|
@@ -808,7 +808,7 @@ Infers the UI message type for a `WorkflowAgent` instance. Optionally accepts a
|
|
|
808
808
|
import { WorkflowAgent, InferWorkflowAgentUIMessage } from '@ai-sdk/workflow';
|
|
809
809
|
|
|
810
810
|
const agent = new WorkflowAgent({
|
|
811
|
-
model: 'anthropic/claude-sonnet-5',
|
|
811
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
812
812
|
tools: { weather: weatherTool },
|
|
813
813
|
});
|
|
814
814
|
|
|
@@ -835,7 +835,7 @@ import { tool } from 'ai';
|
|
|
835
835
|
import { z } from 'zod';
|
|
836
836
|
|
|
837
837
|
const agent = new WorkflowAgent({
|
|
838
|
-
model: 'anthropic/claude-sonnet-5',
|
|
838
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
839
839
|
instructions: 'You are a helpful assistant.',
|
|
840
840
|
tools: {
|
|
841
841
|
weather: tool({
|
|
@@ -890,7 +890,7 @@ export async function chat(messages: UIMessage[]) {
|
|
|
890
890
|
const modelMessages = await convertToModelMessages(messages);
|
|
891
891
|
|
|
892
892
|
const agent = new WorkflowAgent({
|
|
893
|
-
model: 'anthropic/claude-sonnet-5',
|
|
893
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
894
894
|
instructions: 'You are a flight booking assistant.',
|
|
895
895
|
tools: {
|
|
896
896
|
searchFlights: tool({
|
|
@@ -937,7 +937,7 @@ import { WorkflowAgent, Output } from '@ai-sdk/workflow';
|
|
|
937
937
|
import { z } from 'zod';
|
|
938
938
|
|
|
939
939
|
const analysisAgent = new WorkflowAgent({
|
|
940
|
-
model: 'anthropic/claude-sonnet-5',
|
|
940
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
941
941
|
});
|
|
942
942
|
|
|
943
943
|
const result = await analysisAgent.stream({
|
|
@@ -977,7 +977,7 @@ import { tool } from 'ai';
|
|
|
977
977
|
import { z } from 'zod';
|
|
978
978
|
|
|
979
979
|
const agent = new WorkflowAgent({
|
|
980
|
-
model: 'anthropic/claude-sonnet-5',
|
|
980
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
981
981
|
experimental_toolApprovalSecret: {
|
|
982
982
|
environmentVariable: 'TOOL_APPROVAL_SECRET',
|
|
983
983
|
},
|
|
@@ -1009,7 +1009,7 @@ serialize it. A stream-level reference overrides the constructor value.
|
|
|
1009
1009
|
import { WorkflowAgent } from '@ai-sdk/workflow';
|
|
1010
1010
|
|
|
1011
1011
|
const agent = new WorkflowAgent({
|
|
1012
|
-
model: 'anthropic/claude-sonnet-5',
|
|
1012
|
+
model: 'anthropic/claude-sonnet-5.5',
|
|
1013
1013
|
tools: { weather: weatherTool },
|
|
1014
1014
|
|
|
1015
1015
|
// Agent-wide callbacks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.120",
|
|
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.
|
|
46
|
-
"@ai-sdk/provider": "4.0.
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.98",
|
|
46
|
+
"@ai-sdk/provider": "4.0.19",
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.50"
|
|
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.99",
|
|
51
|
+
"@ai-sdk/deepseek": "3.0.55",
|
|
52
|
+
"@ai-sdk/google": "4.0.84",
|
|
53
|
+
"@ai-sdk/groq": "4.0.51",
|
|
54
|
+
"@ai-sdk/huggingface": "2.0.58",
|
|
55
|
+
"@ai-sdk/moonshotai": "3.0.59",
|
|
56
|
+
"@ai-sdk/openai": "4.0.79",
|
|
57
57
|
"@ai-sdk/test-server": "2.0.2",
|
|
58
|
-
"@ai-sdk/xai": "5.0.
|
|
58
|
+
"@ai-sdk/xai": "5.0.11",
|
|
59
59
|
"@edge-runtime/vm": "^5.0.0",
|
|
60
60
|
"@smithy/eventstream-codec": "^4.3.3",
|
|
61
61
|
"@smithy/util-utf8": "^4.3.3",
|
|
@@ -48,6 +48,8 @@ const doWrap = ({
|
|
|
48
48
|
overrideProvider,
|
|
49
49
|
overrideModelId,
|
|
50
50
|
overrideMaxImagesPerCall,
|
|
51
|
+
overrideSupportsFileInputs,
|
|
52
|
+
overrideSupportsMaskInputs,
|
|
51
53
|
},
|
|
52
54
|
modelId,
|
|
53
55
|
providerId,
|
|
@@ -71,11 +73,22 @@ const doWrap = ({
|
|
|
71
73
|
? maxImagesPerCallRaw.bind(model)
|
|
72
74
|
: maxImagesPerCallRaw;
|
|
73
75
|
|
|
76
|
+
const supportsFileInputs =
|
|
77
|
+
overrideSupportsFileInputs !== undefined
|
|
78
|
+
? overrideSupportsFileInputs({ model })
|
|
79
|
+
: model.supportsFileInputs;
|
|
80
|
+
const supportsMaskInputs =
|
|
81
|
+
overrideSupportsMaskInputs !== undefined
|
|
82
|
+
? overrideSupportsMaskInputs({ model })
|
|
83
|
+
: model.supportsMaskInputs;
|
|
84
|
+
|
|
74
85
|
return {
|
|
75
86
|
specificationVersion: 'v4',
|
|
76
87
|
provider: providerId ?? overrideProvider?.({ model }) ?? model.provider,
|
|
77
88
|
modelId: modelId ?? overrideModelId?.({ model }) ?? model.modelId,
|
|
78
89
|
maxImagesPerCall,
|
|
90
|
+
supportsFileInputs,
|
|
91
|
+
supportsMaskInputs,
|
|
79
92
|
async doGenerate(
|
|
80
93
|
params: ImageModelV4CallOptions,
|
|
81
94
|
): Promise<ImageModelV4Result> {
|
|
@@ -6,6 +6,8 @@ export class MockImageModelV4 implements ImageModelV4 {
|
|
|
6
6
|
readonly provider: ImageModelV4['provider'];
|
|
7
7
|
readonly modelId: ImageModelV4['modelId'];
|
|
8
8
|
readonly maxImagesPerCall: ImageModelV4['maxImagesPerCall'];
|
|
9
|
+
readonly supportsFileInputs: ImageModelV4['supportsFileInputs'];
|
|
10
|
+
readonly supportsMaskInputs: ImageModelV4['supportsMaskInputs'];
|
|
9
11
|
|
|
10
12
|
doGenerate: ImageModelV4['doGenerate'];
|
|
11
13
|
|
|
@@ -13,16 +15,22 @@ export class MockImageModelV4 implements ImageModelV4 {
|
|
|
13
15
|
provider = 'mock-provider',
|
|
14
16
|
modelId = 'mock-model-id',
|
|
15
17
|
maxImagesPerCall = 1,
|
|
18
|
+
supportsFileInputs,
|
|
19
|
+
supportsMaskInputs,
|
|
16
20
|
doGenerate = notImplemented,
|
|
17
21
|
}: {
|
|
18
22
|
provider?: ImageModelV4['provider'];
|
|
19
23
|
modelId?: ImageModelV4['modelId'];
|
|
20
24
|
maxImagesPerCall?: ImageModelV4['maxImagesPerCall'];
|
|
25
|
+
supportsFileInputs?: ImageModelV4['supportsFileInputs'];
|
|
26
|
+
supportsMaskInputs?: ImageModelV4['supportsMaskInputs'];
|
|
21
27
|
doGenerate?: ImageModelV4['doGenerate'];
|
|
22
28
|
} = {}) {
|
|
23
29
|
this.provider = provider;
|
|
24
30
|
this.modelId = modelId;
|
|
25
31
|
this.maxImagesPerCall = maxImagesPerCall;
|
|
32
|
+
this.supportsFileInputs = supportsFileInputs;
|
|
33
|
+
this.supportsMaskInputs = supportsMaskInputs;
|
|
26
34
|
this.doGenerate = doGenerate;
|
|
27
35
|
}
|
|
28
36
|
}
|
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;
|
|
@@ -820,13 +830,17 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
820
830
|
|
|
821
831
|
try {
|
|
822
832
|
const response = {
|
|
823
|
-
state:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
833
|
+
state:
|
|
834
|
+
trigger === 'resume-stream' && this.resumableStreamState != null
|
|
835
|
+
? this.resumableStreamState
|
|
836
|
+
: createStreamingUIMessageState({
|
|
837
|
+
lastMessage:
|
|
838
|
+
trigger === 'resume-stream' ||
|
|
839
|
+
trigger === 'regenerate-message'
|
|
840
|
+
? undefined
|
|
841
|
+
: this.state.snapshot(responseMessage),
|
|
842
|
+
messageId: this.generateId(),
|
|
843
|
+
}),
|
|
830
844
|
abortController,
|
|
831
845
|
} as ActiveResponse<UI_MESSAGE>;
|
|
832
846
|
|
|
@@ -916,12 +930,18 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
916
930
|
if (isAbort) {
|
|
917
931
|
if (isCurrentRequest()) {
|
|
918
932
|
this.setStatus({ status: 'ready' });
|
|
933
|
+
if (this.resumableStreamState === response.state) {
|
|
934
|
+
this.resumableStreamState = undefined;
|
|
935
|
+
}
|
|
919
936
|
}
|
|
920
937
|
return null;
|
|
921
938
|
}
|
|
922
939
|
|
|
923
940
|
if (isCurrentRequest()) {
|
|
924
941
|
this.setStatus({ status: 'ready' });
|
|
942
|
+
if (this.resumableStreamState === response.state) {
|
|
943
|
+
this.resumableStreamState = undefined;
|
|
944
|
+
}
|
|
925
945
|
}
|
|
926
946
|
} catch (err) {
|
|
927
947
|
// Ignore abort errors as they are expected.
|
|
@@ -929,6 +949,9 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
929
949
|
isAbort = true;
|
|
930
950
|
if (isCurrentRequest()) {
|
|
931
951
|
this.setStatus({ status: 'ready' });
|
|
952
|
+
if (this.resumableStreamState === activeResponse?.state) {
|
|
953
|
+
this.resumableStreamState = undefined;
|
|
954
|
+
}
|
|
932
955
|
}
|
|
933
956
|
return null;
|
|
934
957
|
}
|
|
@@ -948,6 +971,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
|
|
|
948
971
|
isDisconnect = true;
|
|
949
972
|
}
|
|
950
973
|
|
|
974
|
+
if (isDisconnect) {
|
|
975
|
+
this.resumableStreamState = activeResponse?.state;
|
|
976
|
+
} else if (this.resumableStreamState === activeResponse?.state) {
|
|
977
|
+
this.resumableStreamState = undefined;
|
|
978
|
+
}
|
|
979
|
+
|
|
951
980
|
if (this.onError && err instanceof Error) {
|
|
952
981
|
this.onError(err);
|
|
953
982
|
}
|
|
@@ -31,8 +31,6 @@ import {
|
|
|
31
31
|
type UIMessage,
|
|
32
32
|
type UIMessagePart,
|
|
33
33
|
} from './ui-messages';
|
|
34
|
-
import { warnIfUIMessageHasDeprecatedRawInput } from './warn-if-ui-message-has-deprecated-raw-input';
|
|
35
|
-
|
|
36
34
|
export type StreamingUIMessageState<UI_MESSAGE extends UIMessage> = {
|
|
37
35
|
message: UI_MESSAGE;
|
|
38
36
|
activeTextParts: Record<string, TextUIPart>;
|
|
@@ -732,15 +730,12 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
732
730
|
toolCallId: chunk.toolCallId,
|
|
733
731
|
toolName: chunk.toolName,
|
|
734
732
|
state: 'output-error',
|
|
735
|
-
input:
|
|
736
|
-
rawInput: chunk.input,
|
|
733
|
+
input: chunk.input,
|
|
737
734
|
errorText: chunk.errorText,
|
|
738
735
|
providerExecuted: chunk.providerExecuted,
|
|
739
736
|
providerMetadata: chunk.providerMetadata,
|
|
740
737
|
toolMetadata: chunk.toolMetadata,
|
|
741
738
|
});
|
|
742
|
-
|
|
743
|
-
warnIfUIMessageHasDeprecatedRawInput([state.message]);
|
|
744
739
|
}
|
|
745
740
|
|
|
746
741
|
write();
|
|
@@ -821,7 +816,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
821
816
|
providerExecuted: chunk.providerExecuted,
|
|
822
817
|
providerMetadata: chunk.providerMetadata,
|
|
823
818
|
title: toolInvocation.title,
|
|
824
|
-
toolMetadata:
|
|
819
|
+
toolMetadata:
|
|
820
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
825
821
|
},
|
|
826
822
|
toolInvocation,
|
|
827
823
|
);
|
|
@@ -837,7 +833,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
837
833
|
preliminary: chunk.preliminary,
|
|
838
834
|
providerMetadata: chunk.providerMetadata,
|
|
839
835
|
title: toolInvocation.title,
|
|
840
|
-
toolMetadata:
|
|
836
|
+
toolMetadata:
|
|
837
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
841
838
|
},
|
|
842
839
|
toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
|
|
843
840
|
);
|
|
@@ -861,7 +858,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
861
858
|
providerExecuted: chunk.providerExecuted,
|
|
862
859
|
providerMetadata: chunk.providerMetadata,
|
|
863
860
|
title: toolInvocation.title,
|
|
864
|
-
toolMetadata:
|
|
861
|
+
toolMetadata:
|
|
862
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
865
863
|
},
|
|
866
864
|
toolInvocation,
|
|
867
865
|
);
|
|
@@ -877,7 +875,8 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
877
875
|
providerExecuted: chunk.providerExecuted,
|
|
878
876
|
providerMetadata: chunk.providerMetadata,
|
|
879
877
|
title: toolInvocation.title,
|
|
880
|
-
toolMetadata:
|
|
878
|
+
toolMetadata:
|
|
879
|
+
chunk.toolMetadata ?? toolInvocation.toolMetadata,
|
|
881
880
|
},
|
|
882
881
|
toolInvocation as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>,
|
|
883
882
|
);
|
|
@@ -6,6 +6,10 @@ type FlushableServerResponse = ServerResponse & {
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Writes the content of a stream to a server response.
|
|
9
|
+
*
|
|
10
|
+
* When the client disconnects before the stream has been fully written
|
|
11
|
+
* (premature `close`), the stream is cancelled so that upstream resources
|
|
12
|
+
* can be released, and no further chunks are written.
|
|
9
13
|
*/
|
|
10
14
|
export function writeToServerResponse({
|
|
11
15
|
response,
|
|
@@ -32,11 +36,36 @@ export function writeToServerResponse({
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const reader = stream.getReader();
|
|
39
|
+
|
|
40
|
+
// Detect client disconnects. `close` also fires after a regular `end()`;
|
|
41
|
+
// `writableFinished` distinguishes the two cases.
|
|
42
|
+
let clientDisconnected = false;
|
|
43
|
+
let onDisconnect: (() => void) | undefined;
|
|
44
|
+
const handleClose = () => {
|
|
45
|
+
if (response.writableFinished) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clientDisconnected = true;
|
|
50
|
+
onDisconnect?.();
|
|
51
|
+
|
|
52
|
+
// cancelling the reader resolves a pending read with `done: true`:
|
|
53
|
+
reader.cancel(new Error('Client disconnected.')).catch(() => {});
|
|
54
|
+
};
|
|
55
|
+
response.once('close', handleClose);
|
|
56
|
+
|
|
57
|
+
// The response may have been destroyed before the close listener was
|
|
58
|
+
// registered. Cancel immediately instead of waiting indefinitely for the
|
|
59
|
+
// next stream chunk.
|
|
60
|
+
if (response.destroyed) {
|
|
61
|
+
handleClose();
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
const read = async () => {
|
|
36
65
|
try {
|
|
37
66
|
while (true) {
|
|
38
67
|
const { done, value } = await reader.read();
|
|
39
|
-
if (done) break;
|
|
68
|
+
if (done || clientDisconnected || response.destroyed) break;
|
|
40
69
|
|
|
41
70
|
// Respect backpressure: if write() returns false, wait for 'drain' event
|
|
42
71
|
const canContinue = response.write(value);
|
|
@@ -47,12 +76,22 @@ export function writeToServerResponse({
|
|
|
47
76
|
|
|
48
77
|
if (!canContinue) {
|
|
49
78
|
await new Promise<void>(resolve => {
|
|
79
|
+
// don't wait for `drain` on a disconnected response:
|
|
80
|
+
onDisconnect = resolve;
|
|
50
81
|
response.once('drain', resolve);
|
|
51
82
|
});
|
|
83
|
+
onDisconnect = undefined;
|
|
52
84
|
}
|
|
53
85
|
}
|
|
54
86
|
} finally {
|
|
55
|
-
response.
|
|
87
|
+
response.off('close', handleClose);
|
|
88
|
+
|
|
89
|
+
if (clientDisconnected || response.destroyed) {
|
|
90
|
+
// release the stream; ending a destroyed response is not possible:
|
|
91
|
+
reader.cancel().catch(() => {});
|
|
92
|
+
} else {
|
|
93
|
+
response.end();
|
|
94
|
+
}
|
|
56
95
|
}
|
|
57
96
|
};
|
|
58
97
|
|