ai-gateway-proxy 0.0.1
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/LICENSE +21 -0
- package/dist/index.cjs +386 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +381 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Mintlify
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
var _async_to_generator = require('@swc/helpers/_/_async_to_generator');
|
|
4
|
+
var oidc = require('@vercel/oidc');
|
|
5
|
+
var _extends = require('@swc/helpers/_/_extends');
|
|
6
|
+
var providerUtils = require('@ai-sdk/provider-utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get authentication token for the AI Gateway.
|
|
10
|
+
* Tries API key first (AI_GATEWAY_API_KEY env var), then falls back to Vercel OIDC.
|
|
11
|
+
*/ function getGatewayAuthToken() {
|
|
12
|
+
return _async_to_generator._(function*() {
|
|
13
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY;
|
|
14
|
+
if (apiKey) {
|
|
15
|
+
return {
|
|
16
|
+
token: apiKey,
|
|
17
|
+
authMethod: "api-key"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const oidcToken = yield oidc.getVercelOidcToken();
|
|
22
|
+
return {
|
|
23
|
+
token: oidcToken,
|
|
24
|
+
authMethod: "oidc"
|
|
25
|
+
};
|
|
26
|
+
} catch (unused) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Aggregator for collecting stream content.
|
|
34
|
+
* Tracks text deltas, reasoning, tool calls, and other content to build the final GatewayResponse.
|
|
35
|
+
*
|
|
36
|
+
* Uses AI SDK V3 protocol types.
|
|
37
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
38
|
+
*/ class StreamContentAggregator {
|
|
39
|
+
/**
|
|
40
|
+
* Process a stream part and update aggregated state.
|
|
41
|
+
* Uses LanguageModelV3StreamPart type from @ai-sdk/provider.
|
|
42
|
+
*
|
|
43
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
44
|
+
*/ process(part) {
|
|
45
|
+
switch(part.type){
|
|
46
|
+
case "stream-start":
|
|
47
|
+
this.warnings.push(...part.warnings);
|
|
48
|
+
break;
|
|
49
|
+
case "text-start":
|
|
50
|
+
this.textContent.set(part.id, {
|
|
51
|
+
type: "text",
|
|
52
|
+
text: ""
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
case "text-delta":
|
|
56
|
+
{
|
|
57
|
+
const text = this.textContent.get(part.id);
|
|
58
|
+
if (text) {
|
|
59
|
+
text.text += part.delta;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "text-end":
|
|
64
|
+
{
|
|
65
|
+
const text = this.textContent.get(part.id);
|
|
66
|
+
if (text) {
|
|
67
|
+
this.content.push(text);
|
|
68
|
+
this.textContent.delete(part.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "reasoning-start":
|
|
73
|
+
this.reasoningContent.set(part.id, {
|
|
74
|
+
type: "reasoning",
|
|
75
|
+
text: ""
|
|
76
|
+
});
|
|
77
|
+
break;
|
|
78
|
+
case "reasoning-delta":
|
|
79
|
+
{
|
|
80
|
+
const reasoning = this.reasoningContent.get(part.id);
|
|
81
|
+
if (reasoning) {
|
|
82
|
+
reasoning.text += part.delta;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
case "reasoning-end":
|
|
87
|
+
{
|
|
88
|
+
const reasoning = this.reasoningContent.get(part.id);
|
|
89
|
+
if (reasoning) {
|
|
90
|
+
this.content.push(reasoning);
|
|
91
|
+
this.reasoningContent.delete(part.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "tool-input-start":
|
|
96
|
+
case "tool-input-delta":
|
|
97
|
+
case "tool-input-end":
|
|
98
|
+
break;
|
|
99
|
+
case "tool-call":
|
|
100
|
+
this.content.push(part);
|
|
101
|
+
break;
|
|
102
|
+
case "tool-result":
|
|
103
|
+
this.content.push(part);
|
|
104
|
+
break;
|
|
105
|
+
case "tool-approval-request":
|
|
106
|
+
this.content.push(part);
|
|
107
|
+
break;
|
|
108
|
+
case "file":
|
|
109
|
+
this.content.push(part);
|
|
110
|
+
break;
|
|
111
|
+
case "source":
|
|
112
|
+
this.content.push(part);
|
|
113
|
+
break;
|
|
114
|
+
case "finish":
|
|
115
|
+
// Finalize any remaining content
|
|
116
|
+
for (const [, text] of this.textContent){
|
|
117
|
+
this.content.push(text);
|
|
118
|
+
}
|
|
119
|
+
this.textContent.clear();
|
|
120
|
+
for (const [, reasoning] of this.reasoningContent){
|
|
121
|
+
this.content.push(reasoning);
|
|
122
|
+
}
|
|
123
|
+
this.reasoningContent.clear();
|
|
124
|
+
this.finishReason = part.finishReason;
|
|
125
|
+
this.usage = part.usage;
|
|
126
|
+
this.providerMetadata = part.providerMetadata;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get the aggregated response.
|
|
132
|
+
*/ getResponse() {
|
|
133
|
+
return {
|
|
134
|
+
content: this.content,
|
|
135
|
+
finishReason: this.finishReason,
|
|
136
|
+
usage: this.usage,
|
|
137
|
+
warnings: this.warnings,
|
|
138
|
+
providerMetadata: this.providerMetadata
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
constructor(){
|
|
142
|
+
this.content = [];
|
|
143
|
+
this.warnings = [];
|
|
144
|
+
// Text content being built (keyed by id)
|
|
145
|
+
this.textContent = new Map();
|
|
146
|
+
// Reasoning content being built (keyed by id)
|
|
147
|
+
this.reasoningContent = new Map();
|
|
148
|
+
this.finishReason = {
|
|
149
|
+
unified: "stop",
|
|
150
|
+
raw: undefined
|
|
151
|
+
};
|
|
152
|
+
this.usage = {
|
|
153
|
+
inputTokens: {
|
|
154
|
+
total: undefined,
|
|
155
|
+
noCache: undefined,
|
|
156
|
+
cacheRead: undefined,
|
|
157
|
+
cacheWrite: undefined
|
|
158
|
+
},
|
|
159
|
+
outputTokens: {
|
|
160
|
+
total: undefined,
|
|
161
|
+
text: undefined,
|
|
162
|
+
reasoning: undefined
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Creates a transform stream that:
|
|
169
|
+
* 1. Passes through all SSE events immediately
|
|
170
|
+
* 2. Aggregates content using StreamContentAggregator
|
|
171
|
+
* 3. Calls afterResponse when finish event is received
|
|
172
|
+
* 4. Modifies the finish event based on afterResponse result
|
|
173
|
+
*
|
|
174
|
+
* Uses EventSourceParserStream from @ai-sdk/provider-utils for SSE parsing.
|
|
175
|
+
* EventSourceParserStream is re-exported from the eventsource-parser package.
|
|
176
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider-utils/src/parse-json-event-stream.ts
|
|
177
|
+
* @see https://github.com/rexxars/eventsource-parser
|
|
178
|
+
*/ function createStreamTransformer(body, requestBody, afterResponse) {
|
|
179
|
+
const encoder = new TextEncoder();
|
|
180
|
+
const aggregator = new StreamContentAggregator();
|
|
181
|
+
// Use EventSourceParserStream for SSE parsing (re-exported from eventsource-parser via @ai-sdk/provider-utils)
|
|
182
|
+
const eventStream = body.pipeThrough(new TextDecoderStream()).pipeThrough(new providerUtils.EventSourceParserStream());
|
|
183
|
+
return new ReadableStream({
|
|
184
|
+
start (controller) {
|
|
185
|
+
return _async_to_generator._(function*() {
|
|
186
|
+
const reader = eventStream.getReader();
|
|
187
|
+
try {
|
|
188
|
+
while(true){
|
|
189
|
+
const { done, value } = yield reader.read();
|
|
190
|
+
if (done) break;
|
|
191
|
+
const { data } = value;
|
|
192
|
+
// Handle DONE marker (end of stream)
|
|
193
|
+
if (data === "DONE" || data === "[DONE]") {
|
|
194
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Parse the JSON event data
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(data);
|
|
201
|
+
} catch (unused) {
|
|
202
|
+
// Not valid JSON, pass through as-is
|
|
203
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// Track content for aggregation
|
|
207
|
+
aggregator.process(parsed);
|
|
208
|
+
// Handle finish event specially - call afterResponse hook
|
|
209
|
+
if (parsed.type === "finish") {
|
|
210
|
+
const modifiedResponse = yield afterResponse({
|
|
211
|
+
request: requestBody,
|
|
212
|
+
response: aggregator.getResponse()
|
|
213
|
+
});
|
|
214
|
+
// Emit modified finish event
|
|
215
|
+
const modifiedFinish = {
|
|
216
|
+
type: "finish",
|
|
217
|
+
finishReason: modifiedResponse.finishReason,
|
|
218
|
+
usage: modifiedResponse.usage,
|
|
219
|
+
providerMetadata: modifiedResponse.providerMetadata
|
|
220
|
+
};
|
|
221
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(modifiedFinish)}\n\n`));
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Pass through event unchanged
|
|
225
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
226
|
+
}
|
|
227
|
+
controller.close();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
controller.error(error);
|
|
230
|
+
} finally{
|
|
231
|
+
reader.releaseLock();
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Creates a proxy handler for the Vercel AI Gateway.
|
|
240
|
+
*
|
|
241
|
+
* Uses AI SDK V3 Language Model.
|
|
242
|
+
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
243
|
+
*/ const createGatewayProxy = ({ baseUrl = "https://ai-gateway.vercel.sh/v1/ai", headers, beforeRequest, afterResponse, onError } = {})=>{
|
|
244
|
+
const handler = (request, { params })=>_async_to_generator._(function*() {
|
|
245
|
+
var _ref;
|
|
246
|
+
const auth = yield getGatewayAuthToken();
|
|
247
|
+
if (!auth) {
|
|
248
|
+
return new Response("AI Gateway authentication not configured", {
|
|
249
|
+
status: 500
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const { segments } = yield params;
|
|
253
|
+
const path = (_ref = segments == null ? void 0 : segments.join("/")) != null ? _ref : "";
|
|
254
|
+
const searchParams = new URL(request.url).searchParams;
|
|
255
|
+
const upstream = `${baseUrl}/${path}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
|
|
256
|
+
let requestBody;
|
|
257
|
+
try {
|
|
258
|
+
requestBody = yield request.clone().json();
|
|
259
|
+
} catch (unused) {
|
|
260
|
+
return new Response("Invalid JSON body", {
|
|
261
|
+
status: 400
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Apply beforeRequest hook
|
|
265
|
+
if (beforeRequest) {
|
|
266
|
+
requestBody = yield beforeRequest({
|
|
267
|
+
request: requestBody
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const isStreaming = request.headers.get("ai-language-model-streaming") === "true";
|
|
271
|
+
try {
|
|
272
|
+
var _request_headers_get;
|
|
273
|
+
const res = yield fetch(upstream, {
|
|
274
|
+
method: request.method,
|
|
275
|
+
body: JSON.stringify(requestBody),
|
|
276
|
+
duplex: "half",
|
|
277
|
+
headers: _extends._({
|
|
278
|
+
"content-type": "application/json"
|
|
279
|
+
}, headers, {
|
|
280
|
+
authorization: `Bearer ${auth.token}`,
|
|
281
|
+
"ai-gateway-auth-method": auth.authMethod,
|
|
282
|
+
"ai-gateway-protocol-version": "0.0.1",
|
|
283
|
+
"ai-language-model-id": (_request_headers_get = request.headers.get("ai-language-model-id")) != null ? _request_headers_get : "",
|
|
284
|
+
"ai-language-model-streaming": isStreaming ? "true" : "false"
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
// Handle error responses
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
let errorBody;
|
|
290
|
+
try {
|
|
291
|
+
errorBody = yield res.json();
|
|
292
|
+
} catch (unused) {
|
|
293
|
+
errorBody = {
|
|
294
|
+
error: {
|
|
295
|
+
message: res.statusText || "Unknown error",
|
|
296
|
+
type: "gateway_error"
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (onError) {
|
|
301
|
+
const result = yield onError({
|
|
302
|
+
request: requestBody,
|
|
303
|
+
error: errorBody,
|
|
304
|
+
status: res.status
|
|
305
|
+
});
|
|
306
|
+
// If onError returns a Response, use it directly
|
|
307
|
+
if (result instanceof Response) {
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
// Otherwise, return the modified error
|
|
311
|
+
return new Response(JSON.stringify(result), {
|
|
312
|
+
status: res.status,
|
|
313
|
+
statusText: res.statusText,
|
|
314
|
+
headers: {
|
|
315
|
+
"content-type": "application/json"
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return new Response(JSON.stringify(errorBody), {
|
|
320
|
+
status: res.status,
|
|
321
|
+
statusText: res.statusText,
|
|
322
|
+
headers: {
|
|
323
|
+
"content-type": "application/json"
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// For streaming responses
|
|
328
|
+
if (isStreaming) {
|
|
329
|
+
if (!res.body) {
|
|
330
|
+
return new Response("No response body", {
|
|
331
|
+
status: 500
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// If no afterResponse hook, pass through directly
|
|
335
|
+
if (!afterResponse) {
|
|
336
|
+
return new Response(res.body, {
|
|
337
|
+
status: res.status,
|
|
338
|
+
statusText: res.statusText,
|
|
339
|
+
headers: {
|
|
340
|
+
"content-type": res.headers.get("content-type") || "text/event-stream"
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Transform the stream to call afterResponse on finish
|
|
345
|
+
const transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);
|
|
346
|
+
return new Response(transformedStream, {
|
|
347
|
+
status: res.status,
|
|
348
|
+
statusText: res.statusText,
|
|
349
|
+
headers: {
|
|
350
|
+
"content-type": res.headers.get("content-type") || "text/event-stream"
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// For non-streaming responses, parse and optionally transform
|
|
355
|
+
let responseBody = yield res.json();
|
|
356
|
+
if (afterResponse) {
|
|
357
|
+
responseBody = yield afterResponse({
|
|
358
|
+
request: requestBody,
|
|
359
|
+
response: responseBody
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return new Response(JSON.stringify(responseBody), {
|
|
363
|
+
status: res.status,
|
|
364
|
+
statusText: res.statusText,
|
|
365
|
+
headers: {
|
|
366
|
+
"content-type": "application/json"
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
} catch (unused) {
|
|
370
|
+
return new Response("Error proxying request to AI Gateway", {
|
|
371
|
+
status: 500
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
})();
|
|
375
|
+
handler.GET = handler;
|
|
376
|
+
handler.POST = handler;
|
|
377
|
+
handler.PUT = handler;
|
|
378
|
+
handler.DELETE = handler;
|
|
379
|
+
handler.PATCH = handler;
|
|
380
|
+
return handler;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
exports.StreamContentAggregator = StreamContentAggregator;
|
|
384
|
+
exports.createGatewayProxy = createGatewayProxy;
|
|
385
|
+
exports.createStreamTransformer = createStreamTransformer;
|
|
386
|
+
exports.getGatewayAuthToken = getGatewayAuthToken;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { LanguageModelV3CallOptions, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3Usage, SharedV3Warning, SharedV3ProviderMetadata, LanguageModelV3StreamPart } from '@ai-sdk/provider';
|
|
2
|
+
|
|
3
|
+
type GatewayAuthToken = {
|
|
4
|
+
token: string;
|
|
5
|
+
authMethod: "api-key" | "oidc";
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Get authentication token for the AI Gateway.
|
|
9
|
+
* Tries API key first (AI_GATEWAY_API_KEY env var), then falls back to Vercel OIDC.
|
|
10
|
+
*/
|
|
11
|
+
declare function getGatewayAuthToken(): Promise<GatewayAuthToken | null>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The response body from the AI Gateway for non-streaming requests.
|
|
15
|
+
* For streaming requests, this is aggregated from the stream events.
|
|
16
|
+
*/
|
|
17
|
+
type GatewayResponse = {
|
|
18
|
+
content: LanguageModelV3Content[];
|
|
19
|
+
finishReason: LanguageModelV3FinishReason;
|
|
20
|
+
usage: LanguageModelV3Usage;
|
|
21
|
+
warnings: SharedV3Warning[];
|
|
22
|
+
providerMetadata?: SharedV3ProviderMetadata;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Error response from the AI Gateway.
|
|
26
|
+
*/
|
|
27
|
+
type GatewayError = {
|
|
28
|
+
error: {
|
|
29
|
+
message: string;
|
|
30
|
+
type: string;
|
|
31
|
+
param?: unknown;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Options for creating a gateway proxy.
|
|
36
|
+
*/
|
|
37
|
+
type CreateGatewayProxyOptions = {
|
|
38
|
+
/** Base URL for the AI Gateway. Defaults to Vercel's gateway. */
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
/** Additional headers to send with requests. */
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Called before the request is sent to the gateway.
|
|
44
|
+
* Allows modification of the request body.
|
|
45
|
+
*/
|
|
46
|
+
beforeRequest?: (context: {
|
|
47
|
+
request: LanguageModelV3CallOptions;
|
|
48
|
+
}) => LanguageModelV3CallOptions | Promise<LanguageModelV3CallOptions>;
|
|
49
|
+
/**
|
|
50
|
+
* Called after a successful response is received from the gateway.
|
|
51
|
+
* Allows modification of the response body.
|
|
52
|
+
*
|
|
53
|
+
* For streaming responses, this is called when the stream completes with
|
|
54
|
+
* the aggregated response. The content has already been streamed, but you
|
|
55
|
+
* can modify the finish event metadata (usage, finishReason, providerMetadata).
|
|
56
|
+
*/
|
|
57
|
+
afterResponse?: (context: {
|
|
58
|
+
request: LanguageModelV3CallOptions;
|
|
59
|
+
response: GatewayResponse;
|
|
60
|
+
}) => GatewayResponse | Promise<GatewayResponse>;
|
|
61
|
+
/**
|
|
62
|
+
* Called when the gateway returns an error response.
|
|
63
|
+
* Allows custom error handling or modification.
|
|
64
|
+
*/
|
|
65
|
+
onError?: (context: {
|
|
66
|
+
request: LanguageModelV3CallOptions;
|
|
67
|
+
error: GatewayError;
|
|
68
|
+
status: number;
|
|
69
|
+
}) => GatewayError | Response | Promise<GatewayError | Response>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Next.js route handler function signature for the gateway proxy.
|
|
73
|
+
*/
|
|
74
|
+
type CreateGatewayProxyFn = (request: Request, context: {
|
|
75
|
+
params: Promise<{
|
|
76
|
+
segments?: string[];
|
|
77
|
+
}>;
|
|
78
|
+
}) => Promise<Response>;
|
|
79
|
+
/**
|
|
80
|
+
* Result of createGatewayProxy - a handler function with HTTP method exports.
|
|
81
|
+
*/
|
|
82
|
+
type CreateGatewayProxyResult = CreateGatewayProxyFn & {
|
|
83
|
+
GET: CreateGatewayProxyFn;
|
|
84
|
+
POST: CreateGatewayProxyFn;
|
|
85
|
+
PUT: CreateGatewayProxyFn;
|
|
86
|
+
DELETE: CreateGatewayProxyFn;
|
|
87
|
+
PATCH: CreateGatewayProxyFn;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a proxy handler for the Vercel AI Gateway.
|
|
92
|
+
*
|
|
93
|
+
* Uses AI SDK V3 Language Model.
|
|
94
|
+
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
95
|
+
*/
|
|
96
|
+
declare const createGatewayProxy: ({ baseUrl, headers, beforeRequest, afterResponse, onError, }?: CreateGatewayProxyOptions) => CreateGatewayProxyResult;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Aggregator for collecting stream content.
|
|
100
|
+
* Tracks text deltas, reasoning, tool calls, and other content to build the final GatewayResponse.
|
|
101
|
+
*
|
|
102
|
+
* Uses AI SDK V3 protocol types.
|
|
103
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
104
|
+
*/
|
|
105
|
+
declare class StreamContentAggregator {
|
|
106
|
+
private content;
|
|
107
|
+
private warnings;
|
|
108
|
+
private textContent;
|
|
109
|
+
private reasoningContent;
|
|
110
|
+
private finishReason;
|
|
111
|
+
private usage;
|
|
112
|
+
private providerMetadata;
|
|
113
|
+
/**
|
|
114
|
+
* Process a stream part and update aggregated state.
|
|
115
|
+
* Uses LanguageModelV3StreamPart type from @ai-sdk/provider.
|
|
116
|
+
*
|
|
117
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
118
|
+
*/
|
|
119
|
+
process(part: LanguageModelV3StreamPart): void;
|
|
120
|
+
/**
|
|
121
|
+
* Get the aggregated response.
|
|
122
|
+
*/
|
|
123
|
+
getResponse(): GatewayResponse;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Creates a transform stream that:
|
|
127
|
+
* 1. Passes through all SSE events immediately
|
|
128
|
+
* 2. Aggregates content using StreamContentAggregator
|
|
129
|
+
* 3. Calls afterResponse when finish event is received
|
|
130
|
+
* 4. Modifies the finish event based on afterResponse result
|
|
131
|
+
*
|
|
132
|
+
* Uses EventSourceParserStream from @ai-sdk/provider-utils for SSE parsing.
|
|
133
|
+
* EventSourceParserStream is re-exported from the eventsource-parser package.
|
|
134
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider-utils/src/parse-json-event-stream.ts
|
|
135
|
+
* @see https://github.com/rexxars/eventsource-parser
|
|
136
|
+
*/
|
|
137
|
+
declare function createStreamTransformer(body: ReadableStream<Uint8Array>, requestBody: LanguageModelV3CallOptions, afterResponse: NonNullable<CreateGatewayProxyOptions["afterResponse"]>): ReadableStream<Uint8Array>;
|
|
138
|
+
|
|
139
|
+
export { StreamContentAggregator, createGatewayProxy, createStreamTransformer, getGatewayAuthToken };
|
|
140
|
+
export type { CreateGatewayProxyFn, CreateGatewayProxyOptions, CreateGatewayProxyResult, GatewayAuthToken, GatewayError, GatewayResponse };
|
|
141
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../src/auth.ts","../src/types.ts","../src/proxy.ts","../src/stream.ts"],"sourcesContent":["import { getVercelOidcToken } from \"@vercel/oidc\";\n\nexport type GatewayAuthToken = {\n\ttoken: string;\n\tauthMethod: \"api-key\" | \"oidc\";\n};\n\n/**\n * Get authentication token for the AI Gateway.\n * Tries API key first (AI_GATEWAY_API_KEY env var), then falls back to Vercel OIDC.\n */\nexport async function getGatewayAuthToken(): Promise<GatewayAuthToken | null> {\n\tconst apiKey = process.env.AI_GATEWAY_API_KEY;\n\tif (apiKey) {\n\t\treturn { token: apiKey, authMethod: \"api-key\" };\n\t}\n\n\ttry {\n\t\tconst oidcToken = await getVercelOidcToken();\n\t\treturn { token: oidcToken, authMethod: \"oidc\" };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type {\n\tLanguageModelV3CallOptions,\n\tLanguageModelV3Content,\n\tLanguageModelV3FinishReason,\n\tLanguageModelV3Usage,\n\tSharedV3ProviderMetadata,\n\tSharedV3Warning,\n} from \"@ai-sdk/provider\";\n\n/**\n * The response body from the AI Gateway for non-streaming requests.\n * For streaming requests, this is aggregated from the stream events.\n */\nexport type GatewayResponse = {\n\tcontent: LanguageModelV3Content[];\n\tfinishReason: LanguageModelV3FinishReason;\n\tusage: LanguageModelV3Usage;\n\twarnings: SharedV3Warning[];\n\tproviderMetadata?: SharedV3ProviderMetadata;\n};\n\n/**\n * Error response from the AI Gateway.\n */\nexport type GatewayError = {\n\terror: {\n\t\tmessage: string;\n\t\ttype: string;\n\t\tparam?: unknown;\n\t};\n};\n\n/**\n * Options for creating a gateway proxy.\n */\nexport type CreateGatewayProxyOptions = {\n\t/** Base URL for the AI Gateway. Defaults to Vercel's gateway. */\n\tbaseUrl?: string;\n\t/** Additional headers to send with requests. */\n\theaders?: Record<string, string>;\n\t/**\n\t * Called before the request is sent to the gateway.\n\t * Allows modification of the request body.\n\t */\n\tbeforeRequest?: (context: {\n\t\trequest: LanguageModelV3CallOptions;\n\t}) => LanguageModelV3CallOptions | Promise<LanguageModelV3CallOptions>;\n\t/**\n\t * Called after a successful response is received from the gateway.\n\t * Allows modification of the response body.\n\t *\n\t * For streaming responses, this is called when the stream completes with\n\t * the aggregated response. The content has already been streamed, but you\n\t * can modify the finish event metadata (usage, finishReason, providerMetadata).\n\t */\n\tafterResponse?: (context: {\n\t\trequest: LanguageModelV3CallOptions;\n\t\tresponse: GatewayResponse;\n\t}) => GatewayResponse | Promise<GatewayResponse>;\n\t/**\n\t * Called when the gateway returns an error response.\n\t * Allows custom error handling or modification.\n\t */\n\tonError?: (context: {\n\t\trequest: LanguageModelV3CallOptions;\n\t\terror: GatewayError;\n\t\tstatus: number;\n\t}) => GatewayError | Response | Promise<GatewayError | Response>;\n};\n\n/**\n * Next.js route handler function signature for the gateway proxy.\n */\nexport type CreateGatewayProxyFn = (\n\trequest: Request,\n\tcontext: { params: Promise<{ segments?: string[] }> },\n) => Promise<Response>;\n\n/**\n * Result of createGatewayProxy - a handler function with HTTP method exports.\n */\nexport type CreateGatewayProxyResult = CreateGatewayProxyFn & {\n\tGET: CreateGatewayProxyFn;\n\tPOST: CreateGatewayProxyFn;\n\tPUT: CreateGatewayProxyFn;\n\tDELETE: CreateGatewayProxyFn;\n\tPATCH: CreateGatewayProxyFn;\n};\n","import type { LanguageModelV3CallOptions } from \"@ai-sdk/provider\";\nimport { getGatewayAuthToken } from \"./auth\";\nimport { createStreamTransformer } from \"./stream\";\nimport type {\n\tCreateGatewayProxyOptions,\n\tCreateGatewayProxyResult,\n\tGatewayError,\n\tGatewayResponse,\n} from \"./types\";\n\n/**\n * Creates a proxy handler for the Vercel AI Gateway.\n *\n * Uses AI SDK V3 Language Model.\n * @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts\n */\nexport const createGatewayProxy = ({\n\tbaseUrl = \"https://ai-gateway.vercel.sh/v1/ai\",\n\theaders,\n\tbeforeRequest,\n\tafterResponse,\n\tonError,\n}: CreateGatewayProxyOptions = {}): CreateGatewayProxyResult => {\n\tconst handler = async (\n\t\trequest: Request,\n\t\t{ params }: { params: Promise<{ segments?: string[] }> },\n\t) => {\n\t\tconst auth = await getGatewayAuthToken();\n\t\tif (!auth) {\n\t\t\treturn new Response(\"AI Gateway authentication not configured\", {\n\t\t\t\tstatus: 500,\n\t\t\t});\n\t\t}\n\n\t\tconst { segments } = await params;\n\t\tconst path = segments?.join(\"/\") ?? \"\";\n\t\tconst searchParams = new URL(request.url).searchParams;\n\t\tconst upstream = `${baseUrl}/${path}${searchParams.toString() ? `?${searchParams.toString()}` : \"\"}`;\n\n\t\tlet requestBody: LanguageModelV3CallOptions;\n\t\ttry {\n\t\t\trequestBody = await request.clone().json();\n\t\t} catch {\n\t\t\treturn new Response(\"Invalid JSON body\", { status: 400 });\n\t\t}\n\n\t\t// Apply beforeRequest hook\n\t\tif (beforeRequest) {\n\t\t\trequestBody = await beforeRequest({ request: requestBody });\n\t\t}\n\n\t\tconst isStreaming = request.headers.get(\"ai-language-model-streaming\") === \"true\";\n\n\t\ttry {\n\t\t\tconst res = await fetch(upstream, {\n\t\t\t\tmethod: request.method,\n\t\t\t\tbody: JSON.stringify(requestBody),\n\t\t\t\tduplex: \"half\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t\t...headers,\n\t\t\t\t\tauthorization: `Bearer ${auth.token}`,\n\t\t\t\t\t\"ai-gateway-auth-method\": auth.authMethod,\n\t\t\t\t\t\"ai-gateway-protocol-version\": \"0.0.1\",\n\t\t\t\t\t\"ai-language-model-id\": request.headers.get(\"ai-language-model-id\") ?? \"\",\n\t\t\t\t\t\"ai-language-model-streaming\": isStreaming ? \"true\" : \"false\",\n\t\t\t\t},\n\t\t\t} as RequestInit);\n\n\t\t\t// Handle error responses\n\t\t\tif (!res.ok) {\n\t\t\t\tlet errorBody: GatewayError;\n\t\t\t\ttry {\n\t\t\t\t\terrorBody = await res.json();\n\t\t\t\t} catch {\n\t\t\t\t\terrorBody = {\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tmessage: res.statusText || \"Unknown error\",\n\t\t\t\t\t\t\ttype: \"gateway_error\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tif (onError) {\n\t\t\t\t\tconst result = await onError({\n\t\t\t\t\t\trequest: requestBody,\n\t\t\t\t\t\terror: errorBody,\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t});\n\n\t\t\t\t\t// If onError returns a Response, use it directly\n\t\t\t\t\tif (result instanceof Response) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Otherwise, return the modified error\n\t\t\t\t\treturn new Response(JSON.stringify(result), {\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tstatusText: res.statusText,\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn new Response(JSON.stringify(errorBody), {\n\t\t\t\t\tstatus: res.status,\n\t\t\t\t\tstatusText: res.statusText,\n\t\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// For streaming responses\n\t\t\tif (isStreaming) {\n\t\t\t\tif (!res.body) {\n\t\t\t\t\treturn new Response(\"No response body\", { status: 500 });\n\t\t\t\t}\n\n\t\t\t\t// If no afterResponse hook, pass through directly\n\t\t\t\tif (!afterResponse) {\n\t\t\t\t\treturn new Response(res.body, {\n\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\tstatusText: res.statusText,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\"content-type\": res.headers.get(\"content-type\") || \"text/event-stream\",\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Transform the stream to call afterResponse on finish\n\t\t\t\tconst transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);\n\n\t\t\t\treturn new Response(transformedStream, {\n\t\t\t\t\tstatus: res.status,\n\t\t\t\t\tstatusText: res.statusText,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"content-type\": res.headers.get(\"content-type\") || \"text/event-stream\",\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// For non-streaming responses, parse and optionally transform\n\t\t\tlet responseBody: GatewayResponse = await res.json();\n\n\t\t\tif (afterResponse) {\n\t\t\t\tresponseBody = await afterResponse({\n\t\t\t\t\trequest: requestBody,\n\t\t\t\t\tresponse: responseBody,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn new Response(JSON.stringify(responseBody), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tstatusText: res.statusText,\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t});\n\t\t} catch {\n\t\t\treturn new Response(\"Error proxying request to AI Gateway\", {\n\t\t\t\tstatus: 500,\n\t\t\t});\n\t\t}\n\t};\n\n\thandler.GET = handler;\n\thandler.POST = handler;\n\thandler.PUT = handler;\n\thandler.DELETE = handler;\n\thandler.PATCH = handler;\n\n\treturn handler;\n};\n","import type {\n\tLanguageModelV3CallOptions,\n\tLanguageModelV3Content,\n\tLanguageModelV3FinishReason,\n\tLanguageModelV3StreamPart,\n\tLanguageModelV3Usage,\n\tSharedV3ProviderMetadata,\n\tSharedV3Warning,\n} from \"@ai-sdk/provider\";\nimport { EventSourceParserStream } from \"@ai-sdk/provider-utils\";\nimport type { CreateGatewayProxyOptions, GatewayResponse } from \"./types\";\n\n/**\n * Aggregator for collecting stream content.\n * Tracks text deltas, reasoning, tool calls, and other content to build the final GatewayResponse.\n *\n * Uses AI SDK V3 protocol types.\n * @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts\n */\nexport class StreamContentAggregator {\n\tprivate content: LanguageModelV3Content[] = [];\n\tprivate warnings: SharedV3Warning[] = [];\n\n\t// Text content being built (keyed by id)\n\tprivate textContent = new Map<string, { type: \"text\"; text: string }>();\n\n\t// Reasoning content being built (keyed by id)\n\tprivate reasoningContent = new Map<string, { type: \"reasoning\"; text: string }>();\n\n\tprivate finishReason: LanguageModelV3FinishReason = {\n\t\tunified: \"stop\",\n\t\traw: undefined,\n\t};\n\n\tprivate usage: LanguageModelV3Usage = {\n\t\tinputTokens: {\n\t\t\ttotal: undefined,\n\t\t\tnoCache: undefined,\n\t\t\tcacheRead: undefined,\n\t\t\tcacheWrite: undefined,\n\t\t},\n\t\toutputTokens: {\n\t\t\ttotal: undefined,\n\t\t\ttext: undefined,\n\t\t\treasoning: undefined,\n\t\t},\n\t};\n\n\tprivate providerMetadata: SharedV3ProviderMetadata | undefined;\n\n\t/**\n\t * Process a stream part and update aggregated state.\n\t * Uses LanguageModelV3StreamPart type from @ai-sdk/provider.\n\t *\n\t * @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts\n\t */\n\tprocess(part: LanguageModelV3StreamPart): void {\n\t\tswitch (part.type) {\n\t\t\tcase \"stream-start\":\n\t\t\t\tthis.warnings.push(...part.warnings);\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-start\":\n\t\t\t\tthis.textContent.set(part.id, { type: \"text\", text: \"\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-delta\":\n\t\t\t\t{\n\t\t\t\t\tconst text = this.textContent.get(part.id);\n\t\t\t\t\tif (text) {\n\t\t\t\t\t\ttext.text += part.delta;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"text-end\":\n\t\t\t\t{\n\t\t\t\t\tconst text = this.textContent.get(part.id);\n\t\t\t\t\tif (text) {\n\t\t\t\t\t\tthis.content.push(text);\n\t\t\t\t\t\tthis.textContent.delete(part.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"reasoning-start\":\n\t\t\t\tthis.reasoningContent.set(part.id, { type: \"reasoning\", text: \"\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"reasoning-delta\":\n\t\t\t\t{\n\t\t\t\t\tconst reasoning = this.reasoningContent.get(part.id);\n\t\t\t\t\tif (reasoning) {\n\t\t\t\t\t\treasoning.text += part.delta;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"reasoning-end\":\n\t\t\t\t{\n\t\t\t\t\tconst reasoning = this.reasoningContent.get(part.id);\n\t\t\t\t\tif (reasoning) {\n\t\t\t\t\t\tthis.content.push(reasoning);\n\t\t\t\t\t\tthis.reasoningContent.delete(part.id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-input-start\":\n\t\t\tcase \"tool-input-delta\":\n\t\t\tcase \"tool-input-end\":\n\t\t\t\t// Tool input is streamed incrementally but we wait for the complete tool-call event\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-call\":\n\t\t\t\tthis.content.push(part);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-result\":\n\t\t\t\tthis.content.push(part);\n\t\t\t\tbreak;\n\n\t\t\tcase \"tool-approval-request\":\n\t\t\t\tthis.content.push(part);\n\t\t\t\tbreak;\n\n\t\t\tcase \"file\":\n\t\t\t\tthis.content.push(part);\n\t\t\t\tbreak;\n\n\t\t\tcase \"source\":\n\t\t\t\tthis.content.push(part);\n\t\t\t\tbreak;\n\n\t\t\tcase \"finish\":\n\t\t\t\t// Finalize any remaining content\n\t\t\t\tfor (const [, text] of this.textContent) {\n\t\t\t\t\tthis.content.push(text);\n\t\t\t\t}\n\t\t\t\tthis.textContent.clear();\n\n\t\t\t\tfor (const [, reasoning] of this.reasoningContent) {\n\t\t\t\t\tthis.content.push(reasoning);\n\t\t\t\t}\n\t\t\t\tthis.reasoningContent.clear();\n\n\t\t\t\tthis.finishReason = part.finishReason;\n\t\t\t\tthis.usage = part.usage;\n\t\t\t\tthis.providerMetadata = part.providerMetadata;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/**\n\t * Get the aggregated response.\n\t */\n\tgetResponse(): GatewayResponse {\n\t\treturn {\n\t\t\tcontent: this.content,\n\t\t\tfinishReason: this.finishReason,\n\t\t\tusage: this.usage,\n\t\t\twarnings: this.warnings,\n\t\t\tproviderMetadata: this.providerMetadata,\n\t\t};\n\t}\n}\n\n/**\n * Creates a transform stream that:\n * 1. Passes through all SSE events immediately\n * 2. Aggregates content using StreamContentAggregator\n * 3. Calls afterResponse when finish event is received\n * 4. Modifies the finish event based on afterResponse result\n *\n * Uses EventSourceParserStream from @ai-sdk/provider-utils for SSE parsing.\n * EventSourceParserStream is re-exported from the eventsource-parser package.\n * @see https://github.com/vercel/ai/blob/main/packages/provider-utils/src/parse-json-event-stream.ts\n * @see https://github.com/rexxars/eventsource-parser\n */\nexport function createStreamTransformer(\n\tbody: ReadableStream<Uint8Array>,\n\trequestBody: LanguageModelV3CallOptions,\n\tafterResponse: NonNullable<CreateGatewayProxyOptions[\"afterResponse\"]>,\n): ReadableStream<Uint8Array> {\n\tconst encoder = new TextEncoder();\n\tconst aggregator = new StreamContentAggregator();\n\n\t// Use EventSourceParserStream for SSE parsing (re-exported from eventsource-parser via @ai-sdk/provider-utils)\n\tconst eventStream = body\n\t\t.pipeThrough(new TextDecoderStream() as TransformStream<Uint8Array, string>)\n\t\t.pipeThrough(new EventSourceParserStream());\n\n\treturn new ReadableStream({\n\t\tasync start(controller) {\n\t\t\tconst reader = eventStream.getReader();\n\n\t\t\ttry {\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\t\tif (done) break;\n\n\t\t\t\t\tconst { data } = value;\n\n\t\t\t\t\t// Handle DONE marker (end of stream)\n\t\t\t\t\tif (data === \"DONE\" || data === \"[DONE]\") {\n\t\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${data}\\n\\n`));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse the JSON event data\n\t\t\t\t\tlet parsed: LanguageModelV3StreamPart;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tparsed = JSON.parse(data);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Not valid JSON, pass through as-is\n\t\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${data}\\n\\n`));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Track content for aggregation\n\t\t\t\t\taggregator.process(parsed);\n\n\t\t\t\t\t// Handle finish event specially - call afterResponse hook\n\t\t\t\t\tif (parsed.type === \"finish\") {\n\t\t\t\t\t\tconst modifiedResponse = await afterResponse({\n\t\t\t\t\t\t\trequest: requestBody,\n\t\t\t\t\t\t\tresponse: aggregator.getResponse(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Emit modified finish event\n\t\t\t\t\t\tconst modifiedFinish: LanguageModelV3StreamPart = {\n\t\t\t\t\t\t\ttype: \"finish\",\n\t\t\t\t\t\t\tfinishReason: modifiedResponse.finishReason,\n\t\t\t\t\t\t\tusage: modifiedResponse.usage,\n\t\t\t\t\t\t\tproviderMetadata: modifiedResponse.providerMetadata,\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${JSON.stringify(modifiedFinish)}\\n\\n`));\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pass through event unchanged\n\t\t\t\t\tcontroller.enqueue(encoder.encode(`data: ${data}\\n\\n`));\n\t\t\t\t}\n\n\t\t\t\tcontroller.close();\n\t\t\t} catch (error) {\n\t\t\t\tcontroller.error(error);\n\t\t\t} finally {\n\t\t\t\treader.releaseLock();\n\t\t\t}\n\t\t},\n\t});\n}\n"],"names":[],"mappings":";;AAAO,KAAA,gBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,mBAAA,IAAA,OAAA,CAAA,gBAAA;;ACPP;AACA;AACA;AACA;AACO,KAAA,eAAA;AACP,aAAA,sBAAA;AACA,kBAAA,2BAAA;AACA,WAAA,oBAAA;AACA,cAAA,eAAA;AACA,uBAAA,wBAAA;AACA;AACA;AACA;AACA;AACO,KAAA,YAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,KAAA,yBAAA;AACP;AACA;AACA;AACA,cAAA,MAAA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,0BAAA;AACA,UAAA,0BAAA,GAAA,OAAA,CAAA,0BAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,0BAAA;AACA,kBAAA,eAAA;AACA,UAAA,eAAA,GAAA,OAAA,CAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAA,0BAAA;AACA,eAAA,YAAA;AACA;AACA,UAAA,YAAA,GAAA,QAAA,GAAA,OAAA,CAAA,YAAA,GAAA,QAAA;AACA;AACA;AACA;AACA;AACO,KAAA,oBAAA,aAAA,OAAA;AACP,YAAA,OAAA;AACA;AACA;AACA,MAAA,OAAA,CAAA,QAAA;AACA;AACA;AACA;AACO,KAAA,wBAAA,GAAA,oBAAA;AACP,SAAA,oBAAA;AACA,UAAA,oBAAA;AACA,SAAA,oBAAA;AACA,YAAA,oBAAA;AACA,WAAA,oBAAA;AACA;;AC3EA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,kBAAA,kEAAA,yBAAA,KAAA,wBAAA;;ACLP;AACA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,uBAAA;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kBAAA,yBAAA;AACA;AACA;AACA;AACA,mBAAA,eAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,iBAAA,uBAAA,OAAA,cAAA,CAAA,UAAA,gBAAA,0BAAA,iBAAA,WAAA,CAAA,yBAAA,qBAAA,cAAA,CAAA,UAAA;;;;"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { _ } from '@swc/helpers/_/_async_to_generator';
|
|
2
|
+
import { getVercelOidcToken } from '@vercel/oidc';
|
|
3
|
+
import { _ as _$1 } from '@swc/helpers/_/_extends';
|
|
4
|
+
import { EventSourceParserStream } from '@ai-sdk/provider-utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get authentication token for the AI Gateway.
|
|
8
|
+
* Tries API key first (AI_GATEWAY_API_KEY env var), then falls back to Vercel OIDC.
|
|
9
|
+
*/ function getGatewayAuthToken() {
|
|
10
|
+
return _(function*() {
|
|
11
|
+
const apiKey = process.env.AI_GATEWAY_API_KEY;
|
|
12
|
+
if (apiKey) {
|
|
13
|
+
return {
|
|
14
|
+
token: apiKey,
|
|
15
|
+
authMethod: "api-key"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const oidcToken = yield getVercelOidcToken();
|
|
20
|
+
return {
|
|
21
|
+
token: oidcToken,
|
|
22
|
+
authMethod: "oidc"
|
|
23
|
+
};
|
|
24
|
+
} catch (unused) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Aggregator for collecting stream content.
|
|
32
|
+
* Tracks text deltas, reasoning, tool calls, and other content to build the final GatewayResponse.
|
|
33
|
+
*
|
|
34
|
+
* Uses AI SDK V3 protocol types.
|
|
35
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
36
|
+
*/ class StreamContentAggregator {
|
|
37
|
+
/**
|
|
38
|
+
* Process a stream part and update aggregated state.
|
|
39
|
+
* Uses LanguageModelV3StreamPart type from @ai-sdk/provider.
|
|
40
|
+
*
|
|
41
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider/src/language-model/v3/language-model-v3-stream-part.ts
|
|
42
|
+
*/ process(part) {
|
|
43
|
+
switch(part.type){
|
|
44
|
+
case "stream-start":
|
|
45
|
+
this.warnings.push(...part.warnings);
|
|
46
|
+
break;
|
|
47
|
+
case "text-start":
|
|
48
|
+
this.textContent.set(part.id, {
|
|
49
|
+
type: "text",
|
|
50
|
+
text: ""
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
case "text-delta":
|
|
54
|
+
{
|
|
55
|
+
const text = this.textContent.get(part.id);
|
|
56
|
+
if (text) {
|
|
57
|
+
text.text += part.delta;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case "text-end":
|
|
62
|
+
{
|
|
63
|
+
const text = this.textContent.get(part.id);
|
|
64
|
+
if (text) {
|
|
65
|
+
this.content.push(text);
|
|
66
|
+
this.textContent.delete(part.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case "reasoning-start":
|
|
71
|
+
this.reasoningContent.set(part.id, {
|
|
72
|
+
type: "reasoning",
|
|
73
|
+
text: ""
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
case "reasoning-delta":
|
|
77
|
+
{
|
|
78
|
+
const reasoning = this.reasoningContent.get(part.id);
|
|
79
|
+
if (reasoning) {
|
|
80
|
+
reasoning.text += part.delta;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "reasoning-end":
|
|
85
|
+
{
|
|
86
|
+
const reasoning = this.reasoningContent.get(part.id);
|
|
87
|
+
if (reasoning) {
|
|
88
|
+
this.content.push(reasoning);
|
|
89
|
+
this.reasoningContent.delete(part.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case "tool-input-start":
|
|
94
|
+
case "tool-input-delta":
|
|
95
|
+
case "tool-input-end":
|
|
96
|
+
break;
|
|
97
|
+
case "tool-call":
|
|
98
|
+
this.content.push(part);
|
|
99
|
+
break;
|
|
100
|
+
case "tool-result":
|
|
101
|
+
this.content.push(part);
|
|
102
|
+
break;
|
|
103
|
+
case "tool-approval-request":
|
|
104
|
+
this.content.push(part);
|
|
105
|
+
break;
|
|
106
|
+
case "file":
|
|
107
|
+
this.content.push(part);
|
|
108
|
+
break;
|
|
109
|
+
case "source":
|
|
110
|
+
this.content.push(part);
|
|
111
|
+
break;
|
|
112
|
+
case "finish":
|
|
113
|
+
// Finalize any remaining content
|
|
114
|
+
for (const [, text] of this.textContent){
|
|
115
|
+
this.content.push(text);
|
|
116
|
+
}
|
|
117
|
+
this.textContent.clear();
|
|
118
|
+
for (const [, reasoning] of this.reasoningContent){
|
|
119
|
+
this.content.push(reasoning);
|
|
120
|
+
}
|
|
121
|
+
this.reasoningContent.clear();
|
|
122
|
+
this.finishReason = part.finishReason;
|
|
123
|
+
this.usage = part.usage;
|
|
124
|
+
this.providerMetadata = part.providerMetadata;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get the aggregated response.
|
|
130
|
+
*/ getResponse() {
|
|
131
|
+
return {
|
|
132
|
+
content: this.content,
|
|
133
|
+
finishReason: this.finishReason,
|
|
134
|
+
usage: this.usage,
|
|
135
|
+
warnings: this.warnings,
|
|
136
|
+
providerMetadata: this.providerMetadata
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
constructor(){
|
|
140
|
+
this.content = [];
|
|
141
|
+
this.warnings = [];
|
|
142
|
+
// Text content being built (keyed by id)
|
|
143
|
+
this.textContent = new Map();
|
|
144
|
+
// Reasoning content being built (keyed by id)
|
|
145
|
+
this.reasoningContent = new Map();
|
|
146
|
+
this.finishReason = {
|
|
147
|
+
unified: "stop",
|
|
148
|
+
raw: undefined
|
|
149
|
+
};
|
|
150
|
+
this.usage = {
|
|
151
|
+
inputTokens: {
|
|
152
|
+
total: undefined,
|
|
153
|
+
noCache: undefined,
|
|
154
|
+
cacheRead: undefined,
|
|
155
|
+
cacheWrite: undefined
|
|
156
|
+
},
|
|
157
|
+
outputTokens: {
|
|
158
|
+
total: undefined,
|
|
159
|
+
text: undefined,
|
|
160
|
+
reasoning: undefined
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Creates a transform stream that:
|
|
167
|
+
* 1. Passes through all SSE events immediately
|
|
168
|
+
* 2. Aggregates content using StreamContentAggregator
|
|
169
|
+
* 3. Calls afterResponse when finish event is received
|
|
170
|
+
* 4. Modifies the finish event based on afterResponse result
|
|
171
|
+
*
|
|
172
|
+
* Uses EventSourceParserStream from @ai-sdk/provider-utils for SSE parsing.
|
|
173
|
+
* EventSourceParserStream is re-exported from the eventsource-parser package.
|
|
174
|
+
* @see https://github.com/vercel/ai/blob/main/packages/provider-utils/src/parse-json-event-stream.ts
|
|
175
|
+
* @see https://github.com/rexxars/eventsource-parser
|
|
176
|
+
*/ function createStreamTransformer(body, requestBody, afterResponse) {
|
|
177
|
+
const encoder = new TextEncoder();
|
|
178
|
+
const aggregator = new StreamContentAggregator();
|
|
179
|
+
// Use EventSourceParserStream for SSE parsing (re-exported from eventsource-parser via @ai-sdk/provider-utils)
|
|
180
|
+
const eventStream = body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream());
|
|
181
|
+
return new ReadableStream({
|
|
182
|
+
start (controller) {
|
|
183
|
+
return _(function*() {
|
|
184
|
+
const reader = eventStream.getReader();
|
|
185
|
+
try {
|
|
186
|
+
while(true){
|
|
187
|
+
const { done, value } = yield reader.read();
|
|
188
|
+
if (done) break;
|
|
189
|
+
const { data } = value;
|
|
190
|
+
// Handle DONE marker (end of stream)
|
|
191
|
+
if (data === "DONE" || data === "[DONE]") {
|
|
192
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Parse the JSON event data
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(data);
|
|
199
|
+
} catch (unused) {
|
|
200
|
+
// Not valid JSON, pass through as-is
|
|
201
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Track content for aggregation
|
|
205
|
+
aggregator.process(parsed);
|
|
206
|
+
// Handle finish event specially - call afterResponse hook
|
|
207
|
+
if (parsed.type === "finish") {
|
|
208
|
+
const modifiedResponse = yield afterResponse({
|
|
209
|
+
request: requestBody,
|
|
210
|
+
response: aggregator.getResponse()
|
|
211
|
+
});
|
|
212
|
+
// Emit modified finish event
|
|
213
|
+
const modifiedFinish = {
|
|
214
|
+
type: "finish",
|
|
215
|
+
finishReason: modifiedResponse.finishReason,
|
|
216
|
+
usage: modifiedResponse.usage,
|
|
217
|
+
providerMetadata: modifiedResponse.providerMetadata
|
|
218
|
+
};
|
|
219
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(modifiedFinish)}\n\n`));
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Pass through event unchanged
|
|
223
|
+
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
|
|
224
|
+
}
|
|
225
|
+
controller.close();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
controller.error(error);
|
|
228
|
+
} finally{
|
|
229
|
+
reader.releaseLock();
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Creates a proxy handler for the Vercel AI Gateway.
|
|
238
|
+
*
|
|
239
|
+
* Uses AI SDK V3 Language Model.
|
|
240
|
+
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
241
|
+
*/ const createGatewayProxy = ({ baseUrl = "https://ai-gateway.vercel.sh/v1/ai", headers, beforeRequest, afterResponse, onError } = {})=>{
|
|
242
|
+
const handler = (request, { params })=>_(function*() {
|
|
243
|
+
var _ref;
|
|
244
|
+
const auth = yield getGatewayAuthToken();
|
|
245
|
+
if (!auth) {
|
|
246
|
+
return new Response("AI Gateway authentication not configured", {
|
|
247
|
+
status: 500
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const { segments } = yield params;
|
|
251
|
+
const path = (_ref = segments == null ? void 0 : segments.join("/")) != null ? _ref : "";
|
|
252
|
+
const searchParams = new URL(request.url).searchParams;
|
|
253
|
+
const upstream = `${baseUrl}/${path}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
|
|
254
|
+
let requestBody;
|
|
255
|
+
try {
|
|
256
|
+
requestBody = yield request.clone().json();
|
|
257
|
+
} catch (unused) {
|
|
258
|
+
return new Response("Invalid JSON body", {
|
|
259
|
+
status: 400
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Apply beforeRequest hook
|
|
263
|
+
if (beforeRequest) {
|
|
264
|
+
requestBody = yield beforeRequest({
|
|
265
|
+
request: requestBody
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const isStreaming = request.headers.get("ai-language-model-streaming") === "true";
|
|
269
|
+
try {
|
|
270
|
+
var _request_headers_get;
|
|
271
|
+
const res = yield fetch(upstream, {
|
|
272
|
+
method: request.method,
|
|
273
|
+
body: JSON.stringify(requestBody),
|
|
274
|
+
duplex: "half",
|
|
275
|
+
headers: _$1({
|
|
276
|
+
"content-type": "application/json"
|
|
277
|
+
}, headers, {
|
|
278
|
+
authorization: `Bearer ${auth.token}`,
|
|
279
|
+
"ai-gateway-auth-method": auth.authMethod,
|
|
280
|
+
"ai-gateway-protocol-version": "0.0.1",
|
|
281
|
+
"ai-language-model-id": (_request_headers_get = request.headers.get("ai-language-model-id")) != null ? _request_headers_get : "",
|
|
282
|
+
"ai-language-model-streaming": isStreaming ? "true" : "false"
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
// Handle error responses
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
let errorBody;
|
|
288
|
+
try {
|
|
289
|
+
errorBody = yield res.json();
|
|
290
|
+
} catch (unused) {
|
|
291
|
+
errorBody = {
|
|
292
|
+
error: {
|
|
293
|
+
message: res.statusText || "Unknown error",
|
|
294
|
+
type: "gateway_error"
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (onError) {
|
|
299
|
+
const result = yield onError({
|
|
300
|
+
request: requestBody,
|
|
301
|
+
error: errorBody,
|
|
302
|
+
status: res.status
|
|
303
|
+
});
|
|
304
|
+
// If onError returns a Response, use it directly
|
|
305
|
+
if (result instanceof Response) {
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
// Otherwise, return the modified error
|
|
309
|
+
return new Response(JSON.stringify(result), {
|
|
310
|
+
status: res.status,
|
|
311
|
+
statusText: res.statusText,
|
|
312
|
+
headers: {
|
|
313
|
+
"content-type": "application/json"
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return new Response(JSON.stringify(errorBody), {
|
|
318
|
+
status: res.status,
|
|
319
|
+
statusText: res.statusText,
|
|
320
|
+
headers: {
|
|
321
|
+
"content-type": "application/json"
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// For streaming responses
|
|
326
|
+
if (isStreaming) {
|
|
327
|
+
if (!res.body) {
|
|
328
|
+
return new Response("No response body", {
|
|
329
|
+
status: 500
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// If no afterResponse hook, pass through directly
|
|
333
|
+
if (!afterResponse) {
|
|
334
|
+
return new Response(res.body, {
|
|
335
|
+
status: res.status,
|
|
336
|
+
statusText: res.statusText,
|
|
337
|
+
headers: {
|
|
338
|
+
"content-type": res.headers.get("content-type") || "text/event-stream"
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// Transform the stream to call afterResponse on finish
|
|
343
|
+
const transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);
|
|
344
|
+
return new Response(transformedStream, {
|
|
345
|
+
status: res.status,
|
|
346
|
+
statusText: res.statusText,
|
|
347
|
+
headers: {
|
|
348
|
+
"content-type": res.headers.get("content-type") || "text/event-stream"
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// For non-streaming responses, parse and optionally transform
|
|
353
|
+
let responseBody = yield res.json();
|
|
354
|
+
if (afterResponse) {
|
|
355
|
+
responseBody = yield afterResponse({
|
|
356
|
+
request: requestBody,
|
|
357
|
+
response: responseBody
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return new Response(JSON.stringify(responseBody), {
|
|
361
|
+
status: res.status,
|
|
362
|
+
statusText: res.statusText,
|
|
363
|
+
headers: {
|
|
364
|
+
"content-type": "application/json"
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
} catch (unused) {
|
|
368
|
+
return new Response("Error proxying request to AI Gateway", {
|
|
369
|
+
status: 500
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
})();
|
|
373
|
+
handler.GET = handler;
|
|
374
|
+
handler.POST = handler;
|
|
375
|
+
handler.PUT = handler;
|
|
376
|
+
handler.DELETE = handler;
|
|
377
|
+
handler.PATCH = handler;
|
|
378
|
+
return handler;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export { StreamContentAggregator, createGatewayProxy, createStreamTransformer, getGatewayAuthToken };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-gateway-proxy",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@ai-sdk/provider": "3.0.0",
|
|
22
|
+
"@ai-sdk/provider-utils": "4.0.1",
|
|
23
|
+
"@vercel/oidc": "3.0.5"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@ai-sdk/gateway": "3.0.2",
|
|
27
|
+
"@types/node": "25.0.3",
|
|
28
|
+
"ai": "6.0.3",
|
|
29
|
+
"bunchee": "6.5.2",
|
|
30
|
+
"dotenv": "17.2.3",
|
|
31
|
+
"is-tree-shakable": "0.4.2",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"vitest": "4.0.16",
|
|
34
|
+
"@sferadev/tsconfig": "0.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "bunchee",
|
|
41
|
+
"dev": "bunchee --watch",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"tsc": "tsc --noEmit",
|
|
45
|
+
"check:tree-shake": "is-tree-shakable",
|
|
46
|
+
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
|
|
47
|
+
}
|
|
48
|
+
}
|