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 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 = "https://ai-gateway.vercel.sh/v1/ai", headers, beforeRequest, afterResponse, onError } = {})=>{
244
- const handler = (request, { params })=>_async_to_generator._(function*() {
245
- var _ref;
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
- 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()}` : ""}`;
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: _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
- })
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
- 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
- });
415
+ const errorBody = yield parseErrorBody(res);
416
+ return createErrorResponse(res, errorBody, requestBody, onError);
326
417
  }
327
- // For streaming responses
418
+ // Handle successful responses
328
419
  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
- });
420
+ return handleStreamingResponse(res, requestBody, afterResponse);
353
421
  }
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
- });
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
- * Next.js route handler function signature for the gateway proxy.
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.
@@ -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 = "https://ai-gateway.vercel.sh/v1/ai", headers, beforeRequest, afterResponse, onError } = {})=>{
242
- const handler = (request, { params })=>_(function*() {
243
- var _ref;
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
- 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()}` : ""}`;
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: _$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
- })
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
- 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
- });
413
+ const errorBody = yield parseErrorBody(res);
414
+ return createErrorResponse(res, errorBody, requestBody, onError);
324
415
  }
325
- // For streaming responses
416
+ // Handle successful responses
326
417
  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
- });
418
+ return handleStreamingResponse(res, requestBody, afterResponse);
351
419
  }
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
- });
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.1",
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",