ai-gateway-proxy 0.0.1

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