ai-gateway-proxy 0.0.1 → 0.1.0
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/README.md +62 -0
- package/dist/index.cjs +149 -94
- package/dist/index.d.ts +29 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +149 -94
- package/package.json +8 -2
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ai-gateway-proxy
|
|
2
|
+
|
|
3
|
+
A lightweight proxy handler for [Vercel AI Gateway](https://vercel.com/docs/ai-gateway).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Request/Response Hooks** - Transform requests and responses with `beforeRequest`, `afterResponse`, and `onError` hooks
|
|
8
|
+
- **Streaming Support** - Full SSE streaming with content aggregation using AI SDK V3 protocol
|
|
9
|
+
- **Framework Agnostic** - Works with Next.js, Hono, Express, and other frameworks
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install ai-gateway-proxy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Next.js App Router
|
|
20
|
+
|
|
21
|
+
Create a catch-all route at `app/api/ai/[...segments]/route.ts`:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createGatewayProxy } from "ai-gateway-proxy";
|
|
25
|
+
|
|
26
|
+
export const { GET, POST, PUT, DELETE, PATCH } = createGatewayProxy();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Hono / Other Frameworks
|
|
30
|
+
|
|
31
|
+
For frameworks without catch-all route support, use the `extractPath` option:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Hono } from "hono";
|
|
35
|
+
import { createGatewayProxy } from "ai-gateway-proxy";
|
|
36
|
+
|
|
37
|
+
const app = new Hono();
|
|
38
|
+
|
|
39
|
+
const proxy = createGatewayProxy({
|
|
40
|
+
extractPath: (request) => {
|
|
41
|
+
const url = new URL(request.url);
|
|
42
|
+
return url.pathname.replace(/^\/api\/ai\//, "");
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
app.all("/api/ai/*", (c) => proxy(c.req.raw));
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Authentication
|
|
50
|
+
|
|
51
|
+
The proxy automatically handles authentication using one of the following methods (in priority order):
|
|
52
|
+
|
|
53
|
+
1. **API Key** - Set `AI_GATEWAY_API_KEY` environment variable
|
|
54
|
+
2. **OIDC** - Automatic when deployed on Vercel
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
For full documentation including configuration options, hooks, and API reference, see the [documentation](https://sferadev.com/docs/packages/ai-gateway-proxy).
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -235,24 +235,159 @@ var providerUtils = require('@ai-sdk/provider-utils');
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
const DEFAULT_BASE_URL = "https://ai-gateway.vercel.sh/v1/ai";
|
|
239
|
+
const PROTOCOL_VERSION = "0.0.1";
|
|
240
|
+
/**
|
|
241
|
+
* Extracts the path from the request using either a custom extractor or route params.
|
|
242
|
+
*/ function extractPathFromRequest(request, context, options) {
|
|
243
|
+
return _async_to_generator._(function*() {
|
|
244
|
+
if (options.extractPath) {
|
|
245
|
+
return options.extractPath(request);
|
|
246
|
+
}
|
|
247
|
+
if (context == null ? void 0 : context.params) {
|
|
248
|
+
const params = yield context.params;
|
|
249
|
+
const segments = params[options.segmentsParam];
|
|
250
|
+
return Array.isArray(segments) ? segments.join("/") : segments != null ? segments : "";
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
})();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Builds the upstream URL from base URL, path, and query params.
|
|
257
|
+
*/ function buildUpstreamUrl(baseUrl, path, requestUrl) {
|
|
258
|
+
const searchParams = new URL(requestUrl).searchParams;
|
|
259
|
+
const queryString = searchParams.toString();
|
|
260
|
+
return `${baseUrl}/${path}${queryString ? `?${queryString}` : ""}`;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Builds the headers for the upstream request.
|
|
264
|
+
*/ function buildRequestHeaders(auth, isStreaming, modelId, customHeaders) {
|
|
265
|
+
return _extends._({
|
|
266
|
+
"content-type": "application/json"
|
|
267
|
+
}, customHeaders, {
|
|
268
|
+
authorization: `Bearer ${auth.token}`,
|
|
269
|
+
"ai-gateway-auth-method": auth.authMethod,
|
|
270
|
+
"ai-gateway-protocol-version": PROTOCOL_VERSION,
|
|
271
|
+
"ai-language-model-id": modelId != null ? modelId : "",
|
|
272
|
+
"ai-language-model-streaming": isStreaming ? "true" : "false"
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Parses the error response from the gateway.
|
|
277
|
+
*/ function parseErrorBody(res) {
|
|
278
|
+
return _async_to_generator._(function*() {
|
|
279
|
+
try {
|
|
280
|
+
return yield res.json();
|
|
281
|
+
} catch (unused) {
|
|
282
|
+
return {
|
|
283
|
+
error: {
|
|
284
|
+
message: res.statusText || "Unknown error",
|
|
285
|
+
type: "gateway_error"
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
})();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Creates an error response, optionally applying the onError hook.
|
|
293
|
+
*/ function createErrorResponse(res, errorBody, requestBody, onError) {
|
|
294
|
+
return _async_to_generator._(function*() {
|
|
295
|
+
if (onError) {
|
|
296
|
+
const result = yield onError({
|
|
297
|
+
request: requestBody,
|
|
298
|
+
error: errorBody,
|
|
299
|
+
status: res.status
|
|
300
|
+
});
|
|
301
|
+
if (result instanceof Response) {
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
return new Response(JSON.stringify(result), {
|
|
305
|
+
status: res.status,
|
|
306
|
+
statusText: res.statusText,
|
|
307
|
+
headers: {
|
|
308
|
+
"content-type": "application/json"
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return new Response(JSON.stringify(errorBody), {
|
|
313
|
+
status: res.status,
|
|
314
|
+
statusText: res.statusText,
|
|
315
|
+
headers: {
|
|
316
|
+
"content-type": "application/json"
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
})();
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Handles a streaming response from the gateway.
|
|
323
|
+
*/ function handleStreamingResponse(res, requestBody, afterResponse) {
|
|
324
|
+
if (!res.body) {
|
|
325
|
+
return new Response("No response body", {
|
|
326
|
+
status: 500
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const contentType = res.headers.get("content-type") || "text/event-stream";
|
|
330
|
+
// Pass through directly if no afterResponse hook
|
|
331
|
+
if (!afterResponse) {
|
|
332
|
+
return new Response(res.body, {
|
|
333
|
+
status: res.status,
|
|
334
|
+
statusText: res.statusText,
|
|
335
|
+
headers: {
|
|
336
|
+
"content-type": contentType
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
// Transform stream to call afterResponse on finish
|
|
341
|
+
const transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);
|
|
342
|
+
return new Response(transformedStream, {
|
|
343
|
+
status: res.status,
|
|
344
|
+
statusText: res.statusText,
|
|
345
|
+
headers: {
|
|
346
|
+
"content-type": contentType
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Handles a non-streaming JSON response from the gateway.
|
|
352
|
+
*/ function handleJsonResponse(res, requestBody, afterResponse) {
|
|
353
|
+
return _async_to_generator._(function*() {
|
|
354
|
+
let responseBody = yield res.json();
|
|
355
|
+
if (afterResponse) {
|
|
356
|
+
responseBody = yield afterResponse({
|
|
357
|
+
request: requestBody,
|
|
358
|
+
response: responseBody
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return new Response(JSON.stringify(responseBody), {
|
|
362
|
+
status: res.status,
|
|
363
|
+
statusText: res.statusText,
|
|
364
|
+
headers: {
|
|
365
|
+
"content-type": "application/json"
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
})();
|
|
369
|
+
}
|
|
238
370
|
/**
|
|
239
371
|
* Creates a proxy handler for the Vercel AI Gateway.
|
|
240
372
|
*
|
|
241
373
|
* Uses AI SDK V3 Language Model.
|
|
242
374
|
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
243
|
-
*/ const createGatewayProxy = ({ baseUrl = "
|
|
244
|
-
const handler = (request,
|
|
245
|
-
|
|
375
|
+
*/ const createGatewayProxy = ({ baseUrl = DEFAULT_BASE_URL, headers, segmentsParam = "segments", extractPath, beforeRequest, afterResponse, onError } = {})=>{
|
|
376
|
+
const handler = (request, context)=>_async_to_generator._(function*() {
|
|
377
|
+
// Authenticate with the gateway
|
|
246
378
|
const auth = yield getGatewayAuthToken();
|
|
247
379
|
if (!auth) {
|
|
248
380
|
return new Response("AI Gateway authentication not configured", {
|
|
249
381
|
status: 500
|
|
250
382
|
});
|
|
251
383
|
}
|
|
252
|
-
|
|
253
|
-
const path =
|
|
254
|
-
|
|
255
|
-
|
|
384
|
+
// Build upstream URL
|
|
385
|
+
const path = yield extractPathFromRequest(request, context, {
|
|
386
|
+
extractPath,
|
|
387
|
+
segmentsParam
|
|
388
|
+
});
|
|
389
|
+
const upstream = buildUpstreamUrl(baseUrl, path, request.url);
|
|
390
|
+
// Parse request body
|
|
256
391
|
let requestBody;
|
|
257
392
|
try {
|
|
258
393
|
requestBody = yield request.clone().json();
|
|
@@ -269,109 +404,29 @@ var providerUtils = require('@ai-sdk/provider-utils');
|
|
|
269
404
|
}
|
|
270
405
|
const isStreaming = request.headers.get("ai-language-model-streaming") === "true";
|
|
271
406
|
try {
|
|
272
|
-
var _request_headers_get;
|
|
273
407
|
const res = yield fetch(upstream, {
|
|
274
408
|
method: request.method,
|
|
275
409
|
body: JSON.stringify(requestBody),
|
|
276
410
|
duplex: "half",
|
|
277
|
-
headers:
|
|
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
|
-
})
|
|
411
|
+
headers: buildRequestHeaders(auth, isStreaming, request.headers.get("ai-language-model-id"), headers)
|
|
286
412
|
});
|
|
287
413
|
// Handle error responses
|
|
288
414
|
if (!res.ok) {
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
});
|
|
415
|
+
const errorBody = yield parseErrorBody(res);
|
|
416
|
+
return createErrorResponse(res, errorBody, requestBody, onError);
|
|
326
417
|
}
|
|
327
|
-
//
|
|
418
|
+
// Handle successful responses
|
|
328
419
|
if (isStreaming) {
|
|
329
|
-
|
|
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
|
-
});
|
|
420
|
+
return handleStreamingResponse(res, requestBody, afterResponse);
|
|
353
421
|
}
|
|
354
|
-
|
|
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
|
-
});
|
|
422
|
+
return handleJsonResponse(res, requestBody, afterResponse);
|
|
369
423
|
} catch (unused) {
|
|
370
424
|
return new Response("Error proxying request to AI Gateway", {
|
|
371
425
|
status: 500
|
|
372
426
|
});
|
|
373
427
|
}
|
|
374
428
|
})();
|
|
429
|
+
// Export handler for all HTTP methods
|
|
375
430
|
handler.GET = handler;
|
|
376
431
|
handler.POST = handler;
|
|
377
432
|
handler.PUT = handler;
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,30 @@ type CreateGatewayProxyOptions = {
|
|
|
39
39
|
baseUrl?: string;
|
|
40
40
|
/** Additional headers to send with requests. */
|
|
41
41
|
headers?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* The name of the catch-all parameter in the route.
|
|
44
|
+
* @default "segments"
|
|
45
|
+
* @example
|
|
46
|
+
* // For Next.js route `app/api/ai/[...path]/route.ts`
|
|
47
|
+
* createGatewayProxy({ segmentsParam: "path" })
|
|
48
|
+
*/
|
|
49
|
+
segmentsParam?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Custom function to extract the path from the request.
|
|
52
|
+
* Use this for frameworks without catch-all route support (e.g., Hono, Express).
|
|
53
|
+
*
|
|
54
|
+
* When provided, `segmentsParam` is ignored.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Hono with a base path
|
|
58
|
+
* createGatewayProxy({
|
|
59
|
+
* extractPath: (request) => {
|
|
60
|
+
* const url = new URL(request.url);
|
|
61
|
+
* return url.pathname.replace(/^\/api\/ai\//, "");
|
|
62
|
+
* }
|
|
63
|
+
* })
|
|
64
|
+
*/
|
|
65
|
+
extractPath?: (request: Request) => string | Promise<string>;
|
|
42
66
|
/**
|
|
43
67
|
* Called before the request is sent to the gateway.
|
|
44
68
|
* Allows modification of the request body.
|
|
@@ -69,12 +93,11 @@ type CreateGatewayProxyOptions = {
|
|
|
69
93
|
}) => GatewayError | Response | Promise<GatewayError | Response>;
|
|
70
94
|
};
|
|
71
95
|
/**
|
|
72
|
-
*
|
|
96
|
+
* Route handler function signature for the gateway proxy.
|
|
97
|
+
* Compatible with Next.js App Router and other frameworks.
|
|
73
98
|
*/
|
|
74
|
-
type CreateGatewayProxyFn = (request: Request, context
|
|
75
|
-
params: Promise<
|
|
76
|
-
segments?: string[];
|
|
77
|
-
}>;
|
|
99
|
+
type CreateGatewayProxyFn = (request: Request, context?: {
|
|
100
|
+
params: Promise<Record<string, string | string[] | undefined>>;
|
|
78
101
|
}) => Promise<Response>;
|
|
79
102
|
/**
|
|
80
103
|
* Result of createGatewayProxy - a handler function with HTTP method exports.
|
|
@@ -93,7 +116,7 @@ type CreateGatewayProxyResult = CreateGatewayProxyFn & {
|
|
|
93
116
|
* Uses AI SDK V3 Language Model.
|
|
94
117
|
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
95
118
|
*/
|
|
96
|
-
declare const createGatewayProxy: ({ baseUrl, headers, beforeRequest, afterResponse, onError, }?: CreateGatewayProxyOptions) => CreateGatewayProxyResult;
|
|
119
|
+
declare const createGatewayProxy: ({ baseUrl, headers, segmentsParam, extractPath, beforeRequest, afterResponse, onError, }?: CreateGatewayProxyOptions) => CreateGatewayProxyResult;
|
|
97
120
|
|
|
98
121
|
/**
|
|
99
122
|
* Aggregator for collecting stream content.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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;;;;"}
|
|
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 * The name of the catch-all parameter in the route.\n\t * @default \"segments\"\n\t * @example\n\t * // For Next.js route `app/api/ai/[...path]/route.ts`\n\t * createGatewayProxy({ segmentsParam: \"path\" })\n\t */\n\tsegmentsParam?: string;\n\t/**\n\t * Custom function to extract the path from the request.\n\t * Use this for frameworks without catch-all route support (e.g., Hono, Express).\n\t *\n\t * When provided, `segmentsParam` is ignored.\n\t *\n\t * @example\n\t * // Hono with a base path\n\t * createGatewayProxy({\n\t * extractPath: (request) => {\n\t * const url = new URL(request.url);\n\t * return url.pathname.replace(/^\\/api\\/ai\\//, \"\");\n\t * }\n\t * })\n\t */\n\textractPath?: (request: Request) => string | Promise<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 * Route handler function signature for the gateway proxy.\n * Compatible with Next.js App Router and other frameworks.\n */\nexport type CreateGatewayProxyFn = (\n\trequest: Request,\n\tcontext?: { params: Promise<Record<string, string | string[] | undefined>> },\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 { type GatewayAuthToken, getGatewayAuthToken } from \"./auth\";\nimport { createStreamTransformer } from \"./stream\";\nimport type {\n\tCreateGatewayProxyOptions,\n\tCreateGatewayProxyResult,\n\tGatewayError,\n\tGatewayResponse,\n} from \"./types\";\n\nconst DEFAULT_BASE_URL = \"https://ai-gateway.vercel.sh/v1/ai\";\nconst PROTOCOL_VERSION = \"0.0.1\";\n\n/**\n * Extracts the path from the request using either a custom extractor or route params.\n */\nasync function extractPathFromRequest(\n\trequest: Request,\n\tcontext: { params: Promise<Record<string, string | string[] | undefined>> } | undefined,\n\toptions: { extractPath?: CreateGatewayProxyOptions[\"extractPath\"]; segmentsParam: string },\n): Promise<string> {\n\tif (options.extractPath) {\n\t\treturn options.extractPath(request);\n\t}\n\n\tif (context?.params) {\n\t\tconst params = await context.params;\n\t\tconst segments = params[options.segmentsParam];\n\t\treturn Array.isArray(segments) ? segments.join(\"/\") : (segments ?? \"\");\n\t}\n\n\treturn \"\";\n}\n\n/**\n * Builds the upstream URL from base URL, path, and query params.\n */\nfunction buildUpstreamUrl(baseUrl: string, path: string, requestUrl: string): string {\n\tconst searchParams = new URL(requestUrl).searchParams;\n\tconst queryString = searchParams.toString();\n\treturn `${baseUrl}/${path}${queryString ? `?${queryString}` : \"\"}`;\n}\n\n/**\n * Builds the headers for the upstream request.\n */\nfunction buildRequestHeaders(\n\tauth: GatewayAuthToken,\n\tisStreaming: boolean,\n\tmodelId: string | null,\n\tcustomHeaders?: Record<string, string>,\n): Record<string, string> {\n\treturn {\n\t\t\"content-type\": \"application/json\",\n\t\t...customHeaders,\n\t\tauthorization: `Bearer ${auth.token}`,\n\t\t\"ai-gateway-auth-method\": auth.authMethod,\n\t\t\"ai-gateway-protocol-version\": PROTOCOL_VERSION,\n\t\t\"ai-language-model-id\": modelId ?? \"\",\n\t\t\"ai-language-model-streaming\": isStreaming ? \"true\" : \"false\",\n\t};\n}\n\n/**\n * Parses the error response from the gateway.\n */\nasync function parseErrorBody(res: Response): Promise<GatewayError> {\n\ttry {\n\t\treturn await res.json();\n\t} catch {\n\t\treturn {\n\t\t\terror: {\n\t\t\t\tmessage: res.statusText || \"Unknown error\",\n\t\t\t\ttype: \"gateway_error\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Creates an error response, optionally applying the onError hook.\n */\nasync function createErrorResponse(\n\tres: Response,\n\terrorBody: GatewayError,\n\trequestBody: LanguageModelV3CallOptions,\n\tonError: CreateGatewayProxyOptions[\"onError\"],\n): Promise<Response> {\n\tif (onError) {\n\t\tconst result = await onError({\n\t\t\trequest: requestBody,\n\t\t\terror: errorBody,\n\t\t\tstatus: res.status,\n\t\t});\n\n\t\tif (result instanceof Response) {\n\t\t\treturn result;\n\t\t}\n\n\t\treturn new Response(JSON.stringify(result), {\n\t\t\tstatus: res.status,\n\t\t\tstatusText: res.statusText,\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t});\n\t}\n\n\treturn new Response(JSON.stringify(errorBody), {\n\t\tstatus: res.status,\n\t\tstatusText: res.statusText,\n\t\theaders: { \"content-type\": \"application/json\" },\n\t});\n}\n\n/**\n * Handles a streaming response from the gateway.\n */\nfunction handleStreamingResponse(\n\tres: Response,\n\trequestBody: LanguageModelV3CallOptions,\n\tafterResponse: CreateGatewayProxyOptions[\"afterResponse\"],\n): Response {\n\tif (!res.body) {\n\t\treturn new Response(\"No response body\", { status: 500 });\n\t}\n\n\tconst contentType = res.headers.get(\"content-type\") || \"text/event-stream\";\n\n\t// Pass through directly if no afterResponse hook\n\tif (!afterResponse) {\n\t\treturn new Response(res.body, {\n\t\t\tstatus: res.status,\n\t\t\tstatusText: res.statusText,\n\t\t\theaders: { \"content-type\": contentType },\n\t\t});\n\t}\n\n\t// Transform stream to call afterResponse on finish\n\tconst transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);\n\n\treturn new Response(transformedStream, {\n\t\tstatus: res.status,\n\t\tstatusText: res.statusText,\n\t\theaders: { \"content-type\": contentType },\n\t});\n}\n\n/**\n * Handles a non-streaming JSON response from the gateway.\n */\nasync function handleJsonResponse(\n\tres: Response,\n\trequestBody: LanguageModelV3CallOptions,\n\tafterResponse: CreateGatewayProxyOptions[\"afterResponse\"],\n): Promise<Response> {\n\tlet responseBody: GatewayResponse = await res.json();\n\n\tif (afterResponse) {\n\t\tresponseBody = await afterResponse({\n\t\t\trequest: requestBody,\n\t\t\tresponse: responseBody,\n\t\t});\n\t}\n\n\treturn new Response(JSON.stringify(responseBody), {\n\t\tstatus: res.status,\n\t\tstatusText: res.statusText,\n\t\theaders: { \"content-type\": \"application/json\" },\n\t});\n}\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 = DEFAULT_BASE_URL,\n\theaders,\n\tsegmentsParam = \"segments\",\n\textractPath,\n\tbeforeRequest,\n\tafterResponse,\n\tonError,\n}: CreateGatewayProxyOptions = {}): CreateGatewayProxyResult => {\n\tconst handler = async (\n\t\trequest: Request,\n\t\tcontext?: { params: Promise<Record<string, string | string[] | undefined>> },\n\t) => {\n\t\t// Authenticate with the gateway\n\t\tconst auth = await getGatewayAuthToken();\n\t\tif (!auth) {\n\t\t\treturn new Response(\"AI Gateway authentication not configured\", { status: 500 });\n\t\t}\n\n\t\t// Build upstream URL\n\t\tconst path = await extractPathFromRequest(request, context, { extractPath, segmentsParam });\n\t\tconst upstream = buildUpstreamUrl(baseUrl, path, request.url);\n\n\t\t// Parse request body\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: buildRequestHeaders(\n\t\t\t\t\tauth,\n\t\t\t\t\tisStreaming,\n\t\t\t\t\trequest.headers.get(\"ai-language-model-id\"),\n\t\t\t\t\theaders,\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\tconst errorBody = await parseErrorBody(res);\n\t\t\t\treturn createErrorResponse(res, errorBody, requestBody, onError);\n\t\t\t}\n\n\t\t\t// Handle successful responses\n\t\t\tif (isStreaming) {\n\t\t\t\treturn handleStreamingResponse(res, requestBody, afterResponse);\n\t\t\t}\n\n\t\t\treturn handleJsonResponse(res, requestBody, afterResponse);\n\t\t} catch {\n\t\t\treturn new Response(\"Error proxying request to AI Gateway\", { status: 500 });\n\t\t}\n\t};\n\n\t// Export handler for all HTTP methods\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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAAA,OAAA,cAAA,OAAA;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;AACA;AACO,KAAA,oBAAA,aAAA,OAAA;AACP,YAAA,OAAA,CAAA,MAAA;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;;AClGA;AACA;AACA;AACA;AACA;AACA;AACO,cAAA,kBAAA,8FAAA,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
CHANGED
|
@@ -233,24 +233,159 @@ import { EventSourceParserStream } from '@ai-sdk/provider-utils';
|
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
const DEFAULT_BASE_URL = "https://ai-gateway.vercel.sh/v1/ai";
|
|
237
|
+
const PROTOCOL_VERSION = "0.0.1";
|
|
238
|
+
/**
|
|
239
|
+
* Extracts the path from the request using either a custom extractor or route params.
|
|
240
|
+
*/ function extractPathFromRequest(request, context, options) {
|
|
241
|
+
return _(function*() {
|
|
242
|
+
if (options.extractPath) {
|
|
243
|
+
return options.extractPath(request);
|
|
244
|
+
}
|
|
245
|
+
if (context == null ? void 0 : context.params) {
|
|
246
|
+
const params = yield context.params;
|
|
247
|
+
const segments = params[options.segmentsParam];
|
|
248
|
+
return Array.isArray(segments) ? segments.join("/") : segments != null ? segments : "";
|
|
249
|
+
}
|
|
250
|
+
return "";
|
|
251
|
+
})();
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Builds the upstream URL from base URL, path, and query params.
|
|
255
|
+
*/ function buildUpstreamUrl(baseUrl, path, requestUrl) {
|
|
256
|
+
const searchParams = new URL(requestUrl).searchParams;
|
|
257
|
+
const queryString = searchParams.toString();
|
|
258
|
+
return `${baseUrl}/${path}${queryString ? `?${queryString}` : ""}`;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Builds the headers for the upstream request.
|
|
262
|
+
*/ function buildRequestHeaders(auth, isStreaming, modelId, customHeaders) {
|
|
263
|
+
return _$1({
|
|
264
|
+
"content-type": "application/json"
|
|
265
|
+
}, customHeaders, {
|
|
266
|
+
authorization: `Bearer ${auth.token}`,
|
|
267
|
+
"ai-gateway-auth-method": auth.authMethod,
|
|
268
|
+
"ai-gateway-protocol-version": PROTOCOL_VERSION,
|
|
269
|
+
"ai-language-model-id": modelId != null ? modelId : "",
|
|
270
|
+
"ai-language-model-streaming": isStreaming ? "true" : "false"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Parses the error response from the gateway.
|
|
275
|
+
*/ function parseErrorBody(res) {
|
|
276
|
+
return _(function*() {
|
|
277
|
+
try {
|
|
278
|
+
return yield res.json();
|
|
279
|
+
} catch (unused) {
|
|
280
|
+
return {
|
|
281
|
+
error: {
|
|
282
|
+
message: res.statusText || "Unknown error",
|
|
283
|
+
type: "gateway_error"
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Creates an error response, optionally applying the onError hook.
|
|
291
|
+
*/ function createErrorResponse(res, errorBody, requestBody, onError) {
|
|
292
|
+
return _(function*() {
|
|
293
|
+
if (onError) {
|
|
294
|
+
const result = yield onError({
|
|
295
|
+
request: requestBody,
|
|
296
|
+
error: errorBody,
|
|
297
|
+
status: res.status
|
|
298
|
+
});
|
|
299
|
+
if (result instanceof Response) {
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
return new Response(JSON.stringify(result), {
|
|
303
|
+
status: res.status,
|
|
304
|
+
statusText: res.statusText,
|
|
305
|
+
headers: {
|
|
306
|
+
"content-type": "application/json"
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return new Response(JSON.stringify(errorBody), {
|
|
311
|
+
status: res.status,
|
|
312
|
+
statusText: res.statusText,
|
|
313
|
+
headers: {
|
|
314
|
+
"content-type": "application/json"
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
})();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Handles a streaming response from the gateway.
|
|
321
|
+
*/ function handleStreamingResponse(res, requestBody, afterResponse) {
|
|
322
|
+
if (!res.body) {
|
|
323
|
+
return new Response("No response body", {
|
|
324
|
+
status: 500
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const contentType = res.headers.get("content-type") || "text/event-stream";
|
|
328
|
+
// Pass through directly if no afterResponse hook
|
|
329
|
+
if (!afterResponse) {
|
|
330
|
+
return new Response(res.body, {
|
|
331
|
+
status: res.status,
|
|
332
|
+
statusText: res.statusText,
|
|
333
|
+
headers: {
|
|
334
|
+
"content-type": contentType
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// Transform stream to call afterResponse on finish
|
|
339
|
+
const transformedStream = createStreamTransformer(res.body, requestBody, afterResponse);
|
|
340
|
+
return new Response(transformedStream, {
|
|
341
|
+
status: res.status,
|
|
342
|
+
statusText: res.statusText,
|
|
343
|
+
headers: {
|
|
344
|
+
"content-type": contentType
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Handles a non-streaming JSON response from the gateway.
|
|
350
|
+
*/ function handleJsonResponse(res, requestBody, afterResponse) {
|
|
351
|
+
return _(function*() {
|
|
352
|
+
let responseBody = yield res.json();
|
|
353
|
+
if (afterResponse) {
|
|
354
|
+
responseBody = yield afterResponse({
|
|
355
|
+
request: requestBody,
|
|
356
|
+
response: responseBody
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return new Response(JSON.stringify(responseBody), {
|
|
360
|
+
status: res.status,
|
|
361
|
+
statusText: res.statusText,
|
|
362
|
+
headers: {
|
|
363
|
+
"content-type": "application/json"
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
})();
|
|
367
|
+
}
|
|
236
368
|
/**
|
|
237
369
|
* Creates a proxy handler for the Vercel AI Gateway.
|
|
238
370
|
*
|
|
239
371
|
* Uses AI SDK V3 Language Model.
|
|
240
372
|
* @see https://github.com/vercel/ai/blob/main/packages/gateway/src/gateway-language-model.ts
|
|
241
|
-
*/ const createGatewayProxy = ({ baseUrl = "
|
|
242
|
-
const handler = (request,
|
|
243
|
-
|
|
373
|
+
*/ const createGatewayProxy = ({ baseUrl = DEFAULT_BASE_URL, headers, segmentsParam = "segments", extractPath, beforeRequest, afterResponse, onError } = {})=>{
|
|
374
|
+
const handler = (request, context)=>_(function*() {
|
|
375
|
+
// Authenticate with the gateway
|
|
244
376
|
const auth = yield getGatewayAuthToken();
|
|
245
377
|
if (!auth) {
|
|
246
378
|
return new Response("AI Gateway authentication not configured", {
|
|
247
379
|
status: 500
|
|
248
380
|
});
|
|
249
381
|
}
|
|
250
|
-
|
|
251
|
-
const path =
|
|
252
|
-
|
|
253
|
-
|
|
382
|
+
// Build upstream URL
|
|
383
|
+
const path = yield extractPathFromRequest(request, context, {
|
|
384
|
+
extractPath,
|
|
385
|
+
segmentsParam
|
|
386
|
+
});
|
|
387
|
+
const upstream = buildUpstreamUrl(baseUrl, path, request.url);
|
|
388
|
+
// Parse request body
|
|
254
389
|
let requestBody;
|
|
255
390
|
try {
|
|
256
391
|
requestBody = yield request.clone().json();
|
|
@@ -267,109 +402,29 @@ import { EventSourceParserStream } from '@ai-sdk/provider-utils';
|
|
|
267
402
|
}
|
|
268
403
|
const isStreaming = request.headers.get("ai-language-model-streaming") === "true";
|
|
269
404
|
try {
|
|
270
|
-
var _request_headers_get;
|
|
271
405
|
const res = yield fetch(upstream, {
|
|
272
406
|
method: request.method,
|
|
273
407
|
body: JSON.stringify(requestBody),
|
|
274
408
|
duplex: "half",
|
|
275
|
-
headers:
|
|
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
|
-
})
|
|
409
|
+
headers: buildRequestHeaders(auth, isStreaming, request.headers.get("ai-language-model-id"), headers)
|
|
284
410
|
});
|
|
285
411
|
// Handle error responses
|
|
286
412
|
if (!res.ok) {
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
});
|
|
413
|
+
const errorBody = yield parseErrorBody(res);
|
|
414
|
+
return createErrorResponse(res, errorBody, requestBody, onError);
|
|
324
415
|
}
|
|
325
|
-
//
|
|
416
|
+
// Handle successful responses
|
|
326
417
|
if (isStreaming) {
|
|
327
|
-
|
|
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
|
-
});
|
|
418
|
+
return handleStreamingResponse(res, requestBody, afterResponse);
|
|
351
419
|
}
|
|
352
|
-
|
|
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
|
-
});
|
|
420
|
+
return handleJsonResponse(res, requestBody, afterResponse);
|
|
367
421
|
} catch (unused) {
|
|
368
422
|
return new Response("Error proxying request to AI Gateway", {
|
|
369
423
|
status: 500
|
|
370
424
|
});
|
|
371
425
|
}
|
|
372
426
|
})();
|
|
427
|
+
// Export handler for all HTTP methods
|
|
373
428
|
handler.GET = handler;
|
|
374
429
|
handler.POST = handler;
|
|
375
430
|
handler.PUT = handler;
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-gateway-proxy",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/SferaDev/SferaDev.git",
|
|
10
|
+
"directory": "packages/ai-gateway-proxy"
|
|
11
|
+
},
|
|
7
12
|
"exports": {
|
|
8
13
|
".": {
|
|
9
14
|
"types": "./dist/index.d.ts",
|
|
@@ -34,7 +39,8 @@
|
|
|
34
39
|
"@sferadev/tsconfig": "0.0.0"
|
|
35
40
|
},
|
|
36
41
|
"publishConfig": {
|
|
37
|
-
"access": "public"
|
|
42
|
+
"access": "public",
|
|
43
|
+
"provenance": true
|
|
38
44
|
},
|
|
39
45
|
"scripts": {
|
|
40
46
|
"build": "bunchee",
|