ai 7.0.100 → 7.0.102

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.d.ts +82 -20
  3. package/dist/index.js +2017 -330
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +12 -5
  6. package/dist/internal/index.js.map +1 -1
  7. package/docs/03-agents/07-workflow-agent.mdx +1 -1
  8. package/docs/03-ai-sdk-core/36-realtime.mdx +41 -18
  9. package/docs/04-ai-sdk-ui/21-transport.mdx +1 -1
  10. package/docs/07-reference/02-ai-sdk-ui/05-use-realtime.mdx +364 -48
  11. package/docs/07-reference/04-ai-sdk-workflow/02-workflow-chat-transport.mdx +4 -4
  12. package/docs/07-reference/05-ai-sdk-errors/index.mdx +131 -36
  13. package/package.json +12 -12
  14. package/src/generate-text/execute-tools-from-stream.ts +7 -0
  15. package/src/generate-text/stream-text.ts +3 -1
  16. package/src/prompt/create-tool-model-output.ts +16 -5
  17. package/src/realtime/__fixtures__/fake-live-websocket.ts +71 -0
  18. package/src/realtime/__fixtures__/fake-realtime.ts +36 -0
  19. package/src/realtime/__fixtures__/fake-webrtc.ts +133 -0
  20. package/src/realtime/browser-realtime-audio.ts +107 -10
  21. package/src/realtime/browser-realtime-live-websocket.ts +247 -0
  22. package/src/realtime/browser-realtime-transport.ts +235 -69
  23. package/src/realtime/browser-realtime-webrtc.ts +582 -0
  24. package/src/realtime/encode-realtime-frame.ts +33 -0
  25. package/src/realtime/index.ts +1 -0
  26. package/src/realtime/realtime-attempt.ts +45 -0
  27. package/src/realtime/realtime-command-tracker.ts +81 -0
  28. package/src/realtime/realtime-event-channel.ts +170 -0
  29. package/src/realtime/realtime-event-reducer.ts +3 -0
  30. package/src/realtime/realtime-session-state.ts +65 -0
  31. package/src/realtime/realtime-session.ts +768 -218
  32. package/src/realtime/realtime-types.ts +1 -1
  33. package/src/realtime/validate-realtime-setup.ts +37 -0
@@ -1,43 +1,138 @@
1
1
  ---
2
2
  title: AI SDK Errors
3
- description: Troubleshooting information for common AI SDK errors.
3
+ description: Reference for AI SDK error classes and typed error handling.
4
4
  collapsed: true
5
5
  ---
6
6
 
7
7
  # AI SDK Errors
8
8
 
