@ai-sdk/gateway 4.0.0-beta.1 → 4.0.0-beta.108
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 +794 -4
- package/dist/index.d.ts +314 -42
- package/dist/index.js +1369 -397
- package/dist/index.js.map +1 -1
- package/docs/00-ai-gateway.mdx +447 -61
- package/package.json +15 -15
- package/src/errors/as-gateway-error.ts +2 -1
- package/src/errors/create-gateway-error.ts +17 -3
- package/src/errors/gateway-authentication-error.ts +8 -5
- package/src/errors/gateway-error.ts +8 -0
- package/src/errors/gateway-failed-dependency-error.ts +35 -0
- package/src/errors/gateway-forbidden-error.ts +34 -0
- package/src/errors/gateway-response-error.ts +1 -1
- package/src/errors/index.ts +2 -0
- package/src/errors/parse-auth-method.ts +1 -2
- package/src/gateway-config.ts +1 -1
- package/src/gateway-embedding-model-settings.ts +1 -0
- package/src/gateway-embedding-model.ts +62 -15
- package/src/gateway-fetch-metadata.ts +51 -38
- package/src/gateway-generation-info.ts +149 -0
- package/src/gateway-headers.ts +3 -0
- package/src/gateway-image-model-settings.ts +14 -1
- package/src/gateway-image-model.ts +46 -21
- package/src/gateway-language-model-settings.ts +52 -31
- package/src/gateway-language-model.ts +72 -42
- package/src/gateway-model-entry.ts +15 -3
- package/src/gateway-provider-options.ts +50 -78
- package/src/gateway-provider.ts +239 -35
- package/src/gateway-realtime-auth.ts +126 -0
- package/src/gateway-realtime-model-settings.ts +1 -0
- package/src/gateway-realtime-model.ts +107 -0
- package/src/gateway-reranking-model-settings.ts +7 -0
- package/src/gateway-reranking-model.ts +142 -0
- package/src/gateway-speech-model-settings.ts +1 -0
- package/src/gateway-speech-model.ts +145 -0
- package/src/gateway-spend-report.ts +193 -0
- package/src/gateway-transcription-model-settings.ts +1 -0
- package/src/gateway-transcription-model.ts +155 -0
- package/src/gateway-video-model-settings.ts +4 -0
- package/src/gateway-video-model.ts +29 -19
- package/src/index.ts +30 -5
- package/src/tool/parallel-search.ts +10 -11
- package/src/tool/perplexity-search.ts +10 -11
- package/dist/index.d.mts +0 -602
- package/dist/index.mjs +0 -1539
- package/dist/index.mjs.map +0 -1
package/src/gateway-provider.ts
CHANGED
|
@@ -1,50 +1,74 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadOptionalSetting,
|
|
3
3
|
withoutTrailingSlash,
|
|
4
|
+
withUserAgentSuffix,
|
|
4
5
|
type FetchFunction,
|
|
5
6
|
} from '@ai-sdk/provider-utils';
|
|
6
7
|
import { asGatewayError, GatewayAuthenticationError } from './errors';
|
|
7
8
|
import {
|
|
8
9
|
GATEWAY_AUTH_METHOD_HEADER,
|
|
9
|
-
|
|
10
|
-
} from './
|
|
10
|
+
VERCEL_AI_GATEWAY_TEAM_HEADER,
|
|
11
|
+
} from './gateway-headers';
|
|
12
|
+
import { parseAuthMethod } from './errors/parse-auth-method';
|
|
11
13
|
import {
|
|
12
14
|
GatewayFetchMetadata,
|
|
13
15
|
type GatewayFetchMetadataResponse,
|
|
14
16
|
type GatewayCreditsResponse,
|
|
15
17
|
} from './gateway-fetch-metadata';
|
|
18
|
+
import {
|
|
19
|
+
GatewaySpendReport,
|
|
20
|
+
type GatewaySpendReportParams,
|
|
21
|
+
type GatewaySpendReportResponse,
|
|
22
|
+
} from './gateway-spend-report';
|
|
23
|
+
import {
|
|
24
|
+
GatewayGenerationInfoFetcher,
|
|
25
|
+
type GatewayGenerationInfoParams,
|
|
26
|
+
type GatewayGenerationInfo,
|
|
27
|
+
} from './gateway-generation-info';
|
|
16
28
|
import { GatewayLanguageModel } from './gateway-language-model';
|
|
17
29
|
import { GatewayEmbeddingModel } from './gateway-embedding-model';
|
|
18
30
|
import { GatewayImageModel } from './gateway-image-model';
|
|
19
31
|
import { GatewayVideoModel } from './gateway-video-model';
|
|
32
|
+
import { GatewayRerankingModel } from './gateway-reranking-model';
|
|
33
|
+
import { GatewaySpeechModel } from './gateway-speech-model';
|
|
34
|
+
import { GatewayTranscriptionModel } from './gateway-transcription-model';
|
|
35
|
+
import { GatewayRealtimeModel } from './gateway-realtime-model';
|
|
20
36
|
import type { GatewayEmbeddingModelId } from './gateway-embedding-model-settings';
|
|
21
37
|
import type { GatewayImageModelId } from './gateway-image-model-settings';
|
|
38
|
+
import type { GatewayRerankingModelId } from './gateway-reranking-model-settings';
|
|
39
|
+
import type { GatewaySpeechModelId } from './gateway-speech-model-settings';
|
|
40
|
+
import type { GatewayTranscriptionModelId } from './gateway-transcription-model-settings';
|
|
41
|
+
import type { GatewayRealtimeModelId } from './gateway-realtime-model-settings';
|
|
22
42
|
import type { GatewayVideoModelId } from './gateway-video-model-settings';
|
|
23
43
|
import { gatewayTools } from './gateway-tools';
|
|
24
44
|
import { getVercelOidcToken, getVercelRequestId } from './vercel-environment';
|
|
25
45
|
import type { GatewayModelId } from './gateway-language-model-settings';
|
|
26
46
|
import type {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
LanguageModelV4,
|
|
48
|
+
EmbeddingModelV4,
|
|
49
|
+
ImageModelV4,
|
|
50
|
+
RerankingModelV4,
|
|
51
|
+
SpeechModelV4,
|
|
52
|
+
TranscriptionModelV4,
|
|
53
|
+
Experimental_VideoModelV4,
|
|
54
|
+
Experimental_RealtimeFactoryV4 as RealtimeFactoryV4,
|
|
55
|
+
Experimental_RealtimeFactoryV4GetTokenOptions as RealtimeFactoryV4GetTokenOptions,
|
|
56
|
+
ProviderV4,
|
|
32
57
|
} from '@ai-sdk/provider';
|
|
33
|
-
import { withUserAgentSuffix } from '@ai-sdk/provider-utils';
|
|
34
58
|
import { VERSION } from './version';
|
|
35
59
|
|
|
36
|
-
export interface GatewayProvider extends
|
|
37
|
-
(modelId: GatewayModelId):
|
|
60
|
+
export interface GatewayProvider extends ProviderV4 {
|
|
61
|
+
(modelId: GatewayModelId): LanguageModelV4;
|
|
38
62
|
|
|
39
63
|
/**
|
|
40
64
|
* Creates a model for text generation.
|
|
41
65
|
*/
|
|
42
|
-
chat(modelId: GatewayModelId):
|
|
66
|
+
chat(modelId: GatewayModelId): LanguageModelV4;
|
|
43
67
|
|
|
44
68
|
/**
|
|
45
69
|
* Creates a model for text generation.
|
|
46
70
|
*/
|
|
47
|
-
languageModel(modelId: GatewayModelId):
|
|
71
|
+
languageModel(modelId: GatewayModelId): LanguageModelV4;
|
|
48
72
|
|
|
49
73
|
/**
|
|
50
74
|
* Returns available providers and models for use with the remote provider.
|
|
@@ -56,40 +80,94 @@ export interface GatewayProvider extends ProviderV3 {
|
|
|
56
80
|
*/
|
|
57
81
|
getCredits(): Promise<GatewayCreditsResponse>;
|
|
58
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns a spend report with cost, token, and request count data,
|
|
85
|
+
* aggregated by the specified dimension.
|
|
86
|
+
*/
|
|
87
|
+
getSpendReport(
|
|
88
|
+
params: GatewaySpendReportParams,
|
|
89
|
+
): Promise<GatewaySpendReportResponse>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns detailed information about a specific generation by its ID,
|
|
93
|
+
* including cost, token usage, latency, and provider details.
|
|
94
|
+
*/
|
|
95
|
+
getGenerationInfo(
|
|
96
|
+
params: GatewayGenerationInfoParams,
|
|
97
|
+
): Promise<GatewayGenerationInfo>;
|
|
98
|
+
|
|
59
99
|
/**
|
|
60
100
|
* Creates a model for generating text embeddings.
|
|
61
101
|
*/
|
|
62
|
-
embedding(modelId: GatewayEmbeddingModelId):
|
|
102
|
+
embedding(modelId: GatewayEmbeddingModelId): EmbeddingModelV4;
|
|
63
103
|
|
|
64
104
|
/**
|
|
65
105
|
* Creates a model for generating text embeddings.
|
|
66
106
|
*/
|
|
67
|
-
embeddingModel(modelId: GatewayEmbeddingModelId):
|
|
107
|
+
embeddingModel(modelId: GatewayEmbeddingModelId): EmbeddingModelV4;
|
|
68
108
|
|
|
69
109
|
/**
|
|
70
110
|
* @deprecated Use `embeddingModel` instead.
|
|
71
111
|
*/
|
|
72
|
-
textEmbeddingModel(modelId: GatewayEmbeddingModelId):
|
|
112
|
+
textEmbeddingModel(modelId: GatewayEmbeddingModelId): EmbeddingModelV4;
|
|
73
113
|
|
|
74
114
|
/**
|
|
75
115
|
* Creates a model for generating images.
|
|
76
116
|
*/
|
|
77
|
-
image(modelId: GatewayImageModelId):
|
|
117
|
+
image(modelId: GatewayImageModelId): ImageModelV4;
|
|
78
118
|
|
|
79
119
|
/**
|
|
80
120
|
* Creates a model for generating images.
|
|
81
121
|
*/
|
|
82
|
-
imageModel(modelId: GatewayImageModelId):
|
|
122
|
+
imageModel(modelId: GatewayImageModelId): ImageModelV4;
|
|
83
123
|
|
|
84
124
|
/**
|
|
85
125
|
* Creates a model for generating videos.
|
|
86
126
|
*/
|
|
87
|
-
video(modelId: GatewayVideoModelId):
|
|
127
|
+
video(modelId: GatewayVideoModelId): Experimental_VideoModelV4;
|
|
88
128
|
|
|
89
129
|
/**
|
|
90
130
|
* Creates a model for generating videos.
|
|
91
131
|
*/
|
|
92
|
-
videoModel(modelId: GatewayVideoModelId):
|
|
132
|
+
videoModel(modelId: GatewayVideoModelId): Experimental_VideoModelV4;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a model for reranking documents.
|
|
136
|
+
*/
|
|
137
|
+
reranking(modelId: GatewayRerankingModelId): RerankingModelV4;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates a model for reranking documents.
|
|
141
|
+
*/
|
|
142
|
+
rerankingModel(modelId: GatewayRerankingModelId): RerankingModelV4;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Creates a model for text-to-speech generation.
|
|
146
|
+
*/
|
|
147
|
+
speech(modelId: GatewaySpeechModelId): SpeechModelV4;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Creates a model for text-to-speech generation.
|
|
151
|
+
*/
|
|
152
|
+
speechModel(modelId: GatewaySpeechModelId): SpeechModelV4;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Creates a model for audio transcription.
|
|
156
|
+
*/
|
|
157
|
+
transcription(modelId: GatewayTranscriptionModelId): TranscriptionModelV4;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Creates a model for audio transcription.
|
|
161
|
+
*/
|
|
162
|
+
transcriptionModel(
|
|
163
|
+
modelId: GatewayTranscriptionModelId,
|
|
164
|
+
): TranscriptionModelV4;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Creates an experimental realtime model for bidirectional audio/text
|
|
168
|
+
* communication over WebSocket, normalized through the AI Gateway.
|
|
169
|
+
*/
|
|
170
|
+
experimental_realtime: RealtimeFactoryV4;
|
|
93
171
|
|
|
94
172
|
/**
|
|
95
173
|
* Gateway-specific tools executed server-side.
|
|
@@ -99,15 +177,22 @@ export interface GatewayProvider extends ProviderV3 {
|
|
|
99
177
|
|
|
100
178
|
export interface GatewayProviderSettings {
|
|
101
179
|
/**
|
|
102
|
-
* The base URL prefix for API calls. Defaults to `https://ai-gateway.vercel.sh/
|
|
180
|
+
* The base URL prefix for API calls. Defaults to `https://ai-gateway.vercel.sh/v4/ai`.
|
|
103
181
|
*/
|
|
104
182
|
baseURL?: string;
|
|
105
183
|
|
|
106
184
|
/**
|
|
107
|
-
* API key that is being sent using the `Authorization`
|
|
185
|
+
* API key or Vercel access token that is being sent using the `Authorization`
|
|
186
|
+
* header. It defaults to the `AI_GATEWAY_API_KEY` environment variable.
|
|
108
187
|
*/
|
|
109
188
|
apiKey?: string;
|
|
110
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Vercel team ID or slug to scope requests for access tokens that can access
|
|
192
|
+
* multiple teams.
|
|
193
|
+
*/
|
|
194
|
+
teamIdOrSlug?: string;
|
|
195
|
+
|
|
111
196
|
/**
|
|
112
197
|
* Custom headers to include in the requests.
|
|
113
198
|
*/
|
|
@@ -137,7 +222,7 @@ const AI_GATEWAY_PROTOCOL_VERSION = '0.0.1';
|
|
|
137
222
|
/**
|
|
138
223
|
* Create a remote provider instance.
|
|
139
224
|
*/
|
|
140
|
-
export function
|
|
225
|
+
export function createGateway(
|
|
141
226
|
options: GatewayProviderSettings = {},
|
|
142
227
|
): GatewayProvider {
|
|
143
228
|
let pendingMetadata: Promise<GatewayFetchMetadataResponse> | null = null;
|
|
@@ -148,20 +233,41 @@ export function createGatewayProvider(
|
|
|
148
233
|
|
|
149
234
|
const baseURL =
|
|
150
235
|
withoutTrailingSlash(options.baseURL) ??
|
|
151
|
-
'https://ai-gateway.vercel.sh/
|
|
236
|
+
'https://ai-gateway.vercel.sh/v4/ai';
|
|
237
|
+
|
|
238
|
+
const createAuthHeaders = (auth: {
|
|
239
|
+
token: string;
|
|
240
|
+
authMethod: 'api-key' | 'oidc';
|
|
241
|
+
}) =>
|
|
242
|
+
withUserAgentSuffix(
|
|
243
|
+
{
|
|
244
|
+
Authorization: `Bearer ${auth.token}`,
|
|
245
|
+
'ai-gateway-protocol-version': AI_GATEWAY_PROTOCOL_VERSION,
|
|
246
|
+
[GATEWAY_AUTH_METHOD_HEADER]: auth.authMethod,
|
|
247
|
+
...(options.teamIdOrSlug != null
|
|
248
|
+
? { [VERCEL_AI_GATEWAY_TEAM_HEADER]: options.teamIdOrSlug }
|
|
249
|
+
: {}),
|
|
250
|
+
...options.headers,
|
|
251
|
+
},
|
|
252
|
+
`ai-sdk/gateway/${VERSION}`,
|
|
253
|
+
);
|
|
152
254
|
|
|
153
255
|
const getHeaders = async () => {
|
|
154
256
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
257
|
+
return createAuthHeaders(await getGatewayAuthToken(options));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw GatewayAuthenticationError.createContextualError({
|
|
260
|
+
apiKeyProvided: false,
|
|
261
|
+
oidcTokenProvided: false,
|
|
262
|
+
statusCode: 401,
|
|
263
|
+
cause: error,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getRealtimeAuthToken = async () => {
|
|
269
|
+
try {
|
|
270
|
+
return await getGatewayAuthToken(options);
|
|
165
271
|
} catch (error) {
|
|
166
272
|
throw GatewayAuthenticationError.createContextualError({
|
|
167
273
|
apiKeyProvided: false,
|
|
@@ -253,6 +359,36 @@ export function createGatewayProvider(
|
|
|
253
359
|
});
|
|
254
360
|
};
|
|
255
361
|
|
|
362
|
+
const getSpendReport = async (params: GatewaySpendReportParams) => {
|
|
363
|
+
return new GatewaySpendReport({
|
|
364
|
+
baseURL,
|
|
365
|
+
headers: getHeaders,
|
|
366
|
+
fetch: options.fetch,
|
|
367
|
+
})
|
|
368
|
+
.getSpendReport(params)
|
|
369
|
+
.catch(async (error: unknown) => {
|
|
370
|
+
throw await asGatewayError(
|
|
371
|
+
error,
|
|
372
|
+
await parseAuthMethod(await getHeaders()),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const getGenerationInfo = async (params: GatewayGenerationInfoParams) => {
|
|
378
|
+
return new GatewayGenerationInfoFetcher({
|
|
379
|
+
baseURL,
|
|
380
|
+
headers: getHeaders,
|
|
381
|
+
fetch: options.fetch,
|
|
382
|
+
})
|
|
383
|
+
.getGenerationInfo(params)
|
|
384
|
+
.catch(async (error: unknown) => {
|
|
385
|
+
throw await asGatewayError(
|
|
386
|
+
error,
|
|
387
|
+
await parseAuthMethod(await getHeaders()),
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
256
392
|
const provider = function (modelId: GatewayModelId) {
|
|
257
393
|
if (new.target) {
|
|
258
394
|
throw new Error(
|
|
@@ -263,9 +399,11 @@ export function createGatewayProvider(
|
|
|
263
399
|
return createLanguageModel(modelId);
|
|
264
400
|
};
|
|
265
401
|
|
|
266
|
-
provider.specificationVersion = '
|
|
402
|
+
provider.specificationVersion = 'v4' as const;
|
|
267
403
|
provider.getAvailableModels = getAvailableModels;
|
|
268
404
|
provider.getCredits = getCredits;
|
|
405
|
+
provider.getSpendReport = getSpendReport;
|
|
406
|
+
provider.getGenerationInfo = getGenerationInfo;
|
|
269
407
|
provider.imageModel = (modelId: GatewayImageModelId) => {
|
|
270
408
|
return new GatewayImageModel(modelId, {
|
|
271
409
|
provider: 'gateway',
|
|
@@ -296,6 +434,64 @@ export function createGatewayProvider(
|
|
|
296
434
|
o11yHeaders: createO11yHeaders(),
|
|
297
435
|
});
|
|
298
436
|
};
|
|
437
|
+
const createRerankingModel = (modelId: GatewayRerankingModelId) => {
|
|
438
|
+
return new GatewayRerankingModel(modelId, {
|
|
439
|
+
provider: 'gateway',
|
|
440
|
+
baseURL,
|
|
441
|
+
headers: getHeaders,
|
|
442
|
+
fetch: options.fetch,
|
|
443
|
+
o11yHeaders: createO11yHeaders(),
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
provider.rerankingModel = createRerankingModel;
|
|
447
|
+
provider.reranking = createRerankingModel;
|
|
448
|
+
const createSpeechModel = (modelId: GatewaySpeechModelId) => {
|
|
449
|
+
return new GatewaySpeechModel(modelId, {
|
|
450
|
+
provider: 'gateway',
|
|
451
|
+
baseURL,
|
|
452
|
+
headers: getHeaders,
|
|
453
|
+
fetch: options.fetch,
|
|
454
|
+
o11yHeaders: createO11yHeaders(),
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
provider.speechModel = createSpeechModel;
|
|
458
|
+
provider.speech = createSpeechModel;
|
|
459
|
+
const createTranscriptionModel = (modelId: GatewayTranscriptionModelId) => {
|
|
460
|
+
return new GatewayTranscriptionModel(modelId, {
|
|
461
|
+
provider: 'gateway',
|
|
462
|
+
baseURL,
|
|
463
|
+
headers: getHeaders,
|
|
464
|
+
fetch: options.fetch,
|
|
465
|
+
o11yHeaders: createO11yHeaders(),
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
provider.transcriptionModel = createTranscriptionModel;
|
|
469
|
+
provider.transcription = createTranscriptionModel;
|
|
470
|
+
|
|
471
|
+
const createRealtimeModel = (modelId: GatewayRealtimeModelId) => {
|
|
472
|
+
assertGatewayRealtimeServerEnvironment();
|
|
473
|
+
return new GatewayRealtimeModel(modelId, {
|
|
474
|
+
provider: 'gateway.realtime',
|
|
475
|
+
baseURL,
|
|
476
|
+
teamIdOrSlug: options.teamIdOrSlug,
|
|
477
|
+
getAuthToken: getRealtimeAuthToken,
|
|
478
|
+
});
|
|
479
|
+
};
|
|
480
|
+
provider.experimental_realtime = Object.assign(
|
|
481
|
+
(modelId: GatewayRealtimeModelId) => createRealtimeModel(modelId),
|
|
482
|
+
{
|
|
483
|
+
getToken: async (tokenOptions: RealtimeFactoryV4GetTokenOptions) => {
|
|
484
|
+
const model = createRealtimeModel(tokenOptions.model);
|
|
485
|
+
const secret = await model.doCreateClientSecret();
|
|
486
|
+
return {
|
|
487
|
+
token: secret.token,
|
|
488
|
+
url: secret.url,
|
|
489
|
+
...(secret.expiresAt != null && { expiresAt: secret.expiresAt }),
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
) as RealtimeFactoryV4;
|
|
494
|
+
|
|
299
495
|
provider.chat = provider.languageModel;
|
|
300
496
|
provider.embedding = provider.embeddingModel;
|
|
301
497
|
provider.image = provider.imageModel;
|
|
@@ -304,7 +500,7 @@ export function createGatewayProvider(
|
|
|
304
500
|
return provider;
|
|
305
501
|
}
|
|
306
502
|
|
|
307
|
-
export const gateway =
|
|
503
|
+
export const gateway = createGateway();
|
|
308
504
|
|
|
309
505
|
export async function getGatewayAuthToken(
|
|
310
506
|
options: GatewayProviderSettings,
|
|
@@ -327,3 +523,11 @@ export async function getGatewayAuthToken(
|
|
|
327
523
|
authMethod: 'oidc',
|
|
328
524
|
};
|
|
329
525
|
}
|
|
526
|
+
|
|
527
|
+
function assertGatewayRealtimeServerEnvironment(): void {
|
|
528
|
+
if (typeof globalThis.window !== 'undefined') {
|
|
529
|
+
throw new Error(
|
|
530
|
+
'AI Gateway realtime models cannot be used in browsers yet. Use gateway.experimental_realtime from server-side code only.',
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared WebSocket subprotocol contract for AI Gateway realtime auth.
|
|
3
|
+
*
|
|
4
|
+
* The browser `WebSocket` API cannot set request headers, so the Gateway auth
|
|
5
|
+
* (bearer) token is carried through the `Sec-WebSocket-Protocol` handshake
|
|
6
|
+
* instead of an `Authorization` header — the same workaround OpenAI uses for
|
|
7
|
+
* `openai-insecure-api-key.<token>`.
|
|
8
|
+
*
|
|
9
|
+
* This module is the single source of truth for that contract so the client and
|
|
10
|
+
* the Gateway server can't drift: the client encodes values with
|
|
11
|
+
* `getGatewayRealtimeProtocols`, and the Gateway server decodes them with
|
|
12
|
+
* `getGatewayRealtimeAuthToken` / `getGatewayRealtimeTeamIdOrSlug`.
|
|
13
|
+
*
|
|
14
|
+
* WebSocket subprotocol values must fit the RFC token grammar. The auth token is
|
|
15
|
+
* sent as-is, so callers must use tokens that are valid subprotocol tokens; the
|
|
16
|
+
* optional team scope is base64url-encoded by this module. Keep the complete
|
|
17
|
+
* `Sec-WebSocket-Protocol` header compact (target under an 8 KiB safe header
|
|
18
|
+
* budget) because intermediaries may reject large upgrade headers.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
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).
|
|
25
|
+
*/
|
|
26
|
+
export const GATEWAY_REALTIME_SUBPROTOCOL = 'ai-gateway-realtime.v1';
|
|
27
|
+
|
|
28
|
+
/** Subprotocol prefix that carries the Gateway auth (bearer) token. */
|
|
29
|
+
export const GATEWAY_AUTH_SUBPROTOCOL_PREFIX = 'ai-gateway-auth.';
|
|
30
|
+
|
|
31
|
+
/** Subprotocol prefix that carries optional Vercel team scoping. */
|
|
32
|
+
export const GATEWAY_TEAM_SUBPROTOCOL_PREFIX = 'ai-gateway-team.';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Client-side: build the WebSocket subprotocols that carry `token` to the
|
|
36
|
+
* Gateway. Pass the result as the second argument to `new WebSocket(url, ...)`.
|
|
37
|
+
*/
|
|
38
|
+
export function getGatewayRealtimeProtocols(
|
|
39
|
+
token: string,
|
|
40
|
+
options?: { teamIdOrSlug?: string },
|
|
41
|
+
): string[] {
|
|
42
|
+
const protocols = [
|
|
43
|
+
GATEWAY_REALTIME_SUBPROTOCOL,
|
|
44
|
+
`${GATEWAY_AUTH_SUBPROTOCOL_PREFIX}${token}`,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
if (options?.teamIdOrSlug) {
|
|
48
|
+
protocols.push(
|
|
49
|
+
`${GATEWAY_TEAM_SUBPROTOCOL_PREFIX}${encodeSubprotocolValue(options.teamIdOrSlug)}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return protocols;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Server-side: extract the Gateway auth (bearer) token from a
|
|
58
|
+
* `Sec-WebSocket-Protocol` header value, or `undefined` when it is absent or
|
|
59
|
+
* empty. The Gateway upgrade handler turns this into an
|
|
60
|
+
* `Authorization: Bearer <token>` before its normal auth path.
|
|
61
|
+
*
|
|
62
|
+
* Accepts the raw header value (subprotocols are comma-separated and may carry
|
|
63
|
+
* surrounding whitespace).
|
|
64
|
+
*/
|
|
65
|
+
export function getGatewayRealtimeAuthToken(
|
|
66
|
+
secWebSocketProtocol: string | null | undefined,
|
|
67
|
+
): string | undefined {
|
|
68
|
+
// `findProtocol` trims the protocol, so the sliced token needs no further
|
|
69
|
+
// trimming; `|| undefined` collapses the empty-token case.
|
|
70
|
+
return (
|
|
71
|
+
findProtocol(secWebSocketProtocol, GATEWAY_AUTH_SUBPROTOCOL_PREFIX)?.slice(
|
|
72
|
+
GATEWAY_AUTH_SUBPROTOCOL_PREFIX.length,
|
|
73
|
+
) || undefined
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Server-side: extract the optional Vercel team ID or slug from the
|
|
79
|
+
* `Sec-WebSocket-Protocol` header value. Team scoping is base64url-encoded so
|
|
80
|
+
* arbitrary team slugs stay within the WebSocket subprotocol token grammar.
|
|
81
|
+
*/
|
|
82
|
+
export function getGatewayRealtimeTeamIdOrSlug(
|
|
83
|
+
secWebSocketProtocol: string | null | undefined,
|
|
84
|
+
): string | undefined {
|
|
85
|
+
const encoded = findProtocol(
|
|
86
|
+
secWebSocketProtocol,
|
|
87
|
+
GATEWAY_TEAM_SUBPROTOCOL_PREFIX,
|
|
88
|
+
)?.slice(GATEWAY_TEAM_SUBPROTOCOL_PREFIX.length);
|
|
89
|
+
if (!encoded) return undefined;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
return decodeSubprotocolValue(encoded) || undefined;
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findProtocol(
|
|
99
|
+
secWebSocketProtocol: string | null | undefined,
|
|
100
|
+
prefix: string,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
return secWebSocketProtocol
|
|
103
|
+
?.split(',')
|
|
104
|
+
.map(protocol => protocol.trim())
|
|
105
|
+
.find(protocol => protocol.startsWith(prefix));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function encodeSubprotocolValue(value: string): string {
|
|
109
|
+
const bytes = new TextEncoder().encode(value);
|
|
110
|
+
let binary = '';
|
|
111
|
+
for (const byte of bytes) {
|
|
112
|
+
binary += String.fromCharCode(byte);
|
|
113
|
+
}
|
|
114
|
+
return btoa(binary)
|
|
115
|
+
.replace(/\+/g, '-')
|
|
116
|
+
.replace(/\//g, '_')
|
|
117
|
+
.replace(/=+$/u, '');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function decodeSubprotocolValue(value: string): string {
|
|
121
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
122
|
+
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
|
123
|
+
const binary = atob(`${base64}${padding}`);
|
|
124
|
+
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
|
|
125
|
+
return new TextDecoder().decode(bytes);
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type GatewayRealtimeModelId = string & {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Experimental_RealtimeModelV4 as RealtimeModelV4,
|
|
3
|
+
Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent,
|
|
4
|
+
Experimental_RealtimeModelV4ClientSecretResult as RealtimeModelV4ClientSecretResult,
|
|
5
|
+
Experimental_RealtimeModelV4ServerEvent as RealtimeModelV4ServerEvent,
|
|
6
|
+
Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
|
|
7
|
+
} from '@ai-sdk/provider';
|
|
8
|
+
import { getGatewayRealtimeProtocols } from './gateway-realtime-auth';
|
|
9
|
+
|
|
10
|
+
export type GatewayRealtimeModelConfig = {
|
|
11
|
+
provider: string;
|
|
12
|
+
baseURL: string;
|
|
13
|
+
teamIdOrSlug?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the Gateway auth token used to authenticate the WebSocket upgrade
|
|
16
|
+
* (API key or Vercel OIDC token).
|
|
17
|
+
*/
|
|
18
|
+
getAuthToken: () => PromiseLike<{
|
|
19
|
+
token: string;
|
|
20
|
+
authMethod: 'api-key' | 'oidc';
|
|
21
|
+
}>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Realtime model backed by the AI Gateway.
|
|
26
|
+
*
|
|
27
|
+
* The Gateway normalizes realtime exactly like it normalizes every other
|
|
28
|
+
* modality: the client speaks the normalized AI SDK realtime protocol and the
|
|
29
|
+
* Gateway translates to and from the upstream provider server-side. This model
|
|
30
|
+
* is therefore a thin identity codec over that normalized protocol — only the
|
|
31
|
+
* connection and authentication are Gateway-specific.
|
|
32
|
+
*/
|
|
33
|
+
export class GatewayRealtimeModel implements RealtimeModelV4 {
|
|
34
|
+
readonly specificationVersion = 'v4' as const;
|
|
35
|
+
readonly provider: string;
|
|
36
|
+
readonly modelId: string;
|
|
37
|
+
|
|
38
|
+
private readonly config: GatewayRealtimeModelConfig;
|
|
39
|
+
|
|
40
|
+
constructor(modelId: string, config: GatewayRealtimeModelConfig) {
|
|
41
|
+
this.modelId = modelId;
|
|
42
|
+
this.provider = config.provider;
|
|
43
|
+
this.config = config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unlike providers with a dedicated ephemeral-secret endpoint (e.g. OpenAI),
|
|
48
|
+
* the Gateway v0 realtime path does not mint a new client secret. The returned
|
|
49
|
+
* token is the Gateway credential resolved by the provider (`apiKey`,
|
|
50
|
+
* `AI_GATEWAY_API_KEY`, or Vercel OIDC token) and the WebSocket upgrade is
|
|
51
|
+
* authenticated directly with that credential. The
|
|
52
|
+
* `RealtimeModelV4ClientSecretOptions` are therefore intentionally unused:
|
|
53
|
+
* `sessionConfig` is applied later via the normalized `session-update` event,
|
|
54
|
+
* and `expiresAfterSeconds` has no Gateway-side equivalent.
|
|
55
|
+
*/
|
|
56
|
+
async doCreateClientSecret(): Promise<RealtimeModelV4ClientSecretResult> {
|
|
57
|
+
const { token } = await this.config.getAuthToken();
|
|
58
|
+
return {
|
|
59
|
+
token,
|
|
60
|
+
url: toGatewayRealtimeUrl(this.config.baseURL, this.modelId),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getWebSocketConfig(options: { token: string; url: string }): {
|
|
65
|
+
url: string;
|
|
66
|
+
protocols?: string[];
|
|
67
|
+
} {
|
|
68
|
+
return {
|
|
69
|
+
url: options.url,
|
|
70
|
+
protocols: getGatewayRealtimeProtocols(options.token, {
|
|
71
|
+
teamIdOrSlug: this.config.teamIdOrSlug,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parseServerEvent(raw: unknown): RealtimeModelV4ServerEvent {
|
|
77
|
+
// The Gateway emits normalized AI SDK realtime events, so no
|
|
78
|
+
// provider-specific mapping is needed on the client.
|
|
79
|
+
return raw as RealtimeModelV4ServerEvent;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
serializeClientEvent(event: RealtimeModelV4ClientEvent): unknown {
|
|
83
|
+
// The Gateway accepts normalized AI SDK realtime events directly.
|
|
84
|
+
return event;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
buildSessionConfig(config: RealtimeModelV4SessionConfig): unknown {
|
|
88
|
+
// The session config is already normalized; the Gateway maps it to the
|
|
89
|
+
// upstream provider's session payload server-side.
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the Gateway realtime WebSocket URL. The HTTP(S) base URL is upgraded to
|
|
96
|
+
* WS(S) and the model id rides the `?ai-model-id=` query — the WS transport of
|
|
97
|
+
* the `ai-model-id` header the HTTP routes use, since a browser `WebSocket`
|
|
98
|
+
* cannot set headers. The model id is passed through verbatim; the Gateway owns
|
|
99
|
+
* resolution (including the bare → `openai/` qualification), exactly like the
|
|
100
|
+
* non-realtime routes. The query is slash-safe for qualified ids such as
|
|
101
|
+
* `openai/gpt-realtime-2`.
|
|
102
|
+
*/
|
|
103
|
+
function toGatewayRealtimeUrl(baseURL: string, modelId: string): string {
|
|
104
|
+
const url = new URL(`${baseURL.replace(/^http/, 'ws')}/realtime-model`);
|
|
105
|
+
url.searchParams.set('ai-model-id', modelId);
|
|
106
|
+
return url.toString();
|
|
107
|
+
}
|