@ai-sdk/gateway 4.0.17 → 4.0.20

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ai-sdk/gateway",
3
3
  "private": false,
4
- "version": "4.0.17",
4
+ "version": "4.0.20",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "sideEffects": false,
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@vercel/oidc": "3.2.0",
34
34
  "@ai-sdk/provider": "4.0.3",
35
- "@ai-sdk/provider-utils": "5.0.7"
35
+ "@ai-sdk/provider-utils": "5.0.10"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "22.19.19",
@@ -1,7 +1,12 @@
1
- import type { FetchFunction, Resolvable } from '@ai-sdk/provider-utils';
1
+ import type {
2
+ FetchFunction,
3
+ Resolvable,
4
+ WebSocketConstructor,
5
+ } from '@ai-sdk/provider-utils';
2
6
 
3
7
  export type GatewayConfig = {
4
8
  baseURL: string;
5
9
  headers?: Resolvable<Record<string, string | undefined>>;
6
10
  fetch?: FetchFunction;
11
+ webSocket?: WebSocketConstructor;
7
12
  };
@@ -34,6 +34,7 @@ export class GatewayFetchMetadata {
34
34
  try {
35
35
  const { value } = await getFromApi({
36
36
  url: `${this.config.baseURL}/config`,
37
+ validateUrl: false,
37
38
  headers: this.config.headers
38
39
  ? await resolve(this.config.headers)
39
40
  : undefined,
@@ -59,6 +60,7 @@ export class GatewayFetchMetadata {
59
60
 
60
61
  const { value } = await getFromApi({
61
62
  url: `${baseUrl.origin}/v1/credits`,
63
+ validateUrl: false,
62
64
  headers: this.config.headers
63
65
  ? await resolve(this.config.headers)
64
66
  : undefined,
@@ -65,6 +65,7 @@ export class GatewayGenerationInfoFetcher {
65
65
 
66
66
  const { value } = await getFromApi({
67
67
  url: `${baseUrl.origin}/v1/generation?id=${encodeURIComponent(params.id)}`,
68
+ validateUrl: false,
68
69
  headers: this.config.headers
69
70
  ? await resolve(this.config.headers)
70
71
  : undefined,
@@ -28,7 +28,6 @@ export type GatewayModelId =
28
28
  | 'amazon/nova-micro'
29
29
  | 'amazon/nova-pro'
30
30
  | 'anthropic/claude-3-haiku'
31
- | 'anthropic/claude-3.5-haiku'
32
31
  | 'anthropic/claude-fable-5'
33
32
  | 'anthropic/claude-haiku-4.5'
34
33
  | 'anthropic/claude-opus-4'
@@ -41,7 +40,6 @@ export type GatewayModelId =
41
40
  | 'anthropic/claude-sonnet-4.5'
42
41
  | 'anthropic/claude-sonnet-4.6'
43
42
  | 'anthropic/claude-sonnet-5'
44
- | 'arcee-ai/trinity-large-preview'
45
43
  | 'arcee-ai/trinity-large-thinking'
46
44
  | 'arcee-ai/trinity-mini'
47
45
  | 'bytedance/seed-1.6'
@@ -79,8 +77,6 @@ export type GatewayModelId =
79
77
  | 'kwaipilot/kat-coder-pro-v1'
80
78
  | 'kwaipilot/kat-coder-pro-v2'
81
79
  | 'kwaipilot/kat-coder-pro-v2.5'
82
- | 'meituan/longcat-flash-chat'
83
- | 'meituan/longcat-flash-thinking-2601'
84
80
  | 'meta/llama-3.1-70b'
85
81
  | 'meta/llama-3.1-8b'
86
82
  | 'meta/llama-3.2-11b'
@@ -101,7 +97,6 @@ export type GatewayModelId =
101
97
  | 'minimax/minimax-m3'
102
98
  | 'mistral/codestral'
103
99
  | 'mistral/devstral-2'
104
- | 'mistral/devstral-small'
105
100
  | 'mistral/devstral-small-2'
106
101
  | 'mistral/magistral-medium'
107
102
  | 'mistral/magistral-small'
@@ -114,7 +109,6 @@ export type GatewayModelId =
114
109
  | 'mistral/mistral-nemo'
115
110
  | 'mistral/mistral-small'
116
111
  | 'mistral/pixtral-12b'
117
- | 'mistral/pixtral-large'
118
112
  | 'moonshotai/kimi-k2'
119
113
  | 'moonshotai/kimi-k2-thinking'
120
114
  | 'moonshotai/kimi-k2.5'
@@ -129,7 +123,6 @@ export type GatewayModelId =
129
123
  | 'nvidia/nemotron-nano-12b-v2-vl'
130
124
  | 'nvidia/nemotron-nano-9b-v2'
131
125
  | 'openai/gpt-3.5-turbo'
132
- | 'openai/gpt-3.5-turbo-instruct'
133
126
  | 'openai/gpt-4-turbo'
134
127
  | 'openai/gpt-4.1'
135
128
  | 'openai/gpt-4.1-mini'
@@ -189,8 +182,6 @@ export type GatewayModelId =
189
182
  | 'xai/grok-4.3'
190
183
  | 'xai/grok-4.5'
191
184
  | 'xai/grok-build-0.1'
192
- | 'xiaomi/mimo-v2-flash'
193
- | 'xiaomi/mimo-v2-pro'
194
185
  | 'xiaomi/mimo-v2.5'
195
186
  | 'xiaomi/mimo-v2.5-pro'
196
187
  | 'zai/glm-4.5'
@@ -6,6 +6,7 @@ import {
6
6
  withoutTrailingSlash,
7
7
  withUserAgentSuffix,
8
8
  type FetchFunction,
9
+ type WebSocketConstructor,
9
10
  } from '@ai-sdk/provider-utils';
10
11
  import { z } from 'zod/v4';
11
12
  import { asGatewayError, GatewayAuthenticationError } from './errors';
@@ -208,6 +209,14 @@ export interface GatewayProviderSettings {
208
209
  */
209
210
  fetch?: FetchFunction;
210
211
 
212
+ /**
213
+ * Custom WebSocket implementation used for streaming transcription. This is
214
+ * useful for testing or for runtimes without a global WebSocket. A
215
+ * header-capable implementation is not required — Gateway WebSocket auth is
216
+ * carried in the subprotocols.
217
+ */
218
+ webSocket?: WebSocketConstructor;
219
+
211
220
  /**
212
221
  * How frequently to refresh the metadata cache in milliseconds.
213
222
  */
@@ -516,6 +525,7 @@ export function createGateway(
516
525
  headers: getHeaders,
517
526
  fetch: options.fetch,
518
527
  o11yHeaders: createO11yHeaders(),
528
+ webSocket: options.webSocket,
519
529
  });
520
530
  };
521
531
  provider.transcriptionModel = createTranscriptionModel;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Shared WebSocket subprotocol contract for AI Gateway realtime auth.
2
+ * Shared WebSocket subprotocol contract for AI Gateway realtime and streaming
3
+ * transcription auth.
3
4
  *
4
5
  * The browser `WebSocket` API cannot set request headers, so the Gateway auth
5
6
  * (bearer) token is carried through the `Sec-WebSocket-Protocol` handshake
@@ -8,8 +9,9 @@
8
9
  *
9
10
  * This module is the single source of truth for that contract so the client and
10
11
  * the Gateway server can't drift: the client encodes values with
11
- * `getGatewayRealtimeProtocols`, and the Gateway server decodes them with
12
- * `getGatewayRealtimeAuthToken` / `getGatewayRealtimeTeamIdOrSlug`.
12
+ * `getGatewayRealtimeProtocols` / `getGatewayTranscriptionProtocols`, and the
13
+ * Gateway server decodes them with `getGatewayRealtimeAuthToken` /
14
+ * `getGatewayRealtimeTeamIdOrSlug`.
13
15
  *
14
16
  * WebSocket subprotocol values must fit the RFC token grammar. The auth token is
15
17
  * sent as-is, so callers must use tokens that are valid subprotocol tokens; the
@@ -19,12 +21,18 @@
19
21
  */
20
22
 
21
23
  /**
22
- * Marker subprotocol offered on every handshake so the Gateway can echo a
23
- * negotiated subprotocol on the 101 response (some clients require the server to
24
- * select one of the offered subprotocols).
24
+ * Marker subprotocol offered on every realtime handshake so the Gateway can
25
+ * echo a negotiated subprotocol on the 101 response (some clients require the
26
+ * server to select one of the offered subprotocols).
25
27
  */
26
28
  export const GATEWAY_REALTIME_SUBPROTOCOL = 'ai-gateway-realtime.v1';
27
29
 
30
+ /**
31
+ * Marker subprotocol for streaming transcription handshakes (same negotiation
32
+ * purpose as `GATEWAY_REALTIME_SUBPROTOCOL`).
33
+ */
34
+ export const GATEWAY_TRANSCRIPTION_SUBPROTOCOL = 'ai-gateway-transcription.v1';
35
+
28
36
  /** Subprotocol prefix that carries the Gateway auth (bearer) token. */
29
37
  export const GATEWAY_AUTH_SUBPROTOCOL_PREFIX = 'ai-gateway-auth.';
30
38
 
@@ -39,10 +47,30 @@ export function getGatewayRealtimeProtocols(
39
47
  token: string,
40
48
  options?: { teamIdOrSlug?: string },
41
49
  ): string[] {
42
- const protocols = [
43
- GATEWAY_REALTIME_SUBPROTOCOL,
44
- `${GATEWAY_AUTH_SUBPROTOCOL_PREFIX}${token}`,
45
- ];
50
+ return buildGatewayProtocols(GATEWAY_REALTIME_SUBPROTOCOL, token, options);
51
+ }
52
+
53
+ /**
54
+ * Client-side: `getGatewayRealtimeProtocols`, but with the streaming
55
+ * transcription marker subprotocol.
56
+ */
57
+ export function getGatewayTranscriptionProtocols(
58
+ token: string,
59
+ options?: { teamIdOrSlug?: string },
60
+ ): string[] {
61
+ return buildGatewayProtocols(
62
+ GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
63
+ token,
64
+ options,
65
+ );
66
+ }
67
+
68
+ function buildGatewayProtocols(
69
+ marker: string,
70
+ token: string,
71
+ options?: { teamIdOrSlug?: string },
72
+ ): string[] {
73
+ const protocols = [marker, `${GATEWAY_AUTH_SUBPROTOCOL_PREFIX}${token}`];
46
74
 
47
75
  if (options?.teamIdOrSlug) {
48
76
  protocols.push(
@@ -106,6 +106,7 @@ export class GatewaySpendReport {
106
106
 
107
107
  const { value } = await getFromApi({
108
108
  url: `${baseUrl.origin}/v1/report?${searchParams.toString()}`,
109
+ validateUrl: false,
109
110
  headers: this.config.headers
110
111
  ? await resolve(this.config.headers)
111
112
  : undefined,
@@ -1,21 +1,41 @@
1
- import type {
2
- SharedV4ProviderMetadata,
3
- SharedV4Warning,
4
- TranscriptionModelV4,
1
+ import {
2
+ getErrorMessage,
3
+ type Experimental_TranscriptionModelV4StreamOptions as TranscriptionModelV4StreamOptions,
4
+ type Experimental_TranscriptionModelV4StreamPart as TranscriptionModelV4StreamPart,
5
+ type Experimental_TranscriptionModelV4StreamResult as TranscriptionModelV4StreamResult,
6
+ type SharedV4ProviderMetadata,
7
+ type SharedV4Warning,
8
+ type TranscriptionModelV4,
5
9
  } from '@ai-sdk/provider';
6
10
  import {
7
11
  combineHeaders,
12
+ convertBase64ToUint8Array,
8
13
  convertUint8ArrayToBase64,
9
14
  createJsonErrorResponseHandler,
10
15
  createJsonResponseHandler,
16
+ connectToWebSocket,
17
+ normalizeHeaders,
18
+ experimental_parseTranscriptionStreamPart as parseTranscriptionStreamPart,
11
19
  postJsonToApi,
12
20
  resolve,
21
+ EXPERIMENTAL_TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE as TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE,
22
+ waitForWebSocketBufferDrain,
23
+ EXPERIMENTAL_TRANSCRIPTION_STREAM_START_FRAME_TYPE as TRANSCRIPTION_STREAM_START_FRAME_TYPE,
24
+ type Experimental_TranscriptionStreamStartFrame,
13
25
  type Resolvable,
26
+ type WebSocketConnection,
27
+ type WebSocketConstructor,
28
+ type WebSocketLike,
14
29
  } from '@ai-sdk/provider-utils';
15
30
  import { z } from 'zod/v4';
16
- import { asGatewayError } from './errors';
31
+ import { asGatewayError, createGatewayErrorFromResponse } from './errors';
17
32
  import { parseAuthMethod } from './errors/parse-auth-method';
18
33
  import type { GatewayConfig } from './gateway-config';
34
+ import { VERCEL_AI_GATEWAY_TEAM_HEADER } from './gateway-headers';
35
+ import {
36
+ GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
37
+ getGatewayTranscriptionProtocols,
38
+ } from './gateway-realtime-auth';
19
39
 
20
40
  export class GatewayTranscriptionModel implements TranscriptionModelV4 {
21
41
  readonly specificationVersion = 'v4' as const;
@@ -25,6 +45,9 @@ export class GatewayTranscriptionModel implements TranscriptionModelV4 {
25
45
  private readonly config: GatewayConfig & {
26
46
  provider: string;
27
47
  o11yHeaders: Resolvable<Record<string, string>>;
48
+ _internal?: {
49
+ currentDate?: () => Date;
50
+ };
28
51
  },
29
52
  ) {}
30
53
 
@@ -99,6 +122,48 @@ export class GatewayTranscriptionModel implements TranscriptionModelV4 {
99
122
  }
100
123
  }
101
124
 
125
+ async doStream(
126
+ options: TranscriptionModelV4StreamOptions,
127
+ ): Promise<TranscriptionModelV4StreamResult> {
128
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
129
+
130
+ const headers = combineHeaders(
131
+ await resolve(this.config.headers ?? {}),
132
+ options.headers ?? {},
133
+ this.getModelConfigHeaders(),
134
+ await resolve(this.config.o11yHeaders),
135
+ );
136
+ const authMethod = await parseAuthMethod(headers);
137
+
138
+ // First frame after the WebSocket opens, per the transcription-stream
139
+ // envelope. Optional keys are omitted so the frame stays minimal.
140
+ const startFrame: Experimental_TranscriptionStreamStartFrame = {
141
+ type: TRANSCRIPTION_STREAM_START_FRAME_TYPE,
142
+ inputAudioFormat: options.inputAudioFormat,
143
+ ...(options.providerOptions != null && {
144
+ providerOptions: options.providerOptions,
145
+ }),
146
+ ...(options.includeRawChunks != null && {
147
+ includeRawChunks: options.includeRawChunks,
148
+ }),
149
+ };
150
+
151
+ return {
152
+ stream: createGatewayTranscriptionStream({
153
+ webSocket: this.config.webSocket,
154
+ url: toGatewayTranscriptionUrl(this.config.baseURL, this.modelId),
155
+ protocols: getProtocolsFromHeaders(headers),
156
+ headers,
157
+ startFrame,
158
+ audio: options.audio,
159
+ abortSignal: options.abortSignal,
160
+ authMethod,
161
+ }),
162
+ request: { body: startFrame },
163
+ response: { timestamp: currentDate, modelId: this.modelId },
164
+ };
165
+ }
166
+
102
167
  private getUrl() {
103
168
  return `${this.config.baseURL}/transcription-model`;
104
169
  }
@@ -111,6 +176,222 @@ export class GatewayTranscriptionModel implements TranscriptionModelV4 {
111
176
  }
112
177
  }
113
178
 
179
+ /**
180
+ * Gateway streaming transcription WebSocket URL: HTTP(S) base upgraded to
181
+ * WS(S), model id in `?ai-model-id=` (browser `WebSocket` cannot set headers;
182
+ * slash-safe for qualified ids like `openai/gpt-realtime-whisper`).
183
+ */
184
+ function toGatewayTranscriptionUrl(baseURL: string, modelId: string): string {
185
+ const url = new URL(`${baseURL.replace(/^http/, 'ws')}/transcription-model`);
186
+ url.searchParams.set('ai-model-id', modelId);
187
+ return url.toString();
188
+ }
189
+
190
+ /**
191
+ * Auth-carrying subprotocols from the resolved request headers (bearer token
192
+ * + optional team scope). Native `WebSocket` cannot send headers, so auth
193
+ * rides the `Sec-WebSocket-Protocol` handshake. Header lookups are
194
+ * case-insensitive because `combineHeaders` does not normalize casing and
195
+ * callers can pass arbitrary casing via the `headers` option.
196
+ */
197
+ function getProtocolsFromHeaders(
198
+ headers: Record<string, string | undefined>,
199
+ ): string[] {
200
+ const normalizedHeaders = normalizeHeaders(headers);
201
+ const authorization = normalizedHeaders.authorization;
202
+ const token = authorization?.startsWith('Bearer ')
203
+ ? authorization.slice('Bearer '.length)
204
+ : undefined;
205
+
206
+ return token == null
207
+ ? [GATEWAY_TRANSCRIPTION_SUBPROTOCOL]
208
+ : getGatewayTranscriptionProtocols(token, {
209
+ teamIdOrSlug: normalizedHeaders[VERCEL_AI_GATEWAY_TEAM_HEADER],
210
+ });
211
+ }
212
+
213
+ /** Audio frames stay under server frame-size limits (envelope rule 7). */
214
+ const MAX_AUDIO_FRAME_BYTES = 64 * 1024;
215
+
216
+ function createGatewayTranscriptionStream({
217
+ webSocket,
218
+ url,
219
+ protocols,
220
+ headers,
221
+ startFrame,
222
+ audio,
223
+ abortSignal,
224
+ authMethod,
225
+ }: {
226
+ webSocket: WebSocketConstructor | undefined;
227
+ url: string;
228
+ protocols: string[];
229
+ headers: Record<string, string | undefined>;
230
+ startFrame: Experimental_TranscriptionStreamStartFrame;
231
+ audio: ReadableStream<Uint8Array | string>;
232
+ abortSignal: AbortSignal | undefined;
233
+ authMethod: 'api-key' | 'oidc' | undefined;
234
+ }): ReadableStream<TranscriptionModelV4StreamPart> {
235
+ let finished = false;
236
+ let cleanup: (closeCode?: number) => void = () => {};
237
+
238
+ return new ReadableStream<TranscriptionModelV4StreamPart>({
239
+ start: controller => {
240
+ let audioReader:
241
+ | ReadableStreamDefaultReader<Uint8Array | string>
242
+ | undefined;
243
+ let hasServerErrorPart = false;
244
+ let lastServerError: unknown;
245
+ let audioStopped = false;
246
+ let connection: WebSocketConnection | undefined;
247
+
248
+ cleanup = (closeCode?: number) => {
249
+ if (audioReader != null) {
250
+ void audioReader.cancel().catch(() => {});
251
+ } else {
252
+ // Pre-open failure or abort: `sendAudio` never took a reader, so
253
+ // cancel the caller's audio stream directly — otherwise an upstream
254
+ // producer piping into it hangs.
255
+ void audio.cancel().catch(() => {});
256
+ }
257
+ connection?.close(closeCode);
258
+ };
259
+
260
+ const stopAudio = () => {
261
+ audioStopped = true;
262
+ if (audioReader != null) {
263
+ void audioReader.cancel().catch(() => {});
264
+ audioReader = undefined;
265
+ } else {
266
+ void audio.cancel().catch(() => {});
267
+ }
268
+ };
269
+
270
+ const finishWithError = (error: unknown) => {
271
+ if (finished) return;
272
+ finished = true;
273
+ cleanup();
274
+ void errorControllerWithGatewayError(controller, error, authMethod);
275
+ };
276
+
277
+ const sendAudio = async (socket: WebSocketLike) => {
278
+ const reader = audio.getReader();
279
+ audioReader = reader;
280
+ try {
281
+ while (true) {
282
+ const { done, value } = await reader.read();
283
+ if (done || finished) break;
284
+ // Binary frames; base64 string chunks are decoded first. Caller
285
+ // chunks are split to stay under server frame-size limits
286
+ // (envelope rule 7), with backpressure between frames.
287
+ const bytes =
288
+ typeof value === 'string'
289
+ ? convertBase64ToUint8Array(value)
290
+ : value;
291
+ for (
292
+ let offset = 0;
293
+ offset < bytes.length;
294
+ offset += MAX_AUDIO_FRAME_BYTES
295
+ ) {
296
+ if (finished) break;
297
+ socket.send(
298
+ bytes.subarray(offset, offset + MAX_AUDIO_FRAME_BYTES),
299
+ );
300
+ await waitForWebSocketBufferDrain(socket);
301
+ }
302
+ }
303
+ } finally {
304
+ reader.releaseLock();
305
+ // unlocked again: cleanup must cancel `audio`, not the reader
306
+ if (audioReader === reader) {
307
+ audioReader = undefined;
308
+ }
309
+ }
310
+ if (!finished && !audioStopped) {
311
+ socket.send(
312
+ JSON.stringify({
313
+ type: TRANSCRIPTION_STREAM_AUDIO_DONE_FRAME_TYPE,
314
+ }),
315
+ );
316
+ }
317
+ };
318
+
319
+ connection = connectToWebSocket({
320
+ url,
321
+ protocols,
322
+ headers,
323
+ webSocket,
324
+ abortSignal,
325
+ onAbort: reason => {
326
+ if (finished) return;
327
+ finished = true;
328
+ cleanup();
329
+ controller.error(reason);
330
+ },
331
+ onProcessingError: finishWithError,
332
+ onOpen: socket => {
333
+ socket.send(JSON.stringify(startFrame));
334
+ void sendAudio(socket).catch(finishWithError);
335
+ },
336
+ // Server frames are envelope-serialized stream parts; the codec
337
+ // handles parsing, unknown-part skipping, and timestamp revival.
338
+ onMessageText: text => {
339
+ if (finished) return;
340
+ const part = parseTranscriptionStreamPart(text);
341
+ if (part == null) return;
342
+ if (part.type === 'finish') {
343
+ finished = true;
344
+ controller.enqueue(part);
345
+ controller.close();
346
+ cleanup(1000);
347
+ return;
348
+ }
349
+
350
+ if (part.type === 'error') {
351
+ // Remembered so the terminal close error (the server closes
352
+ // non-1000 after an error part) surfaces the server's message
353
+ // to promise-based consumers.
354
+ hasServerErrorPart = true;
355
+ lastServerError = part.error;
356
+ // envelope rule 5: error parts are terminal — stop sending audio
357
+ // while the server holds the connection open (e.g. for its final
358
+ // billing flush)
359
+ stopAudio();
360
+ }
361
+
362
+ controller.enqueue(part);
363
+ },
364
+ onSocketError: () => {
365
+ finishWithError(
366
+ new Error('Connection error on AI Gateway transcription stream'),
367
+ );
368
+ },
369
+ onClose: () => {
370
+ if (hasServerErrorPart) {
371
+ if (finished) return;
372
+ void createErrorFromServerErrorPart(
373
+ lastServerError,
374
+ authMethod,
375
+ ).then(finishWithError);
376
+ return;
377
+ }
378
+ finishWithError(
379
+ new Error(
380
+ 'AI Gateway transcription stream closed before a finish part was received',
381
+ ),
382
+ );
383
+ },
384
+ });
385
+ },
386
+
387
+ cancel: () => {
388
+ if (finished) return;
389
+ finished = true;
390
+ cleanup();
391
+ },
392
+ });
393
+ }
394
+
114
395
  const providerMetadataEntrySchema = z.object({}).catchall(z.unknown());
115
396
 
116
397
  const gatewayTranscriptionWarningSchema = z.discriminatedUnion('type', [
@@ -153,3 +434,71 @@ const gatewayTranscriptionResponseSchema = z.object({
153
434
  .record(z.string(), providerMetadataEntrySchema)
154
435
  .optional(),
155
436
  });
437
+
438
+ /**
439
+ * `asGatewayError` is async while the WebSocket event handlers are
440
+ * synchronous, hence this helper.
441
+ */
442
+ async function errorControllerWithGatewayError(
443
+ controller: ReadableStreamDefaultController<TranscriptionModelV4StreamPart>,
444
+ error: unknown,
445
+ authMethod: 'api-key' | 'oidc' | undefined,
446
+ ): Promise<void> {
447
+ controller.error(await asGatewayError(error, authMethod));
448
+ }
449
+
450
+ /**
451
+ * Extract a human-readable message from an `error` stream part's payload for
452
+ * the terminal close error.
453
+ */
454
+ function getServerErrorMessage(error: unknown): string {
455
+ if (
456
+ error != null &&
457
+ typeof error === 'object' &&
458
+ 'message' in error &&
459
+ typeof error.message === 'string'
460
+ ) {
461
+ return error.message;
462
+ }
463
+ // JSON-stringifies object payloads (`String` would yield '[object Object]').
464
+ return getErrorMessage(error);
465
+ }
466
+
467
+ /** Canonical status codes for server error-part types (no HTTP status exists on the WebSocket). */
468
+ const SERVER_ERROR_STATUS_CODES: Record<string, number> = {
469
+ authentication_error: 401,
470
+ failed_dependency: 424,
471
+ forbidden: 403,
472
+ internal_server_error: 500,
473
+ invalid_request_error: 400,
474
+ model_not_found: 404,
475
+ rate_limit_exceeded: 429,
476
+ };
477
+
478
+ /**
479
+ * Maps a server error-part payload (`{ message, type }`) to the public
480
+ * Gateway error class for its type; unknown shapes keep the generic message.
481
+ */
482
+ async function createErrorFromServerErrorPart(
483
+ error: unknown,
484
+ authMethod: 'api-key' | 'oidc' | undefined,
485
+ ): Promise<unknown> {
486
+ if (
487
+ typeof error === 'object' &&
488
+ error != null &&
489
+ 'message' in error &&
490
+ typeof error.message === 'string' &&
491
+ 'type' in error &&
492
+ typeof error.type === 'string' &&
493
+ error.type in SERVER_ERROR_STATUS_CODES
494
+ ) {
495
+ return createGatewayErrorFromResponse({
496
+ response: { error: { message: error.message, type: error.type } },
497
+ statusCode: SERVER_ERROR_STATUS_CODES[error.type],
498
+ authMethod,
499
+ });
500
+ }
501
+ return new Error(
502
+ `AI Gateway transcription stream failed: ${getServerErrorMessage(error)}`,
503
+ );
504
+ }
package/src/index.ts CHANGED
@@ -3,9 +3,11 @@ export {
3
3
  GATEWAY_AUTH_SUBPROTOCOL_PREFIX,
4
4
  GATEWAY_REALTIME_SUBPROTOCOL,
5
5
  GATEWAY_TEAM_SUBPROTOCOL_PREFIX,
6
+ GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
6
7
  getGatewayRealtimeAuthToken,
7
8
  getGatewayRealtimeProtocols,
8
9
  getGatewayRealtimeTeamIdOrSlug,
10
+ getGatewayTranscriptionProtocols,
9
11
  } from './gateway-realtime-auth';
10
12
  export type { GatewayRealtimeModelId } from './gateway-realtime-model-settings';
11
13
  export type { GatewayRerankingModelId } from './gateway-reranking-model-settings';