9
- - [AI_APICallError](/docs/reference/ai-sdk-errors/ai-api-call-error)
10
- - [AI_DownloadError](/docs/reference/ai-sdk-errors/ai-download-error)
11
- - [AI_EmptyResponseBodyError](/docs/reference/ai-sdk-errors/ai-empty-response-body-error)
12
- - [AI_InvalidArgumentError](/docs/reference/ai-sdk-errors/ai-invalid-argument-error)
13
- - [AI_InvalidDataContentError](/docs/reference/ai-sdk-errors/ai-invalid-data-content-error)
14
- - [AI_InvalidMessageRoleError](/docs/reference/ai-sdk-errors/ai-invalid-message-role-error)
15
- - [AI_InvalidPromptError](/docs/reference/ai-sdk-errors/ai-invalid-prompt-error)
16
- - [AI_InvalidResponseDataError](/docs/reference/ai-sdk-errors/ai-invalid-response-data-error)
17
- - [AI_InvalidToolApprovalError](/docs/reference/ai-sdk-errors/ai-invalid-tool-approval-error)
18
- - [AI_InvalidToolInputError](/docs/reference/ai-sdk-errors/ai-invalid-tool-input-error)
19
- - [AI_JSONParseError](/docs/reference/ai-sdk-errors/ai-json-parse-error)
20
- - [AI_LoadAPIKeyError](/docs/reference/ai-sdk-errors/ai-load-api-key-error)
21
- - [AI_LoadSettingError](/docs/reference/ai-sdk-errors/ai-load-setting-error)
22
- - [AI_MessageConversionError](/docs/reference/ai-sdk-errors/ai-message-conversion-error)
23
- - [AI_NoSpeechGeneratedError](/docs/reference/ai-sdk-errors/ai-no-speech-generated-error)
24
- - [AI_NoContentGeneratedError](/docs/reference/ai-sdk-errors/ai-no-content-generated-error)
25
- - [AI_NoImageGeneratedError](/docs/reference/ai-sdk-errors/ai-no-image-generated-error)
26
- - [AI_NoTranscriptGeneratedError](/docs/reference/ai-sdk-errors/ai-no-transcript-generated-error)
27
- - [AI_NoTranslationGeneratedError](/docs/reference/ai-sdk-errors/ai-no-translation-generated-error)
28
- - [AI_NoVideoGeneratedError](/docs/reference/ai-sdk-errors/ai-no-video-generated-error)
29
- - [AI_NoObjectGeneratedError](/docs/reference/ai-sdk-errors/ai-no-object-generated-error)
30
- - [AI_NoOutputGeneratedError](/docs/reference/ai-sdk-errors/ai-no-output-generated-error)
31
- - [AI_NoSuchModelError](/docs/reference/ai-sdk-errors/ai-no-such-model-error)
32
- - [AI_NoSuchProviderError](/docs/reference/ai-sdk-errors/ai-no-such-provider-error)
33
- - [AI_NoSuchProviderReferenceError](/docs/reference/ai-sdk-errors/ai-no-such-provider-reference-error)
34
- - [AI_NoSuchToolError](/docs/reference/ai-sdk-errors/ai-no-such-tool-error)
35
- - [AI_RetryError](/docs/reference/ai-sdk-errors/ai-retry-error)
36
- - [AI_StreamProviderError](/docs/reference/ai-sdk-errors/ai-stream-provider-error)
37
- - [AI_ToolCallNotFoundForApprovalError](/docs/reference/ai-sdk-errors/ai-tool-call-not-found-for-approval-error)
38
- - [AI_ToolCallRepairError](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error)
39
- - [AI_ToolChoiceViolationError](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error)
40
- - [AI_TooManyEmbeddingValuesForCallError](/docs/reference/ai-sdk-errors/ai-too-many-embedding-values-for-call-error)
41
- - [AI_TypeValidationError](/docs/reference/ai-sdk-errors/ai-type-validation-error)
42
- - [AI_UIMessageStreamError](/docs/reference/ai-sdk-errors/ai-ui-message-stream-error)
43
- - [AI_UnsupportedFunctionalityError](/docs/reference/ai-sdk-errors/ai-unsupported-functionality-error)
9
+ The AI SDK exposes typed errors so applications can handle expected failure
10
+ modes without matching error message strings.
11
+
12
+ ## Importing Errors
13
+
14
+ The application package is named `ai`, not `@ai-sdk/ai`. It re-exports common
15
+ provider-level errors from `@ai-sdk/provider` together with the higher-level
16
+ errors raised by AI SDK Core:
17
+
18
+ ```typescript
19
+ import { APICallError, NoObjectGeneratedError } from 'ai';
20
+ ```
21
+
22
+ Provider implementations that depend directly on `@ai-sdk/provider` can import
23
+ its provider-level errors from that package instead:
24
+
25
+ ```typescript
26
+ import { APICallError, InvalidResponseDataError } from '@ai-sdk/provider';
27
+ ```
28
+
29
+ ## Migrating to Typed Error Handling
30
+
31
+ Replace message matching or an unconditionally generic `catch` block with the
32
+ most specific static `isInstance` guard available. Check `AISDKError` last when
33
+ you need a fallback for any AI SDK error:
34
+
35
+ ```typescript
36
+ import { AISDKError, APICallError, generateText } from 'ai';
37
+
38
+ try {
39
+ await generateText({
40
+ model,
41
+ prompt: 'Write a vegetarian lasagna recipe for 4 people.',
42
+ });
43
+ } catch (error) {
44
+ if (APICallError.isInstance(error)) {
45
+ console.error('Provider request failed:', error.statusCode);
46
+ return;
47
+ }
48
+
49
+ if (AISDKError.isInstance(error)) {
50
+ console.error('AI SDK error:', error.name);
51
+ return;
52
+ }
53
+
54
+ throw error;
55
+ }
56
+ ```
57
+
58
+ Prefer `ErrorClass.isInstance(error)` over `error instanceof ErrorClass` when
59
+ the class provides it. The static guard also works when multiple AI SDK package
60
+ versions are loaded.
61
+
62
+ ## Common Failure Modes
63
+
64
+ | Failure mode | Error to check |
65
+ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | A provider request fails because of a network error or non-success response | [`APICallError`](/docs/reference/ai-sdk-errors/ai-api-call-error) |
67
+ | Automatic retries are exhausted | [`RetryError`](/docs/reference/ai-sdk-errors/ai-retry-error) |
68
+ | A provider reports an error after a response stream has started | [`StreamProviderError`](/docs/reference/ai-sdk-errors/ai-stream-provider-error) |
69
+ | A structured output cannot be parsed or validated | [`NoObjectGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-object-generated-error); inspect its `cause` for [`JSONParseError`](/docs/reference/ai-sdk-errors/ai-json-parse-error) or [`TypeValidationError`](/docs/reference/ai-sdk-errors/ai-type-validation-error) |
70
+ | A generation call returns no usable output | [`NoOutputGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-output-generated-error) or the modality-specific `No*GeneratedError` |
71
+ | A model calls a missing tool or supplies invalid tool input | [`NoSuchToolError`](/docs/reference/ai-sdk-errors/ai-no-such-tool-error) or [`InvalidToolInputError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-input-error) |
72
+ | Tool-call repair fails | [`ToolCallRepairError`](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error) |
73
+ | A response violates an enforced `toolChoice` | [`ToolChoiceViolationError`](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error) |
74
+ | Message history contains unresolved tool calls or invalid approvals | [`MissingToolResultsError`](/docs/troubleshooting/missing-tool-results-error), [`InvalidToolApprovalError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-approval-error), or [`InvalidToolApprovalSignatureError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-approval-signature-error) |
75
+ | A provider or model cannot be resolved | [`NoSuchProviderError`](/docs/reference/ai-sdk-errors/ai-no-such-provider-error), [`NoSuchModelError`](/docs/reference/ai-sdk-errors/ai-no-such-model-error), or [`NoSuchProviderReferenceError`](/docs/reference/ai-sdk-errors/ai-no-such-provider-reference-error) |
76
+ | A provider response is empty, malformed, or invalid | [`EmptyResponseBodyError`](/docs/reference/ai-sdk-errors/ai-empty-response-body-error), [`JSONParseError`](/docs/reference/ai-sdk-errors/ai-json-parse-error), or [`InvalidResponseDataError`](/docs/reference/ai-sdk-errors/ai-invalid-response-data-error) |
77
+ | The requested model or provider does not support a capability or specification version | [`UnsupportedFunctionalityError`](/docs/reference/ai-sdk-errors/ai-unsupported-functionality-error) or [`UnsupportedModelVersionError`](/docs/troubleshooting/unsupported-model-version) |
78
+ | UI messages cannot be converted or a UI message stream is invalid | [`MessageConversionError`](/docs/reference/ai-sdk-errors/ai-message-conversion-error) or [`UIMessageStreamError`](/docs/reference/ai-sdk-errors/ai-ui-message-stream-error) |
79
+
80
+ ## Error Reference
81
+
82
+ ### Base Error
83
+
84
+ - `AISDKError`: Base class and broad type guard for AI SDK errors.
85
+
86
+ ### Provider Requests and Responses
87
+
88
+ - [`APICallError`](/docs/reference/ai-sdk-errors/ai-api-call-error)
89
+ - [`DownloadError`](/docs/reference/ai-sdk-errors/ai-download-error)
90
+ - [`EmptyResponseBodyError`](/docs/reference/ai-sdk-errors/ai-empty-response-body-error)
91
+ - [`InvalidPromptError`](/docs/reference/ai-sdk-errors/ai-invalid-prompt-error)
92
+ - [`InvalidResponseDataError`](/docs/reference/ai-sdk-errors/ai-invalid-response-data-error)
93
+ - [`JSONParseError`](/docs/reference/ai-sdk-errors/ai-json-parse-error)
94
+ - [`LoadAPIKeyError`](/docs/reference/ai-sdk-errors/ai-load-api-key-error)
95
+ - [`LoadSettingError`](/docs/reference/ai-sdk-errors/ai-load-setting-error)
96
+ - [`NoContentGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-content-generated-error)
97
+ - [`NoSuchModelError`](/docs/reference/ai-sdk-errors/ai-no-such-model-error)
98
+ - [`NoSuchProviderReferenceError`](/docs/reference/ai-sdk-errors/ai-no-such-provider-reference-error)
99
+ - [`RetryError`](/docs/reference/ai-sdk-errors/ai-retry-error)
100
+ - [`StreamProviderError`](/docs/reference/ai-sdk-errors/ai-stream-provider-error)
101
+ - [`TooManyEmbeddingValuesForCallError`](/docs/reference/ai-sdk-errors/ai-too-many-embedding-values-for-call-error)
102
+ - [`TypeValidationError`](/docs/reference/ai-sdk-errors/ai-type-validation-error)
103
+ - [`UnsupportedFunctionalityError`](/docs/reference/ai-sdk-errors/ai-unsupported-functionality-error)
104
+
105
+ ### Inputs and Message Streams
106
+
107
+ - [`InvalidArgumentError`](/docs/reference/ai-sdk-errors/ai-invalid-argument-error)
108
+ - [`InvalidDataContentError`](/docs/reference/ai-sdk-errors/ai-invalid-data-content-error)
109
+ - [`InvalidMessageRoleError`](/docs/reference/ai-sdk-errors/ai-invalid-message-role-error)
110
+ - `InvalidStreamPartError`
111
+ - [`MessageConversionError`](/docs/reference/ai-sdk-errors/ai-message-conversion-error)
112
+ - [`UIMessageStreamError`](/docs/reference/ai-sdk-errors/ai-ui-message-stream-error)
113
+
114
+ ### Generated Output
115
+
116
+ - [`NoImageGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-image-generated-error)
117
+ - [`NoObjectGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-object-generated-error)
118
+ - [`NoOutputGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-output-generated-error)
119
+ - [`NoSpeechGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-speech-generated-error)
120
+ - [`NoTranscriptGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-transcript-generated-error)
121
+ - [`NoTranslationGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-translation-generated-error)
122
+ - [`NoVideoGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-video-generated-error)
123
+
124
+ ### Providers and Model Compatibility
125
+
126
+ - [`NoSuchProviderError`](/docs/reference/ai-sdk-errors/ai-no-such-provider-error)
127
+ - [`UnsupportedModelVersionError`](/docs/troubleshooting/unsupported-model-version)
128
+
129
+ ### Tools and Approvals
130
+
131
+ - [`InvalidToolApprovalError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-approval-error)
132
+ - [`InvalidToolApprovalSignatureError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-approval-signature-error)
133
+ - [`InvalidToolInputError`](/docs/reference/ai-sdk-errors/ai-invalid-tool-input-error)
134
+ - [`MissingToolResultsError`](/docs/troubleshooting/missing-tool-results-error)
135
+ - [`NoSuchToolError`](/docs/reference/ai-sdk-errors/ai-no-such-tool-error)
136
+ - [`ToolCallNotFoundForApprovalError`](/docs/reference/ai-sdk-errors/ai-tool-call-not-found-for-approval-error)
137
+ - [`ToolCallRepairError`](/docs/reference/ai-sdk-errors/ai-tool-call-repair-error)
138
+ - [`ToolChoiceViolationError`](/docs/reference/ai-sdk-errors/ai-tool-choice-violation-error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.100",
3
+ "version": "7.0.102",
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.81",
46
- "@ai-sdk/provider": "4.0.14",
47
- "@ai-sdk/provider-utils": "5.0.40"
45
+ "@ai-sdk/gateway": "4.0.82",
46
+ "@ai-sdk/provider": "4.0.15",
47
+ "@ai-sdk/provider-utils": "5.0.41"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.82",
51
- "@ai-sdk/deepseek": "3.0.44",
52
- "@ai-sdk/google": "4.0.69",
53
- "@ai-sdk/groq": "4.0.41",
54
- "@ai-sdk/huggingface": "2.0.48",
55
- "@ai-sdk/moonshotai": "3.0.49",
56
- "@ai-sdk/openai": "4.0.66",
50
+ "@ai-sdk/amazon-bedrock": "5.0.83",
51
+ "@ai-sdk/deepseek": "3.0.45",
52
+ "@ai-sdk/google": "4.0.71",
53
+ "@ai-sdk/groq": "4.0.42",
54
+ "@ai-sdk/huggingface": "2.0.49",
55
+ "@ai-sdk/moonshotai": "3.0.50",
56
+ "@ai-sdk/openai": "4.0.67",
57
57
  "@ai-sdk/test-server": "2.0.1",
58
- "@ai-sdk/xai": "4.0.58",
58
+ "@ai-sdk/xai": "4.0.59",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -25,6 +25,7 @@ import type {
25
25
  OnToolExecutionEndCallback,
26
26
  OnToolExecutionStartCallback,
27
27
  } from './tool-execution-events';
28
+ import type { StaticToolOutputDenied } from './tool-output-denied';
28
29
 
29
30
  export type ToolExecutionEndStreamPart = {
30
31
  type: 'tool-execution-end';
@@ -34,6 +35,7 @@ export type ToolExecutionEndStreamPart = {
34
35
 
35
36
  export type ExecuteToolsStreamPart<TOOLS extends ToolSet = ToolSet> =
36
37
  | LanguageModelStreamPart<TOOLS>
38
+ | StaticToolOutputDenied<TOOLS>
37
39
  | ToolExecutionEndStreamPart
38
40
  | StreamRetryAttemptBoundaryPart;
39
41
 
@@ -180,6 +182,11 @@ export function executeToolsFromStream<
180
182
  reason: toolApprovalStatus.reason,
181
183
  providerExecuted: chunk.providerExecuted,
182
184
  });
185
+ controller.enqueue({
186
+ type: 'tool-output-denied',
187
+ toolCallId: chunk.toolCallId,
188
+ toolName: chunk.toolName,
189
+ } as StaticToolOutputDenied<TOOLS>);
183
190
 
184
191
  return; // don't execute tool
185
192
  }
@@ -196,6 +196,7 @@ const isOutputChunkType = {
196
196
  'tool-call': true,
197
197
  'tool-result': false,
198
198
  'tool-error': false,
199
+ 'tool-output-denied': false,
199
200
  'tool-execution-end': false,
200
201
  'model-call-start': false,
201
202
  'model-call-response-metadata': false,
@@ -2833,7 +2834,8 @@ class DefaultStreamTextResult<
2833
2834
  case 'tool-input-start':
2834
2835
  case 'tool-input-end':
2835
2836
  case 'tool-input-delta':
2836
- case 'tool-approval-request': {
2837
+ case 'tool-approval-request':
2838
+ case 'tool-output-denied': {
2837
2839
  enqueueStepPart(controller, chunk);
2838
2840
  break;
2839
2841
  }
@@ -1,5 +1,9 @@
1
1
  import { getErrorMessage, type JSONValue } from '@ai-sdk/provider';
2
- import type { Tool, ToolResultOutput } from '@ai-sdk/provider-utils';
2
+ import {
3
+ parseJSON,
4
+ type Tool,
5
+ type ToolResultOutput,
6
+ } from '@ai-sdk/provider-utils';
3
7
 
4
8
  export async function createToolModelOutput({
5
9
  toolCallId,
@@ -17,7 +21,7 @@ export async function createToolModelOutput({
17
21
  if (errorMode === 'text') {
18
22
  return { type: 'error-text', value: getErrorMessage(output) };
19
23
  } else if (errorMode === 'json') {
20
- return { type: 'error-json', value: toJSONValue(output) };
24
+ return { type: 'error-json', value: await toJSONValue(output) };
21
25
  }
22
26
 
23
27
  if (tool?.toModelOutput) {
@@ -26,9 +30,16 @@ export async function createToolModelOutput({
26
30
 
27
31
  return typeof output === 'string'
28
32
  ? { type: 'text', value: output }
29
- : { type: 'json', value: toJSONValue(output) };
33
+ : { type: 'json', value: await toJSONValue(output) };
30
34
  }
31
35
 
32
- function toJSONValue(value: unknown): JSONValue {
33
- return value === undefined ? null : (value as JSONValue);
36
+ async function toJSONValue(value: unknown): Promise<JSONValue> {
37
+ if (value === undefined) {
38
+ return null;
39
+ }
40
+
41
+ const serialized = JSON.stringify(value);
42
+ return serialized === undefined
43
+ ? null
44
+ : await parseJSON({ text: serialized });
34
45
  }
@@ -0,0 +1,71 @@
1
+ import { vi, type Mock } from 'vitest';
2
+ import type { RealtimeServerEvent } from '../../types/realtime-model';
3
+ import { fakeStream } from './fake-realtime';
4
+
5
+ export class FakeWebSocket {
6
+ static OPEN = 1;
7
+ static instances: FakeWebSocket[] = [];
8
+ readyState = 0;
9
+ bufferedAmount = 0;
10
+ onopen: (() => void) | null = null;
11
+ onclose: ((event: CloseEvent) => void) | null = null;
12
+ onerror: (() => void) | null = null;
13
+ onmessage: ((event: { data: unknown }) => void) | null = null;
14
+ sent: Array<{ type: string; [key: string]: unknown }> = [];
15
+ constructor(public url: string, public protocols?: string[]) { FakeWebSocket.instances.push(this); }
16
+ send = vi.fn((data: string) => { this.sent.push(JSON.parse(data)); });
17
+ close = vi.fn(() => { this.closeFromServer(); });
18
+ open() { this.readyState = 1; this.onopen?.(); }
19
+ emit(event: RealtimeServerEvent) { this.onmessage?.({ data: JSON.stringify(event) }); }
20
+ closeFromServer({ code = 1000, reason = '', wasClean = true }: Partial<Pick<CloseEvent, 'code' | 'reason' | 'wasClean'>> = {}) {
21
+ this.readyState = 3;
22
+ this.onclose?.({ code, reason, wasClean } as CloseEvent);
23
+ }
24
+ }
25
+
26
+ class FakeAudioNode {
27
+ connect: Mock = vi.fn();
28
+ disconnect: Mock = vi.fn();
29
+ onaudioprocess: ((event: { inputBuffer: { getChannelData: (channel: number) => Float32Array } }) => void) | null = null;
30
+ onended: (() => void) | null = null;
31
+ buffer: unknown;
32
+ start: Mock = vi.fn();
33
+ stop: Mock = vi.fn();
34
+ }
35
+
36
+ export class FakeAudioContext {
37
+ static instances: FakeAudioContext[] = [];
38
+ state = 'suspended';
39
+ currentTime = 0;
40
+ sampleRate: number;
41
+ destination = {};
42
+ processors: FakeAudioNode[] = [];
43
+ sources: FakeAudioNode[] = [];
44
+ mediaSources: FakeAudioNode[] = [];
45
+ constructor(options: { sampleRate: number }) { this.sampleRate = options.sampleRate; FakeAudioContext.instances.push(this); }
46
+ resume = vi.fn(async () => { this.state = 'running'; });
47
+ close = vi.fn(async () => { this.state = 'closed'; });
48
+ createScriptProcessor = vi.fn(() => {
49
+ const node = new FakeAudioNode(); this.processors.push(node); return node;
50
+ });
51
+ createMediaStreamSource = vi.fn(() => {
52
+ const node = new FakeAudioNode(); this.mediaSources.push(node); return node;
53
+ });
54
+ createBufferSource = vi.fn(() => {
55
+ const node = new FakeAudioNode(); this.sources.push(node); return node;
56
+ });
57
+ createBuffer = vi.fn((_channels: number, size: number, rate: number) => ({ duration: size / rate, getChannelData: () => new Float32Array(size) }));
58
+ }
59
+
60
+ export function installLiveWebSocket() {
61
+ const media = fakeStream();
62
+ const getUserMedia = vi.fn(async () => media.stream);
63
+ const fetch = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>();
64
+ vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
65
+ vi.stubGlobal('fetch', fetch);
66
+ FakeWebSocket.instances = [];
67
+ FakeAudioContext.instances = [];
68
+ vi.stubGlobal('WebSocket', FakeWebSocket);
69
+ vi.stubGlobal('AudioContext', FakeAudioContext);
70
+ return { ...media, getUserMedia, fetch };
71
+ }
@@ -0,0 +1,36 @@
1
+ import { vi } from 'vitest';
2
+ import type { RealtimeModel, RealtimeServerEvent } from '../../types/realtime-model';
3
+
4
+ export class FakeTrack extends EventTarget {
5
+ enabled = true;
6
+ muted = false;
7
+ readyState = 'live';
8
+ stop = vi.fn(() => { this.readyState = 'ended'; });
9
+ }
10
+
11
+ export function fakeStream() {
12
+ const track = new FakeTrack();
13
+ return { track, stream: { getTracks: () => [track], getAudioTracks: () => [track] } as unknown as MediaStream };
14
+ }
15
+
16
+ export function liveModel(): RealtimeModel {
17
+ return {
18
+ specificationVersion: 'v4', provider: 'test', modelId: 'live',
19
+ capabilities: { conversation: 'continuous', transports: ['websocket'], connections: ['server-websocket'], startup: 'session-start', finalization: 'session-close' },
20
+ getWebSocketConfig: vi.fn(),
21
+ doCreateClientSecret: vi.fn(),
22
+ buildSessionConfig: vi.fn(),
23
+ parseServerEvent: event => event as RealtimeServerEvent,
24
+ serializeClientEvent: async event => event,
25
+ };
26
+ }
27
+
28
+ export async function flushEvents() {
29
+ for (let i = 0; i < 100; i++) await Promise.resolve();
30
+ }
31
+
32
+ export function deferred<T>() {
33
+ let resolve: (value: T) => void = () => {};
34
+ const promise = new Promise<T>(r => { resolve = r; });
35
+ return { promise, resolve };
36
+ }
@@ -0,0 +1,133 @@
1
+ import { vi, type Mock } from 'vitest';
2
+ import type { RealtimeModel, RealtimeServerEvent } from '../../types/realtime-model';
3
+
4
+ export class FakeTrack extends EventTarget {
5
+ enabled = true;
6
+ muted = false;
7
+ readyState = 'live';
8
+ stop = vi.fn(() => { this.readyState = 'ended'; });
9
+ }
10
+
11
+ export function fakeStream(tracks = [new FakeTrack()]) {
12
+ return { track: tracks[0], stream: { getTracks: () => tracks, getAudioTracks: () => tracks } as unknown as MediaStream };
13
+ }
14
+
15
+ export class FakeDataChannel {
16
+ readyState = 'connecting';
17
+ bufferedAmount = 0;
18
+ onopen: (() => void) | null = null;
19
+ onclose: (() => void) | null = null;
20
+ onerror: (() => void) | null = null;
21
+ onmessage: ((event: { data: unknown }) => void) | null = null;
22
+ sent: Array<{ type: string }> = [];
23
+ send = vi.fn((data: string) => { this.sent.push(JSON.parse(data)); });
24
+ close = vi.fn(() => { this.readyState = 'closed'; this.onclose?.(); });
25
+ emit(event: RealtimeServerEvent) { this.onmessage?.({ data: JSON.stringify(event) }); }
26
+ open() { this.readyState = 'open'; this.onopen?.(); }
27
+ }
28
+
29
+ export class FakePeerConnection {
30
+ static instances: FakePeerConnection[] = [];
31
+ static autoStart = true;
32
+ static autoOpen = true;
33
+ static gather = true;
34
+ dc = new FakeDataChannel();
35
+ iceGatheringState = FakePeerConnection.gather ? 'complete' : 'gathering';
36
+ connectionState = 'new';
37
+ localDescription: { sdp: string } | null = null;
38
+ ontrack: ((event: { streams: MediaStream[] }) => void) | null = null;
39
+ onconnectionstatechange: (() => void) | null = null;
40
+ iceConnectionState = 'new';
41
+ oniceconnectionstatechange: (() => void) | null = null;
42
+ onicegatheringstatechange: (() => void) | null = null;
43
+ constructor() { FakePeerConnection.instances.push(this); }
44
+ createDataChannel = vi.fn(() => this.dc);
45
+ sender = {
46
+ track: null as MediaStreamTrack | null,
47
+ replaceTrack: vi.fn(async (track: MediaStreamTrack | null) => { this.sender.track = track; }),
48
+ };
49
+ addTrack: Mock = vi.fn((track: MediaStreamTrack) => { this.sender.track = track; return this.sender; });
50
+ addTransceiver = vi.fn(() => ({ sender: this.sender }));
51
+ createOffer = vi.fn(async () => {
52
+ if (this.dc.onmessage == null || this.ontrack == null || this.onicegatheringstatechange == null || this.dc.onopen == null) throw new Error('Listeners were installed too late');
53
+ return { type: 'offer', sdp: 'local-offer' };
54
+ });
55
+ setLocalDescription = vi.fn(async (offer: { sdp: string }) => { this.localDescription = offer; });
56
+ setRemoteDescription = vi.fn(async () => {
57
+ if (FakePeerConnection.autoOpen) this.dc.open();
58
+ if (FakePeerConnection.autoStart) this.dc.emit({ type: 'session-started', sessionId: 'session-1', delegationMode: 'client', raw: {} });
59
+ });
60
+ close = vi.fn(() => { this.connectionState = 'closed'; this.onconnectionstatechange?.(); });
61
+ }
62
+
63
+ type FakeAudio = {
64
+ autoplay: boolean;
65
+ srcObject: MediaStream | null;
66
+ onplaying: (() => void) | null;
67
+ onpause: (() => void) | null;
68
+ onwaiting: (() => void) | null;
69
+ onended: (() => void) | null;
70
+ play: Mock<() => Promise<void>>;
71
+ pause: Mock;
72
+ };
73
+
74
+ export function installWebRTC(): {
75
+ track: FakeTrack;
76
+ stream: MediaStream;
77
+ getUserMedia: Mock<() => Promise<MediaStream>>;
78
+ audio: FakeAudio;
79
+ fetch: Mock<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>;
80
+ } {
81
+ FakePeerConnection.instances = [];
82
+ FakePeerConnection.autoOpen = true;
83
+ FakePeerConnection.autoStart = true;
84
+ FakePeerConnection.gather = true;
85
+ const media = fakeStream();
86
+ const getUserMedia = vi.fn(async () => media.stream);
87
+ const audio = {
88
+ autoplay: false,
89
+ srcObject: null as MediaStream | null,
90
+ onplaying: null as (() => void) | null,
91
+ onpause: null as (() => void) | null,
92
+ onwaiting: null as (() => void) | null,
93
+ onended: null as (() => void) | null,
94
+ play: vi.fn(async () => {}),
95
+ pause: vi.fn(),
96
+ };
97
+ vi.stubGlobal('RTCPeerConnection', FakePeerConnection);
98
+ vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
99
+ // Preserve the DOM used by React tests while substituting its audio element.
100
+ if (typeof document === 'undefined') {
101
+ vi.stubGlobal('document', { createElement: vi.fn(() => audio) });
102
+ } else {
103
+ const createElement = document.createElement.bind(document);
104
+ vi.spyOn(document, 'createElement').mockImplementation(((tag: string, options?: ElementCreationOptions) =>
105
+ tag === 'audio' ? audio : createElement(tag, options)) as typeof document.createElement);
106
+ }
107
+ const fetch = vi.fn<(input: RequestInfo | URL, init?: RequestInit) => Promise<Response>>(async () => Response.json({ sessionId: 'session-1', sdp: 'remote-answer' }));
108
+ vi.stubGlobal('fetch', fetch);
109
+ return { ...media, getUserMedia, audio, fetch };
110
+ }
111
+
112
+ export function liveModel(): RealtimeModel & { capabilities: NonNullable<RealtimeModel['capabilities']> } {
113
+ return {
114
+ specificationVersion: 'v4', provider: 'test', modelId: 'live',
115
+ capabilities: { conversation: 'continuous', transports: ['webrtc', 'websocket'], connections: ['webrtc', 'server-websocket'], startup: 'session-start', finalization: 'session-close' },
116
+ getWebRTCConfig: () => ({ dataChannelLabel: 'model-events' }),
117
+ getWebSocketConfig: vi.fn(),
118
+ doCreateClientSecret: vi.fn(),
119
+ buildSessionConfig: vi.fn(),
120
+ parseServerEvent: event => event as RealtimeServerEvent,
121
+ serializeClientEvent: async event => event,
122
+ };
123
+ }
124
+
125
+ export async function flushEvents() {
126
+ for (let i = 0; i < 100; i++) await Promise.resolve();
127
+ }
128
+
129
+ export function deferred<T>() {
130
+ let resolve: (value: T) => void = () => {};
131
+ const promise = new Promise<T>(r => { resolve = r; });
132
+ return { promise, resolve };
133
+ }