ai 7.0.101 → 7.0.103
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 +49 -0
- package/dist/index.d.ts +138 -22
- package/dist/index.js +2373 -316
- package/dist/index.js.map +1 -1
- package/dist/internal/index.js +2 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/test/index.d.ts +16 -2
- package/dist/test/index.js +17 -0
- package/dist/test/index.js.map +1 -1
- package/docs/03-agents/07-workflow-agent.mdx +1 -1
- package/docs/03-ai-sdk-core/18-code-mode.mdx +39 -0
- package/docs/03-ai-sdk-core/32-evaluation.mdx +110 -0
- package/docs/03-ai-sdk-core/36-realtime.mdx +41 -18
- package/docs/03-ai-sdk-core/42-batch.mdx +1 -2
- package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +35 -0
- package/docs/04-ai-sdk-ui/21-transport.mdx +1 -1
- package/docs/06-advanced/11-secure-url-fetching.mdx +8 -2
- package/docs/07-reference/01-ai-sdk-core/14-evaluate.mdx +54 -0
- package/docs/07-reference/01-ai-sdk-core/32-validate-ui-messages.mdx +12 -0
- package/docs/07-reference/01-ai-sdk-core/33-safe-validate-ui-messages.mdx +12 -0
- package/docs/07-reference/02-ai-sdk-ui/05-use-realtime.mdx +364 -48
- package/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx +12 -0
- package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +4 -4
- package/docs/07-reference/05-ai-sdk-errors/ai-evaluation-unsupported-question-type-error.mdx +31 -0
- package/docs/07-reference/05-ai-sdk-errors/index.mdx +131 -36
- package/package.json +12 -12
- package/src/error/index.ts +1 -0
- package/src/evaluate/evaluate.ts +112 -0
- package/src/evaluate/evaluation-result.ts +39 -0
- package/src/evaluate/index.ts +7 -0
- package/src/evaluate/validate-evaluation.ts +298 -0
- package/src/generate-text/execute-tools-from-stream.ts +7 -0
- package/src/generate-text/generate-text.ts +16 -12
- package/src/generate-text/stream-text.ts +9 -2
- package/src/generate-text/tool-caller-configuration.ts +58 -4
- package/src/index.ts +1 -0
- package/src/realtime/__fixtures__/fake-live-websocket.ts +71 -0
- package/src/realtime/__fixtures__/fake-realtime.ts +36 -0
- package/src/realtime/__fixtures__/fake-webrtc.ts +133 -0
- package/src/realtime/browser-realtime-audio.ts +107 -10
- package/src/realtime/browser-realtime-live-websocket.ts +247 -0
- package/src/realtime/browser-realtime-transport.ts +235 -69
- package/src/realtime/browser-realtime-webrtc.ts +582 -0
- package/src/realtime/encode-realtime-frame.ts +33 -0
- package/src/realtime/index.ts +1 -0
- package/src/realtime/realtime-attempt.ts +45 -0
- package/src/realtime/realtime-command-tracker.ts +81 -0
- package/src/realtime/realtime-event-channel.ts +170 -0
- package/src/realtime/realtime-event-reducer.ts +3 -0
- package/src/realtime/realtime-session-state.ts +65 -0
- package/src/realtime/realtime-session.ts +768 -218
- package/src/realtime/realtime-types.ts +1 -1
- package/src/realtime/validate-realtime-setup.ts +37 -0
- package/src/test/evaluation-mock-model-v4.ts +27 -0
- package/src/ui/convert-to-model-messages.ts +3 -0
- package/src/ui/process-ui-message-stream.ts +4 -0
- package/src/ui/ui-messages.ts +5 -1
- package/src/ui/validate-ui-messages.ts +3 -0
- package/src/ui/warn-if-ui-message-has-deprecated-raw-input.ts +36 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { generateId } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type {
|
|
3
|
+
RealtimeClientEvent,
|
|
4
|
+
RealtimeServerEvent,
|
|
5
|
+
} from '../types/realtime-model';
|
|
6
|
+
|
|
7
|
+
/** Attempt-local ACK correlation; unresolved commands are never evicted. */
|
|
8
|
+
export class RealtimeCommandTracker {
|
|
9
|
+
private readonly pending = new Map<string, { muted?: boolean }>();
|
|
10
|
+
private readonly recent = new Set<string>();
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly sendCommand: (event: RealtimeClientEvent) => Promise<void>,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
validateId(id: string): void {
|
|
17
|
+
if (this.pending.has(id) || this.recent.has(id))
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Realtime command ID was already used; retry with a fresh eventId',
|
|
20
|
+
);
|
|
21
|
+
if (this.pending.size >= 512)
|
|
22
|
+
throw new Error('Too many pending realtime commands');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private complete(id: string): { muted?: boolean } | undefined {
|
|
26
|
+
const command = this.pending.get(id);
|
|
27
|
+
if (command == null) return;
|
|
28
|
+
this.pending.delete(id);
|
|
29
|
+
this.recent.add(id);
|
|
30
|
+
if (this.recent.size > 4096) {
|
|
31
|
+
const oldest = this.recent.values().next().value;
|
|
32
|
+
if (oldest != null) this.recent.delete(oldest);
|
|
33
|
+
}
|
|
34
|
+
return command;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
send(event: RealtimeClientEvent): Promise<void> {
|
|
38
|
+
const muted =
|
|
39
|
+
event.type === 'input-audio-mute'
|
|
40
|
+
? true
|
|
41
|
+
: event.type === 'input-audio-unmute'
|
|
42
|
+
? false
|
|
43
|
+
: undefined;
|
|
44
|
+
if (
|
|
45
|
+
event.type === 'input-audio-mute' ||
|
|
46
|
+
event.type === 'input-audio-unmute'
|
|
47
|
+
)
|
|
48
|
+
event = { ...event, eventId: event.eventId ?? generateId() };
|
|
49
|
+
const id = 'eventId' in event ? event.eventId : undefined;
|
|
50
|
+
if (id != null) {
|
|
51
|
+
this.validateId(id);
|
|
52
|
+
this.pending.set(id, { muted });
|
|
53
|
+
}
|
|
54
|
+
const awaitsAcknowledgement =
|
|
55
|
+
event.type === 'context-append' ||
|
|
56
|
+
event.type === 'session-update' ||
|
|
57
|
+
muted != null;
|
|
58
|
+
try {
|
|
59
|
+
const sent = this.sendCommand(event);
|
|
60
|
+
void sent.then(
|
|
61
|
+
() => {
|
|
62
|
+
if (id != null && !awaitsAcknowledgement) this.complete(id);
|
|
63
|
+
},
|
|
64
|
+
() => {
|
|
65
|
+
if (id != null) this.complete(id);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
return sent;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (id != null) this.complete(id);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
receive(event: RealtimeServerEvent): { muted?: boolean } | undefined {
|
|
76
|
+
if (event.type === 'command-acknowledged' && event.clientEventId != null)
|
|
77
|
+
return this.complete(event.clientEventId);
|
|
78
|
+
if (event.type === 'error' && event.clientEventId != null)
|
|
79
|
+
this.complete(event.clientEventId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { safeParseJSON } from '@ai-sdk/provider-utils';
|
|
2
|
+
import type {
|
|
3
|
+
RealtimeClientEvent,
|
|
4
|
+
RealtimeModel,
|
|
5
|
+
RealtimeServerEvent,
|
|
6
|
+
} from '../types/realtime-model';
|
|
7
|
+
|
|
8
|
+
/** One codec queue per connection; retired connections cannot deliver late work. */
|
|
9
|
+
export class RealtimeEventChannel {
|
|
10
|
+
private active = true;
|
|
11
|
+
private finishing = false;
|
|
12
|
+
private readonly parse: (
|
|
13
|
+
raw: unknown,
|
|
14
|
+
) => RealtimeServerEvent | RealtimeServerEvent[];
|
|
15
|
+
private incoming = Promise.resolve();
|
|
16
|
+
private outgoing = Promise.resolve();
|
|
17
|
+
private incomingCount = 0;
|
|
18
|
+
private incomingBytes = 0;
|
|
19
|
+
private outgoingCount = 0;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly options: {
|
|
23
|
+
model: RealtimeModel;
|
|
24
|
+
send: (data: unknown) => void;
|
|
25
|
+
onEvent: (event: RealtimeServerEvent) => void | Promise<void>;
|
|
26
|
+
onError: (error: Error) => void;
|
|
27
|
+
onFatalError?: (error: Error) => void;
|
|
28
|
+
maxPending?: number;
|
|
29
|
+
maxPendingBytes?: number;
|
|
30
|
+
},
|
|
31
|
+
) {
|
|
32
|
+
this.parse =
|
|
33
|
+
options.model.createServerEventParser?.() ??
|
|
34
|
+
options.model.parseServerEvent.bind(options.model);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dispose(): void {
|
|
38
|
+
this.active = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Stop accepting messages, but deliver already received terminal events. */
|
|
42
|
+
async finish(): Promise<void> {
|
|
43
|
+
this.stopWriting();
|
|
44
|
+
await this.incoming;
|
|
45
|
+
this.dispose();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stopWriting(): void {
|
|
49
|
+
this.finishing = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
send(event: RealtimeClientEvent, shouldSend?: () => boolean): Promise<void> {
|
|
53
|
+
if (!this.active || this.finishing)
|
|
54
|
+
throw new Error('Realtime connection is closed');
|
|
55
|
+
if (this.outgoingCount >= (this.options.maxPending ?? 512)) {
|
|
56
|
+
throw new Error('Realtime outgoing queue is full');
|
|
57
|
+
}
|
|
58
|
+
this.outgoingCount++;
|
|
59
|
+
const operation = this.outgoing.then(async () => {
|
|
60
|
+
if (!this.active || this.finishing)
|
|
61
|
+
throw new Error('Realtime connection is closed');
|
|
62
|
+
if (shouldSend?.() === false) return;
|
|
63
|
+
if (!this.active || this.finishing)
|
|
64
|
+
throw new Error('Realtime connection is closed');
|
|
65
|
+
const data = await this.options.model.serializeClientEvent(event);
|
|
66
|
+
if (!this.active || this.finishing)
|
|
67
|
+
throw new Error('Realtime connection is closed');
|
|
68
|
+
if (shouldSend?.() === false) return;
|
|
69
|
+
if (!this.active || this.finishing)
|
|
70
|
+
throw new Error('Realtime connection is closed');
|
|
71
|
+
if (data != null) this.options.send(data);
|
|
72
|
+
});
|
|
73
|
+
this.outgoing = operation
|
|
74
|
+
.catch(error => {
|
|
75
|
+
if (!this.finishing) {
|
|
76
|
+
// Capture owners escalate automatic audio failures; manual sends stay recoverable.
|
|
77
|
+
if (
|
|
78
|
+
event.type !== 'input-audio-append' &&
|
|
79
|
+
event.type !== 'session-close'
|
|
80
|
+
)
|
|
81
|
+
this.report(error);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
this.outgoingCount--;
|
|
86
|
+
});
|
|
87
|
+
return operation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
receive(data: unknown): void {
|
|
91
|
+
if (!this.active || this.finishing) return;
|
|
92
|
+
const bytes =
|
|
93
|
+
typeof data === 'string'
|
|
94
|
+
? data.length * 2
|
|
95
|
+
: data instanceof Blob
|
|
96
|
+
? data.size
|
|
97
|
+
: ((data as ArrayBuffer).byteLength ?? 0);
|
|
98
|
+
if (
|
|
99
|
+
this.incomingCount >= (this.options.maxPending ?? 512) ||
|
|
100
|
+
this.incomingBytes + bytes >
|
|
101
|
+
(this.options.maxPendingBytes ?? 8 * 1024 * 1024)
|
|
102
|
+
) {
|
|
103
|
+
this.finishing = true;
|
|
104
|
+
this.fatal(
|
|
105
|
+
new Error(
|
|
106
|
+
'Realtime incoming queue is full; input protocol continuity was lost',
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.incomingCount++;
|
|
112
|
+
this.incomingBytes += bytes;
|
|
113
|
+
this.incoming = this.incoming
|
|
114
|
+
.then(async () => {
|
|
115
|
+
if (!this.active) return;
|
|
116
|
+
const text =
|
|
117
|
+
typeof data === 'string'
|
|
118
|
+
? data
|
|
119
|
+
: data instanceof Blob
|
|
120
|
+
? await data.text()
|
|
121
|
+
: new TextDecoder().decode(data as ArrayBuffer);
|
|
122
|
+
const parsed = await safeParseJSON({ text });
|
|
123
|
+
if (!this.active) return;
|
|
124
|
+
if (!parsed.success)
|
|
125
|
+
throw new Error('Invalid JSON in realtime server message', {
|
|
126
|
+
cause: parsed.error,
|
|
127
|
+
});
|
|
128
|
+
const health = this.options.model.getHealthCheckResponse?.(
|
|
129
|
+
parsed.value,
|
|
130
|
+
);
|
|
131
|
+
if (!this.active) return;
|
|
132
|
+
if (health != null && !this.finishing) this.options.send(health);
|
|
133
|
+
if (!this.active) return;
|
|
134
|
+
const result = this.parse(parsed.value);
|
|
135
|
+
for (const event of Array.isArray(result) ? result : [result]) {
|
|
136
|
+
if (!this.active) return;
|
|
137
|
+
try {
|
|
138
|
+
await this.options.onEvent(event);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.report(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.catch(error => {
|
|
145
|
+
this.finishing = true;
|
|
146
|
+
this.fatal(error instanceof Error ? error : new Error(String(error)));
|
|
147
|
+
})
|
|
148
|
+
.finally(() => {
|
|
149
|
+
this.incomingCount--;
|
|
150
|
+
this.incomingBytes -= bytes;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fatal(error: Error): void {
|
|
155
|
+
if (!this.active) return;
|
|
156
|
+
if (this.options.onFatalError != null) this.options.onFatalError(error);
|
|
157
|
+
else this.report(error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private report(error: unknown): void {
|
|
161
|
+
if (!this.active) return;
|
|
162
|
+
try {
|
|
163
|
+
this.options.onError(
|
|
164
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
/* Application callbacks cannot break the codec queue. */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { safeParseJSON } from '@ai-sdk/provider-utils';
|
|
2
2
|
import type { RealtimeServerEvent } from '../types/realtime-model';
|
|
3
|
+
import type { RealtimeSessionState } from './realtime-session-state';
|
|
3
4
|
import type {
|
|
4
5
|
DynamicToolUIPart,
|
|
5
6
|
TextUIPart,
|
|
@@ -10,6 +11,7 @@ export type RealtimeStatus =
|
|
|
10
11
|
| 'disconnected'
|
|
11
12
|
| 'connecting'
|
|
12
13
|
| 'connected'
|
|
14
|
+
| 'closing'
|
|
13
15
|
| 'error';
|
|
14
16
|
|
|
15
17
|
export interface RealtimeState {
|
|
@@ -18,6 +20,7 @@ export interface RealtimeState {
|
|
|
18
20
|
events: RealtimeServerEvent[];
|
|
19
21
|
isCapturing: boolean;
|
|
20
22
|
isPlaying: boolean;
|
|
23
|
+
session?: RealtimeSessionState;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export type RealtimeReducerEffect =
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RealtimeServerEvent } from '../types/realtime-model';
|
|
2
|
+
|
|
3
|
+
export type RealtimeSessionState = {
|
|
4
|
+
delegationMode?: 'client' | 'provider';
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
transcripts: Extract<RealtimeServerEvent, { type: 'transcript-fragment' }>[];
|
|
7
|
+
delegations: Extract<RealtimeServerEvent, { type: 'delegation-created' }>[];
|
|
8
|
+
usage?: { seconds: number };
|
|
9
|
+
finalization: 'pending' | 'confirmed' | 'unconfirmed';
|
|
10
|
+
terminationReason?: string;
|
|
11
|
+
isInputMuted: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createSessionState(): RealtimeSessionState {
|
|
15
|
+
return {
|
|
16
|
+
transcripts: [],
|
|
17
|
+
delegations: [],
|
|
18
|
+
isInputMuted: false,
|
|
19
|
+
finalization: 'pending',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reduceSessionState(
|
|
24
|
+
state: RealtimeSessionState,
|
|
25
|
+
event: RealtimeServerEvent,
|
|
26
|
+
limit: number,
|
|
27
|
+
muted?: boolean,
|
|
28
|
+
): RealtimeSessionState {
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case 'session-started':
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
sessionId: event.sessionId,
|
|
34
|
+
delegationMode: event.delegationMode,
|
|
35
|
+
};
|
|
36
|
+
case 'session-usage':
|
|
37
|
+
return { ...state, usage: event.usage };
|
|
38
|
+
case 'session-closed':
|
|
39
|
+
return {
|
|
40
|
+
...state,
|
|
41
|
+
sessionId: event.sessionId ?? state.sessionId,
|
|
42
|
+
usage: event.usage,
|
|
43
|
+
terminationReason: event.reason,
|
|
44
|
+
finalization: 'confirmed',
|
|
45
|
+
};
|
|
46
|
+
case 'transcript-fragment':
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
transcripts: [...state.transcripts, event].slice(-limit),
|
|
50
|
+
};
|
|
51
|
+
case 'delegation-created':
|
|
52
|
+
return state.delegations.some(
|
|
53
|
+
item => item.delegationId === event.delegationId,
|
|
54
|
+
)
|
|
55
|
+
? state
|
|
56
|
+
: {
|
|
57
|
+
...state,
|
|
58
|
+
delegations: [...state.delegations, event].slice(-limit),
|
|
59
|
+
};
|
|
60
|
+
case 'command-acknowledged':
|
|
61
|
+
return muted == null ? state : { ...state, isInputMuted: muted };
|
|
62
|
+
default:
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
}
|