@ai-sdk/gateway 4.0.121 → 4.0.123
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 +30 -0
- package/dist/index.d.ts +137 -42
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +377 -145
- package/dist/index.js.map +1 -1
- package/docs/00-ai-gateway.mdx +93 -11
- package/package.json +3 -3
- package/src/gateway-decision-model-settings.ts +2 -0
- package/src/gateway-language-model-settings.ts +2 -0
- package/src/gateway-provider.ts +71 -7
- package/src/gateway-realtime-auth.ts +21 -3
- package/src/gateway-realtime-model-settings.ts +1 -1
- package/src/gateway-reranking-model-settings.ts +2 -0
- package/src/gateway-speech-model-settings.ts +1 -0
- package/src/gateway-speech-model.ts +305 -1
- package/src/gateway-stream-errors.ts +71 -0
- package/src/gateway-transcription-model-settings.ts +2 -0
- package/src/gateway-transcription-model.ts +18 -72
- package/src/index.ts +2 -0
- package/src/tool/parallel-search.ts +158 -46
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Experimental_SpeechModelV4StreamOptions as SpeechModelV4StreamOptions,
|
|
3
|
+
Experimental_SpeechModelV4StreamPart as SpeechModelV4StreamPart,
|
|
4
|
+
Experimental_SpeechModelV4StreamResult as SpeechModelV4StreamResult,
|
|
5
|
+
Experimental_SpeechModelV4StreamTextPart as SpeechModelV4StreamTextPart,
|
|
6
|
+
Experimental_StreamingSpeechModelV4 as StreamingSpeechModelV4,
|
|
2
7
|
SharedV4ProviderMetadata,
|
|
3
8
|
SharedV4Warning,
|
|
4
9
|
SpeechModelV4,
|
|
@@ -7,19 +12,41 @@ import {
|
|
|
7
12
|
lazySchema,
|
|
8
13
|
zodSchema,
|
|
9
14
|
combineHeaders,
|
|
15
|
+
connectToWebSocket,
|
|
10
16
|
createJsonErrorResponseHandler,
|
|
11
17
|
createJsonResponseHandler,
|
|
12
18
|
getErrorMessage,
|
|
19
|
+
experimental_parseSpeechStreamPart as parseSpeechStreamPart,
|
|
13
20
|
postJsonToApi,
|
|
14
21
|
resolve,
|
|
22
|
+
EXPERIMENTAL_SPEECH_STREAM_START_FRAME_TYPE as SPEECH_STREAM_START_FRAME_TYPE,
|
|
23
|
+
EXPERIMENTAL_SPEECH_STREAM_TEXT_DELTA_FRAME_TYPE as SPEECH_STREAM_TEXT_DELTA_FRAME_TYPE,
|
|
24
|
+
EXPERIMENTAL_SPEECH_STREAM_TEXT_DONE_FRAME_TYPE as SPEECH_STREAM_TEXT_DONE_FRAME_TYPE,
|
|
25
|
+
waitForWebSocketBufferDrain,
|
|
26
|
+
type Experimental_SpeechStreamStartFrame,
|
|
27
|
+
type Experimental_SpeechStreamTextDeltaFrame,
|
|
15
28
|
type Resolvable,
|
|
29
|
+
type WebSocketConnection,
|
|
30
|
+
type WebSocketConstructor,
|
|
31
|
+
type WebSocketLike,
|
|
16
32
|
} from '@ai-sdk/provider-utils';
|
|
17
33
|
import { z } from './zod';
|
|
18
34
|
import { asGatewayError } from './errors';
|
|
19
35
|
import { parseAuthMethod } from './errors/parse-auth-method';
|
|
20
36
|
import type { GatewayConfig } from './gateway-config';
|
|
37
|
+
import {
|
|
38
|
+
GATEWAY_SPEECH_SUBPROTOCOL,
|
|
39
|
+
getGatewaySpeechProtocols,
|
|
40
|
+
} from './gateway-realtime-auth';
|
|
41
|
+
import {
|
|
42
|
+
createErrorFromServerErrorPart,
|
|
43
|
+
errorControllerWithGatewayError,
|
|
44
|
+
} from './gateway-stream-errors';
|
|
45
|
+
import { getProtocolsFromHeaders } from './gateway-transcription-model';
|
|
21
46
|
|
|
22
|
-
export class GatewaySpeechModel
|
|
47
|
+
export class GatewaySpeechModel
|
|
48
|
+
implements SpeechModelV4, StreamingSpeechModelV4
|
|
49
|
+
{
|
|
23
50
|
readonly specificationVersion = 'v4' as const;
|
|
24
51
|
|
|
25
52
|
constructor(
|
|
@@ -27,6 +54,9 @@ export class GatewaySpeechModel implements SpeechModelV4 {
|
|
|
27
54
|
private readonly config: GatewayConfig & {
|
|
28
55
|
provider: string;
|
|
29
56
|
o11yHeaders: Resolvable<Record<string, string>>;
|
|
57
|
+
_internal?: {
|
|
58
|
+
currentDate?: () => Date;
|
|
59
|
+
};
|
|
30
60
|
},
|
|
31
61
|
) {}
|
|
32
62
|
|
|
@@ -104,6 +134,59 @@ export class GatewaySpeechModel implements SpeechModelV4 {
|
|
|
104
134
|
}
|
|
105
135
|
}
|
|
106
136
|
|
|
137
|
+
async doStream(
|
|
138
|
+
options: SpeechModelV4StreamOptions,
|
|
139
|
+
): Promise<SpeechModelV4StreamResult> {
|
|
140
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
141
|
+
|
|
142
|
+
const headers = combineHeaders(
|
|
143
|
+
await resolve(this.config.headers ?? {}),
|
|
144
|
+
options.headers ?? {},
|
|
145
|
+
this.getModelConfigHeaders(),
|
|
146
|
+
await resolve(this.config.o11yHeaders),
|
|
147
|
+
);
|
|
148
|
+
const authMethod = await parseAuthMethod(headers);
|
|
149
|
+
|
|
150
|
+
// First frame after the WebSocket opens, per the speech-stream envelope.
|
|
151
|
+
// Optional keys are omitted so the frame stays minimal.
|
|
152
|
+
const startFrame: Experimental_SpeechStreamStartFrame = {
|
|
153
|
+
type: SPEECH_STREAM_START_FRAME_TYPE,
|
|
154
|
+
...(options.voice != null && { voice: options.voice }),
|
|
155
|
+
...(options.outputFormat != null && {
|
|
156
|
+
outputFormat: options.outputFormat,
|
|
157
|
+
}),
|
|
158
|
+
...(options.instructions != null && {
|
|
159
|
+
instructions: options.instructions,
|
|
160
|
+
}),
|
|
161
|
+
...(options.speed != null && { speed: options.speed }),
|
|
162
|
+
...(options.language != null && { language: options.language }),
|
|
163
|
+
...(options.providerOptions != null && {
|
|
164
|
+
providerOptions: options.providerOptions,
|
|
165
|
+
}),
|
|
166
|
+
...(options.includeRawChunks != null && {
|
|
167
|
+
includeRawChunks: options.includeRawChunks,
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
stream: createGatewaySpeechStream({
|
|
173
|
+
webSocket: this.config.webSocket,
|
|
174
|
+
url: toGatewaySpeechUrl(this.config.baseURL, this.modelId),
|
|
175
|
+
protocols: getProtocolsFromHeaders(headers, {
|
|
176
|
+
marker: GATEWAY_SPEECH_SUBPROTOCOL,
|
|
177
|
+
getProtocols: getGatewaySpeechProtocols,
|
|
178
|
+
}),
|
|
179
|
+
headers,
|
|
180
|
+
startFrame,
|
|
181
|
+
text: options.text,
|
|
182
|
+
abortSignal: options.abortSignal,
|
|
183
|
+
authMethod,
|
|
184
|
+
}),
|
|
185
|
+
request: { body: startFrame },
|
|
186
|
+
response: { timestamp: currentDate, modelId: this.modelId },
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
107
190
|
private getUrl() {
|
|
108
191
|
return `${this.config.baseURL}/speech-model`;
|
|
109
192
|
}
|
|
@@ -116,6 +199,227 @@ export class GatewaySpeechModel implements SpeechModelV4 {
|
|
|
116
199
|
}
|
|
117
200
|
}
|
|
118
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Gateway streaming speech WebSocket URL: HTTP(S) base upgraded to WS(S),
|
|
204
|
+
* model id in `?ai-model-id=` (browser `WebSocket` cannot set headers).
|
|
205
|
+
*/
|
|
206
|
+
export function toGatewaySpeechUrl(baseURL: string, modelId: string): string {
|
|
207
|
+
const url = new URL(`${baseURL.replace(/^http/, 'ws')}/speech-model`);
|
|
208
|
+
url.searchParams.set('ai-model-id', modelId);
|
|
209
|
+
return url.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Keeps text-delta frames under 64 KiB (envelope rule 7) even when JSON
|
|
214
|
+
* escapes every code unit as `\uXXXX`.
|
|
215
|
+
*/
|
|
216
|
+
const MAX_TEXT_DELTA_CODE_UNITS = 8 * 1024;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Splits a long text delta into pieces of at most `MAX_TEXT_DELTA_CODE_UNITS`
|
|
220
|
+
* UTF-16 code units without separating a surrogate pair.
|
|
221
|
+
*/
|
|
222
|
+
export function splitTextDelta(delta: string): string[] {
|
|
223
|
+
const pieces: string[] = [];
|
|
224
|
+
let offset = 0;
|
|
225
|
+
while (offset < delta.length) {
|
|
226
|
+
let end = Math.min(offset + MAX_TEXT_DELTA_CODE_UNITS, delta.length);
|
|
227
|
+
const lastCodeUnit = delta.charCodeAt(end - 1);
|
|
228
|
+
if (
|
|
229
|
+
end < delta.length &&
|
|
230
|
+
lastCodeUnit >= 0xd800 &&
|
|
231
|
+
lastCodeUnit <= 0xdbff
|
|
232
|
+
) {
|
|
233
|
+
end--;
|
|
234
|
+
}
|
|
235
|
+
pieces.push(delta.slice(offset, end));
|
|
236
|
+
offset = end;
|
|
237
|
+
}
|
|
238
|
+
return pieces;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function createGatewaySpeechStream({
|
|
242
|
+
webSocket,
|
|
243
|
+
url,
|
|
244
|
+
protocols,
|
|
245
|
+
headers,
|
|
246
|
+
startFrame,
|
|
247
|
+
text,
|
|
248
|
+
abortSignal,
|
|
249
|
+
authMethod,
|
|
250
|
+
}: {
|
|
251
|
+
webSocket: WebSocketConstructor | undefined;
|
|
252
|
+
url: string;
|
|
253
|
+
protocols: string[];
|
|
254
|
+
headers: Record<string, string | undefined>;
|
|
255
|
+
startFrame: Experimental_SpeechStreamStartFrame;
|
|
256
|
+
text: ReadableStream<SpeechModelV4StreamTextPart>;
|
|
257
|
+
abortSignal: AbortSignal | undefined;
|
|
258
|
+
authMethod: 'api-key' | 'oidc' | undefined;
|
|
259
|
+
}): ReadableStream<SpeechModelV4StreamPart> {
|
|
260
|
+
let finished = false;
|
|
261
|
+
let cleanup: (closeCode?: number) => void = () => {};
|
|
262
|
+
|
|
263
|
+
return new ReadableStream<SpeechModelV4StreamPart>({
|
|
264
|
+
start: controller => {
|
|
265
|
+
let textReader:
|
|
266
|
+
| ReadableStreamDefaultReader<SpeechModelV4StreamTextPart>
|
|
267
|
+
| undefined;
|
|
268
|
+
let hasServerErrorPart = false;
|
|
269
|
+
let lastServerError: unknown;
|
|
270
|
+
let textStopped = false;
|
|
271
|
+
let connection: WebSocketConnection | undefined;
|
|
272
|
+
|
|
273
|
+
const cancelText = () => {
|
|
274
|
+
if (textReader != null) {
|
|
275
|
+
void textReader.cancel().catch(() => {});
|
|
276
|
+
textReader = undefined;
|
|
277
|
+
} else {
|
|
278
|
+
void text.cancel().catch(() => {});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
cleanup = (closeCode?: number) => {
|
|
283
|
+
cancelText();
|
|
284
|
+
connection?.close(closeCode);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const finishWithError = (error: unknown) => {
|
|
288
|
+
if (finished) return;
|
|
289
|
+
finished = true;
|
|
290
|
+
cleanup();
|
|
291
|
+
void errorControllerWithGatewayError(controller, error, authMethod);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const sendText = async (socket: WebSocketLike) => {
|
|
295
|
+
const reader = text.getReader();
|
|
296
|
+
textReader = reader;
|
|
297
|
+
try {
|
|
298
|
+
while (true) {
|
|
299
|
+
const { done, value } = await reader.read();
|
|
300
|
+
if (done || finished || textStopped) break;
|
|
301
|
+
let first = true;
|
|
302
|
+
// An empty part still carries its voice or provider options.
|
|
303
|
+
const pieces = splitTextDelta(value.text);
|
|
304
|
+
if (
|
|
305
|
+
pieces.length === 0 &&
|
|
306
|
+
(value.voice != null || value.providerOptions != null)
|
|
307
|
+
) {
|
|
308
|
+
pieces.push('');
|
|
309
|
+
}
|
|
310
|
+
for (const delta of pieces) {
|
|
311
|
+
if (finished || textStopped) break;
|
|
312
|
+
// The voice applies to every frame of the part; provider
|
|
313
|
+
// options (e.g. a turn break) only to its first frame.
|
|
314
|
+
const frame: Experimental_SpeechStreamTextDeltaFrame = {
|
|
315
|
+
type: SPEECH_STREAM_TEXT_DELTA_FRAME_TYPE,
|
|
316
|
+
delta,
|
|
317
|
+
...(value.voice != null && { voice: value.voice }),
|
|
318
|
+
...(first &&
|
|
319
|
+
value.providerOptions != null && {
|
|
320
|
+
providerOptions: value.providerOptions,
|
|
321
|
+
}),
|
|
322
|
+
};
|
|
323
|
+
first = false;
|
|
324
|
+
socket.send(JSON.stringify(frame));
|
|
325
|
+
await waitForWebSocketBufferDrain(socket);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
reader.releaseLock();
|
|
330
|
+
if (textReader === reader) {
|
|
331
|
+
textReader = undefined;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!finished && !textStopped) {
|
|
335
|
+
socket.send(
|
|
336
|
+
JSON.stringify({ type: SPEECH_STREAM_TEXT_DONE_FRAME_TYPE }),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
connection = connectToWebSocket({
|
|
342
|
+
url,
|
|
343
|
+
protocols,
|
|
344
|
+
headers,
|
|
345
|
+
webSocket,
|
|
346
|
+
abortSignal,
|
|
347
|
+
onAbort: reason => {
|
|
348
|
+
if (finished) return;
|
|
349
|
+
finished = true;
|
|
350
|
+
cleanup();
|
|
351
|
+
controller.error(reason);
|
|
352
|
+
},
|
|
353
|
+
onProcessingError: finishWithError,
|
|
354
|
+
onOpen: socket => {
|
|
355
|
+
socket.send(JSON.stringify(startFrame));
|
|
356
|
+
void sendText(socket).catch(finishWithError);
|
|
357
|
+
},
|
|
358
|
+
onMessageText: async messageText => {
|
|
359
|
+
if (finished) return;
|
|
360
|
+
const part = parseSpeechStreamPart(messageText);
|
|
361
|
+
if (part == null) return;
|
|
362
|
+
if (part.type === 'finish') {
|
|
363
|
+
finished = true;
|
|
364
|
+
controller.enqueue(part);
|
|
365
|
+
controller.close();
|
|
366
|
+
cleanup(1000);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (part.type === 'error') {
|
|
371
|
+
// Error parts are terminal (envelope rule 5): stop sending text
|
|
372
|
+
// and surface the server's message when the socket closes.
|
|
373
|
+
hasServerErrorPart = true;
|
|
374
|
+
lastServerError = part.error;
|
|
375
|
+
textStopped = true;
|
|
376
|
+
cancelText();
|
|
377
|
+
part.error = await createErrorFromServerErrorPart(
|
|
378
|
+
part.error,
|
|
379
|
+
authMethod,
|
|
380
|
+
'speech',
|
|
381
|
+
);
|
|
382
|
+
if (finished) return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
controller.enqueue(part);
|
|
386
|
+
},
|
|
387
|
+
onSocketError: () => {
|
|
388
|
+
finishWithError(
|
|
389
|
+
new Error('Connection error on AI Gateway speech stream'),
|
|
390
|
+
);
|
|
391
|
+
},
|
|
392
|
+
onClose: ({ code, reason }) => {
|
|
393
|
+
if (hasServerErrorPart) {
|
|
394
|
+
if (finished) return;
|
|
395
|
+
void createErrorFromServerErrorPart(
|
|
396
|
+
lastServerError,
|
|
397
|
+
authMethod,
|
|
398
|
+
'speech',
|
|
399
|
+
).then(finishWithError);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
finishWithError(
|
|
403
|
+
new Error(
|
|
404
|
+
'AI Gateway speech stream closed before a finish part was received' +
|
|
405
|
+
(code != null
|
|
406
|
+
? ` (code ${code}${reason ? `: ${reason}` : ''})`
|
|
407
|
+
: '') +
|
|
408
|
+
'.',
|
|
409
|
+
),
|
|
410
|
+
);
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
cancel: () => {
|
|
416
|
+
if (finished) return;
|
|
417
|
+
finished = true;
|
|
418
|
+
cleanup();
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
119
423
|
const providerMetadataEntrySchema = () => z.object({}).catchall(z.unknown());
|
|
120
424
|
|
|
121
425
|
const gatewaySpeechWarningSchema = () =>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { getErrorMessage } from '@ai-sdk/provider';
|
|
2
|
+
import { asGatewayError, createGatewayErrorFromResponse } from './errors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `asGatewayError` is async while the WebSocket event handlers are
|
|
6
|
+
* synchronous, hence this helper.
|
|
7
|
+
*/
|
|
8
|
+
export async function errorControllerWithGatewayError<PART>(
|
|
9
|
+
controller: ReadableStreamDefaultController<PART>,
|
|
10
|
+
error: unknown,
|
|
11
|
+
authMethod: 'api-key' | 'oidc' | undefined,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
controller.error(await asGatewayError(error, authMethod));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a human-readable message from an `error` stream part's payload for
|
|
18
|
+
* the terminal close error.
|
|
19
|
+
*/
|
|
20
|
+
function getServerErrorMessage(error: unknown): string {
|
|
21
|
+
if (
|
|
22
|
+
error != null &&
|
|
23
|
+
typeof error === 'object' &&
|
|
24
|
+
'message' in error &&
|
|
25
|
+
typeof error.message === 'string'
|
|
26
|
+
) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
// JSON-stringifies object payloads (`String` would yield '[object Object]').
|
|
30
|
+
return getErrorMessage(error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Canonical status codes for server error-part types (no HTTP status exists on the WebSocket). */
|
|
34
|
+
const SERVER_ERROR_STATUS_CODES: Record<string, number> = {
|
|
35
|
+
authentication_error: 401,
|
|
36
|
+
failed_dependency: 424,
|
|
37
|
+
forbidden: 403,
|
|
38
|
+
internal_server_error: 500,
|
|
39
|
+
invalid_request_error: 400,
|
|
40
|
+
model_not_found: 404,
|
|
41
|
+
rate_limit_exceeded: 429,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Maps a server error-part payload (`{ message, type }`) to the public
|
|
46
|
+
* Gateway error class for its type; unknown shapes keep the generic message.
|
|
47
|
+
*/
|
|
48
|
+
export async function createErrorFromServerErrorPart(
|
|
49
|
+
error: unknown,
|
|
50
|
+
authMethod: 'api-key' | 'oidc' | undefined,
|
|
51
|
+
streamLabel = 'transcription',
|
|
52
|
+
): Promise<unknown> {
|
|
53
|
+
if (
|
|
54
|
+
typeof error === 'object' &&
|
|
55
|
+
error != null &&
|
|
56
|
+
'message' in error &&
|
|
57
|
+
typeof error.message === 'string' &&
|
|
58
|
+
'type' in error &&
|
|
59
|
+
typeof error.type === 'string' &&
|
|
60
|
+
error.type in SERVER_ERROR_STATUS_CODES
|
|
61
|
+
) {
|
|
62
|
+
return createGatewayErrorFromResponse({
|
|
63
|
+
response: { error: { message: error.message, type: error.type } },
|
|
64
|
+
statusCode: SERVER_ERROR_STATUS_CODES[error.type],
|
|
65
|
+
authMethod,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return new Error(
|
|
69
|
+
`AI Gateway ${streamLabel} stream failed: ${getServerErrorMessage(error)}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -4,6 +4,8 @@ export type GatewayTranscriptionModelId =
|
|
|
4
4
|
| 'google/gemini-3.5-transcribe-live'
|
|
5
5
|
| 'microsoft/mai-transcribe-2'
|
|
6
6
|
| 'microsoft/mai-transcribe-2-streaming'
|
|
7
|
+
| 'mistral/voxtral-mini-transcribe-3'
|
|
8
|
+
| 'mistral/voxtral-small-transcribe-3'
|
|
7
9
|
| 'openai/gpt-4o-mini-transcribe'
|
|
8
10
|
| 'openai/gpt-4o-transcribe'
|
|
9
11
|
| 'openai/gpt-realtime-whisper'
|
|
@@ -30,9 +30,13 @@ import {
|
|
|
30
30
|
type WebSocketLike,
|
|
31
31
|
} from '@ai-sdk/provider-utils';
|
|
32
32
|
import { z } from './zod';
|
|
33
|
-
import { asGatewayError
|
|
33
|
+
import { asGatewayError } from './errors';
|
|
34
34
|
import { parseAuthMethod } from './errors/parse-auth-method';
|
|
35
35
|
import type { GatewayConfig } from './gateway-config';
|
|
36
|
+
import {
|
|
37
|
+
createErrorFromServerErrorPart,
|
|
38
|
+
errorControllerWithGatewayError,
|
|
39
|
+
} from './gateway-stream-errors';
|
|
36
40
|
import { VERCEL_AI_GATEWAY_TEAM_HEADER } from './gateway-headers';
|
|
37
41
|
import {
|
|
38
42
|
GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
|
|
@@ -247,8 +251,18 @@ export function toGatewayTranscriptionUrl(
|
|
|
247
251
|
* case-insensitive because `combineHeaders` does not normalize casing and
|
|
248
252
|
* callers can pass arbitrary casing via the `headers` option.
|
|
249
253
|
*/
|
|
250
|
-
function getProtocolsFromHeaders(
|
|
254
|
+
export function getProtocolsFromHeaders(
|
|
251
255
|
headers: Record<string, string | undefined>,
|
|
256
|
+
{
|
|
257
|
+
marker = GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
|
|
258
|
+
getProtocols = getGatewayTranscriptionProtocols,
|
|
259
|
+
}: {
|
|
260
|
+
marker?: string;
|
|
261
|
+
getProtocols?: (
|
|
262
|
+
token: string,
|
|
263
|
+
options: { teamIdOrSlug?: string },
|
|
264
|
+
) => string[];
|
|
265
|
+
} = {},
|
|
252
266
|
): string[] {
|
|
253
267
|
const normalizedHeaders = normalizeHeaders(headers);
|
|
254
268
|
const authorization = normalizedHeaders.authorization;
|
|
@@ -257,8 +271,8 @@ function getProtocolsFromHeaders(
|
|
|
257
271
|
: undefined;
|
|
258
272
|
|
|
259
273
|
return token == null
|
|
260
|
-
? [
|
|
261
|
-
:
|
|
274
|
+
? [marker]
|
|
275
|
+
: getProtocols(token, {
|
|
262
276
|
teamIdOrSlug: normalizedHeaders[VERCEL_AI_GATEWAY_TEAM_HEADER],
|
|
263
277
|
});
|
|
264
278
|
}
|
|
@@ -493,71 +507,3 @@ const gatewayTranscriptionResponseSchema = lazySchema(() =>
|
|
|
493
507
|
}),
|
|
494
508
|
),
|
|
495
509
|
);
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* `asGatewayError` is async while the WebSocket event handlers are
|
|
499
|
-
* synchronous, hence this helper.
|
|
500
|
-
*/
|
|
501
|
-
async function errorControllerWithGatewayError(
|
|
502
|
-
controller: ReadableStreamDefaultController<TranscriptionModelV4StreamPart>,
|
|
503
|
-
error: unknown,
|
|
504
|
-
authMethod: 'api-key' | 'oidc' | undefined,
|
|
505
|
-
): Promise<void> {
|
|
506
|
-
controller.error(await asGatewayError(error, authMethod));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Extract a human-readable message from an `error` stream part's payload for
|
|
511
|
-
* the terminal close error.
|
|
512
|
-
*/
|
|
513
|
-
function getServerErrorMessage(error: unknown): string {
|
|
514
|
-
if (
|
|
515
|
-
error != null &&
|
|
516
|
-
typeof error === 'object' &&
|
|
517
|
-
'message' in error &&
|
|
518
|
-
typeof error.message === 'string'
|
|
519
|
-
) {
|
|
520
|
-
return error.message;
|
|
521
|
-
}
|
|
522
|
-
// JSON-stringifies object payloads (`String` would yield '[object Object]').
|
|
523
|
-
return getErrorMessage(error);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Canonical status codes for server error-part types (no HTTP status exists on the WebSocket). */
|
|
527
|
-
const SERVER_ERROR_STATUS_CODES: Record<string, number> = {
|
|
528
|
-
authentication_error: 401,
|
|
529
|
-
failed_dependency: 424,
|
|
530
|
-
forbidden: 403,
|
|
531
|
-
internal_server_error: 500,
|
|
532
|
-
invalid_request_error: 400,
|
|
533
|
-
model_not_found: 404,
|
|
534
|
-
rate_limit_exceeded: 429,
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Maps a server error-part payload (`{ message, type }`) to the public
|
|
539
|
-
* Gateway error class for its type; unknown shapes keep the generic message.
|
|
540
|
-
*/
|
|
541
|
-
async function createErrorFromServerErrorPart(
|
|
542
|
-
error: unknown,
|
|
543
|
-
authMethod: 'api-key' | 'oidc' | undefined,
|
|
544
|
-
): Promise<unknown> {
|
|
545
|
-
if (
|
|
546
|
-
typeof error === 'object' &&
|
|
547
|
-
error != null &&
|
|
548
|
-
'message' in error &&
|
|
549
|
-
typeof error.message === 'string' &&
|
|
550
|
-
'type' in error &&
|
|
551
|
-
typeof error.type === 'string' &&
|
|
552
|
-
error.type in SERVER_ERROR_STATUS_CODES
|
|
553
|
-
) {
|
|
554
|
-
return createGatewayErrorFromResponse({
|
|
555
|
-
response: { error: { message: error.message, type: error.type } },
|
|
556
|
-
statusCode: SERVER_ERROR_STATUS_CODES[error.type],
|
|
557
|
-
authMethod,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
return new Error(
|
|
561
|
-
`AI Gateway transcription stream failed: ${getServerErrorMessage(error)}`,
|
|
562
|
-
);
|
|
563
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -8,11 +8,13 @@ export type { GatewayModelId } from './gateway-language-model-settings';
|
|
|
8
8
|
export {
|
|
9
9
|
GATEWAY_AUTH_SUBPROTOCOL_PREFIX,
|
|
10
10
|
GATEWAY_REALTIME_SUBPROTOCOL,
|
|
11
|
+
GATEWAY_SPEECH_SUBPROTOCOL,
|
|
11
12
|
GATEWAY_TEAM_SUBPROTOCOL_PREFIX,
|
|
12
13
|
GATEWAY_TRANSCRIPTION_SUBPROTOCOL,
|
|
13
14
|
getGatewayRealtimeAuthToken,
|
|
14
15
|
getGatewayRealtimeProtocols,
|
|
15
16
|
getGatewayRealtimeTeamIdOrSlug,
|
|
17
|
+
getGatewaySpeechProtocols,
|
|
16
18
|
getGatewayTranscriptionProtocols,
|
|
17
19
|
} from './gateway-realtime-auth';
|
|
18
20
|
export type { GatewayRealtimeModelId } from './gateway-realtime-model-settings';
|