ai-gateway-proxy 0.0.2 → 0.2.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Mintlify
3
+ Copyright (c) 2025 Alexis Rico
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # ai-gateway-proxy
2
2
 
3
- A lightweight proxy handler for [Vercel AI Gateway](https://vercel.com/docs/ai/ai-gateway).
3
+ A lightweight proxy handler for [Vercel AI Gateway](https://vercel.com/docs/ai-gateway).
4
4
 
5
5
  ## Features
6
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
- - **Next.js Integration** - Works seamlessly with Next.js App Router catch-all routes
7
+ - **Request and response hooks:** transform language model requests and responses with `beforeRequest`, `afterResponse` and `onError`, and meter or rewrite the other calls (embedding, image, video, speech, transcription, reranking, decision, batch start) with `beforeRawRequest` and `afterRawResponse`
8
+ - **Streaming:** forwards the gateway's SSE stream as it arrives and gives `afterResponse` the aggregated result when the stream finishes (AI SDK V4 protocol)
9
+ - **Fetch handler:** takes a Fetch `Request` and returns a `Response`, so it plugs into Next.js and Hono directly and into Express or `node:http` through a small adapter
10
10
 
11
11
  ## Installation
12
12
 
@@ -14,16 +14,10 @@ A lightweight proxy handler for [Vercel AI Gateway](https://vercel.com/docs/ai/a
14
14
  npm install ai-gateway-proxy
15
15
  ```
16
16
 
17
- ```bash
18
- pnpm add ai-gateway-proxy
19
- ```
20
-
21
- ```bash
22
- yarn add ai-gateway-proxy
23
- ```
24
-
25
17
  ## Quick Start
26
18
 
19
+ ### Next.js App Router
20
+
27
21
  Create a catch-all route at `app/api/ai/[...segments]/route.ts`:
28
22
 
29
23
  ```typescript
@@ -32,150 +26,261 @@ import { createGatewayProxy } from "ai-gateway-proxy";
32
26
  export const { GET, POST, PUT, DELETE, PATCH } = createGatewayProxy();
33
27
  ```
34
28
 
35
- ## Authentication
29
+ The handler reads the path from the route's `segments` param. For a differently named catch-all, such as `app/api/ai/[...path]/route.ts`, pass `createGatewayProxy({ segmentsParam: "path" })`.
36
30
 
37
- The proxy automatically handles authentication using one of the following methods (in priority order):
31
+ ### Hono
38
32
 
39
- ### API Key
33
+ For frameworks without catch-all route params, use the `extractPath` option:
40
34
 
41
- Set the `AI_GATEWAY_API_KEY` environment variable:
35
+ ```typescript
36
+ import { Hono } from "hono";
37
+ import { createGatewayProxy } from "ai-gateway-proxy";
42
38
 
43
- ```bash
44
- AI_GATEWAY_API_KEY=your-api-key
45
- ```
39
+ const app = new Hono();
46
40
 
47
- ### OIDC (Vercel)
41
+ const proxy = createGatewayProxy({
42
+ extractPath: (request) => {
43
+ const url = new URL(request.url);
44
+ return url.pathname.replace(/^\/api\/ai\//, "");
45
+ },
46
+ });
48
47
 
49
- When deployed on Vercel, the proxy automatically uses [Vercel OIDC](https://vercel.com/docs/security/oidc) for authentication. No additional configuration is required.
48
+ app.all("/api/ai/*", (c) => proxy(c.req.raw));
49
+ ```
50
50
 
51
- ## Configuration
51
+ ### Express
52
52
 
53
- ### Custom Base URL
53
+ The handler takes a Fetch `Request`, not an Express `req`, so convert in both directions. Register the route before any body parser such as `express.json()`, which would consume the body the proxy needs to read.
54
54
 
55
55
  ```typescript
56
- createGatewayProxy({
57
- baseUrl: "https://ai-gateway.vercel.sh/v1/ai",
56
+ import type { IncomingMessage, ServerResponse } from "node:http";
57
+ import { Readable } from "node:stream";
58
+ import { pipeline } from "node:stream/promises";
59
+ import type { ReadableStream as NodeReadableStream } from "node:stream/web";
60
+ import express from "express";
61
+ import { createGatewayProxy } from "ai-gateway-proxy";
62
+
63
+ const proxy = createGatewayProxy({
64
+ extractPath: (request) => new URL(request.url).pathname.replace(/^\/api\/ai\//, ""),
58
65
  });
66
+
67
+ async function handle(req: IncomingMessage, res: ServerResponse) {
68
+ const controller = new AbortController();
69
+ res.on("close", () => {
70
+ if (!res.writableFinished) controller.abort();
71
+ });
72
+
73
+ const headers = new Headers();
74
+ for (const [name, value] of Object.entries(req.headers)) {
75
+ if (value !== undefined) headers.set(name, Array.isArray(value) ? value.join(", ") : value);
76
+ }
77
+
78
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
79
+ const request = new Request(new URL(req.url ?? "/", `http://${req.headers.host}`), {
80
+ method: req.method,
81
+ headers,
82
+ body: hasBody ? (Readable.toWeb(req) as ReadableStream<Uint8Array>) : undefined,
83
+ duplex: "half",
84
+ signal: controller.signal,
85
+ } as RequestInit);
86
+
87
+ const response = await proxy(request);
88
+
89
+ res.statusCode = response.status;
90
+ response.headers.forEach((value, name) => res.setHeader(name, value));
91
+ if (!response.body) {
92
+ res.end();
93
+ return;
94
+ }
95
+ // Rejects when the client disconnects mid-stream; the abort above has already cancelled upstream.
96
+ await pipeline(Readable.fromWeb(response.body as NodeReadableStream<Uint8Array>), res).catch(() => {});
97
+ }
98
+
99
+ const app = express();
100
+ app.all("/api/ai/*splat", handle);
101
+ app.listen(3000);
59
102
  ```
60
103
 
61
- ### Additional Headers
104
+ `/api/ai/*splat` is Express 5 syntax; Express 4 uses `/api/ai/*`. `handle` only uses `node:http` types, so it also works as a plain `createServer(handle)` callback.
105
+
106
+ ### Client
107
+
108
+ Point the AI SDK's gateway provider at the route. It sends `ai-language-model-streaming: true` for `streamText` and the like, which is how the proxy knows to stream.
62
109
 
63
110
  ```typescript
64
- createGatewayProxy({
65
- headers: {
66
- "x-custom-header": "value",
67
- },
111
+ import { createGateway } from "@ai-sdk/gateway";
112
+ import { generateText } from "ai";
113
+
114
+ const gateway = createGateway({
115
+ baseURL: "https://your-app.example.com/api/ai",
116
+ // The proxy adds the real credentials upstream.
117
+ apiKey: "unused",
118
+ });
119
+
120
+ const { text } = await generateText({
121
+ model: gateway("openai/gpt-5"),
122
+ prompt: "Hello",
68
123
  });
69
124
  ```
70
125
 
71
- ### Request Transformation
126
+ Some gateway client calls do not go through the proxy:
72
127
 
73
- Transform the request body before sending to the gateway:
128
+ - Streaming speech and transcription (the gateway models' `doStream`) open a WebSocket to `speech-model` or `transcription-model` under `baseURL`. The proxy handles HTTP requests only, so it cannot carry those connections. Generating speech or a transcription without streaming does go through the proxy.
129
+ - Realtime (`experimental_realtime`) opens a WebSocket to `realtime-model` under `baseURL`, which the proxy cannot carry for the same reason.
130
+ - `getCredits()`, `getSpendReport()` and `getGenerationInfo()` request `/v1/credits`, `/v1/report` and `/v1/generation` at the origin of `baseURL`, which is your app's host and not the proxy route.
131
+ - `getToken()` on `experimental_realtime`, `experimental_transcription` and `experimental_speech` posts to `/v1/realtime/client-secrets` at the same origin, and the WebSocket URL it returns is under `baseURL`, so neither reaches the proxy.
74
132
 
75
- ```typescript
76
- createGatewayProxy({
77
- beforeRequest: ({ request }) => {
78
- // Modify the request body
79
- return {
80
- ...request,
81
- // Add custom modifications
82
- };
83
- },
84
- });
85
- ```
133
+ ## Authentication
86
134
 
87
- ### Response Transformation
135
+ The proxy gets a credential on every request with `getGatewayAuthToken()`, in this order:
88
136
 
89
- Transform the response after receiving from the gateway. For streaming responses, this is called when the stream completes with aggregated content:
137
+ 1. **API key:** the `AI_GATEWAY_API_KEY` environment variable
138
+ 2. **OIDC:** a Vercel OIDC token, available when deployed on Vercel
90
139
 
91
- ```typescript
92
- createGatewayProxy({
93
- afterResponse: ({ request, response }) => {
94
- // Log usage, modify metadata, etc.
95
- console.log("Usage:", response.usage);
96
- return response;
97
- },
98
- });
99
- ```
140
+ `getGatewayAuthToken()` is exported. It resolves to `{ token, authMethod: "api-key" | "oidc" }`, or `null` when neither is available, in which case the proxy answers `500` with a `proxy_error` (see below). The proxy sends the token upstream as `authorization: Bearer <token>` with `ai-gateway-auth-method`, so the credential never reaches the client.
100
141
 
101
- ### Error Handling
142
+ ## Options
102
143
 
103
- Custom error handling with the `onError` hook:
144
+ `createGatewayProxy(options?)` returns a handler `(request: Request, context?) => Promise<Response>`. The same function is also exposed as its `GET`, `POST`, `PUT`, `DELETE` and `PATCH` properties.
104
145
 
105
- ```typescript
106
- createGatewayProxy({
107
- onError: ({ request, error, status }) => {
108
- // Log errors, modify error response, or return a custom Response
109
- console.error("Gateway error:", error);
146
+ | Option | Default | Description |
147
+ | --- | --- | --- |
148
+ | `baseUrl` | `https://ai-gateway.vercel.sh/v4/ai` | Gateway URL that the extracted path and the incoming query string are appended to. |
149
+ | `headers` | none | Extra headers for every upstream request. The proxy's own `authorization`, `ai-gateway-auth-method` and `ai-gateway-protocol-version` override entries with the same name in any casing, as does `content-type` on requests with a body. On requests with a body, the incoming request's `ai-language-model-*`, `ai-model-id` and `ai-*-model-specification-version` headers replace any entries describing the model call, and its `ai-o11y-*`, `idempotency-key` and `accept` headers override entries with the same name. No other incoming header is forwarded, so the client's `authorization` and cookies never reach the gateway. |
150
+ | `segmentsParam` | `"segments"` | Name of the catch-all route param read from `context.params` when `extractPath` is not set, joined with `/`. Use `"path"` for `app/api/ai/[...path]/route.ts`. |
151
+ | `extractPath` | none | `(request) => string \| Promise<string>`. Returns the upstream path relative to `baseUrl`, such as `language-model`. Takes precedence over `segmentsParam`. |
152
+ | `beforeRequest` | none | `({ request, modelId }) => LanguageModelV4CallOptions`. Receives the parsed JSON body and returns the body to send upstream. Returning `undefined` or `null` sends the parsed body and logs an error. Any other result that is not an object, such as a string or an array, returns a 500 without calling the gateway. |
153
+ | `afterResponse` | none | `({ request, modelId, response }) => GatewayResponse`. Runs on a successful response; see below. |
154
+ | `onError` | none | `({ request, modelId, error, status }) => GatewayError \| Response`. Runs when the gateway returns a non-OK status; see below. |
155
+ | `beforeRawRequest` | none | `({ kind, path, modelId, request }) => unknown`. Runs on the calls that are not language model calls; see below. |
156
+ | `afterRawResponse` | none | `({ kind, path, modelId, request, response }) => Response \| undefined`. Runs on the response to those calls; see below. |
110
157
 
111
- // Return modified error
112
- return error;
158
+ Each hook may also return a promise. `beforeRequest`, `afterResponse` and `onError` are typed for language model calls, and every hook runs only for requests with a body. Their `modelId` is the client's `ai-language-model-id` header, such as `openai/gpt-5`, since the call body does not name the model; it is `undefined` when the request has no such header. A `GET` or `HEAD` request, such as the provider's `GET /config` model list, and any other request whose body is missing or empty, such as a bodyless `DELETE`, is forwarded without a body and without running any hook, and the upstream status and body come back unchanged, except that a redirect is answered with a 502 as below. A request body that neither `beforeRequest` nor `beforeRawRequest` receives is sent upstream as the client sent it; once either hook runs, the body it returns or leaves in place is serialised again.
113
159
 
114
- // Or return a custom Response
115
- // return new Response("Custom error", { status: 500 });
116
- },
117
- });
118
- ```
160
+ Path matching fails closed: the language model hooks run on every request with a body except one whose path relative to `baseUrl` is exactly one of the paths `@ai-sdk/gateway` uses for calls that are not language model calls:
119
161
 
120
- ## API Reference
162
+ | Path | Call |
163
+ | --- | --- |
164
+ | `embedding-model`, `image-model`, `video-model`, `video-model/start`, `speech-model`, `transcription-model`, `reranking-model`, `decision-model` | Model calls of the other kinds |
165
+ | `batch/start` | Starting a batch |
166
+ | `video-model/status`, `batch/status`, `batch/results`, `batch/cancel` | Reading or cancelling a job |
121
167
 
122
- ### `createGatewayProxy(options?)`
168
+ The match is exact and ignores headers, so a request to any other path, including a different spelling of a listed one (`Embedding-Model`, `embedding-model/`, `embedding%2Dmodel`) or a listed path under a further prefix (`ai/embedding-model`), runs the language model hooks, and a client cannot turn them off by sending `ai-model-id` or dropping `ai-language-model-id`. Set `baseUrl` to the URL the client's paths are relative to, so the listed paths match.
123
169
 
124
- Creates a proxy handler for the AI Gateway.
170
+ The path, whether from `extractPath` or the route param, must stay relative: one that starts with `/`, contains `://`, or has a segment that fails to percent-decode, decodes to `.` or `..` or contains `/`, `\` or a NUL byte is rejected with `400 Invalid path` before anything is sent upstream.
125
171
 
126
- #### Options
172
+ ### `afterResponse`
127
173
 
128
- | Option | Type | Default | Description |
129
- |--------|------|---------|-------------|
130
- | `baseUrl` | `string` | `"https://ai-gateway.vercel.sh/v1/ai"` | The base URL of the AI Gateway |
131
- | `headers` | `Record<string, string>` | `{}` | Additional headers to include in requests |
132
- | `beforeRequest` | `(ctx) => request` | - | Transform request before sending |
133
- | `afterResponse` | `(ctx) => response` | - | Transform response after receiving |
134
- | `onError` | `(ctx) => error \| Response` | - | Custom error handling |
174
+ - **Non-streamed responses:** receives the gateway's JSON body as `response`, and the value it returns is sent to the client instead, with the gateway's `usage` when the result has none, as on a stream, since an AI SDK client reads usage from every result. `undefined` or `null` forwards the upstream body. Any other result is checked against `GatewayResponse` (see "Result validation" below), and one that fails is answered with a 500 naming the problem instead of being sent.
175
+ - **Streamed responses:** every event is forwarded to the client as it arrives. When the `finish` event comes, the hook receives the aggregated response, and the `finishReason`, `usage` and `providerMetadata` it returns replace those in the `finish` event; content changes have no effect because the content has already been sent. The aggregated response also carries `response`, the `id`, `modelId` and `timestamp` (the ISO string sent over the wire) of the stream's `response-metadata` parts, the latest value of each winning, so a hook can log the provider's response id or the model that answered; it is absent when no part carried them. If the hook throws, or returns a result that fails the check below, `undefined` or `null`, the error is logged and the original `finish` event is forwarded. If the stream ends, fails or is cancelled before its `finish` event, the hook runs once with the content received so far, `incomplete` set (`{ reason: "ended" }`, `{ reason: "error", error }` or `{ reason: "cancelled", cancelReason }`), `response.usage` `undefined` and `response.finishReason` `error` when the read failed or the stream sent an `error` part, `other` otherwise. A client that leaves counts as `cancelled` whether the framework cancels the response body or aborts `request.signal`, and is not logged. Nothing can be rewritten then, so its return value is ignored and a throw is logged. `response.usage` is typed `LanguageModelV4Usage | undefined`, so a hook that meters usage must handle `undefined`.
176
+ - **Event streams and JSON Lines on other calls:** a successful `text/event-stream` or JSON Lines (`application/x-ndjson`, `application/jsonl`) response to a request without `ai-language-model-streaming: true`, such as the video model's `doGenerate` or the batch API's `doGetBatchResults`, is forwarded as is and the hook does not run, since it is not a language model response.
177
+ - **No content:** a `204` or `205` response is forwarded with no body and no `content-type`, keeping the allowlisted headers, and the hook does not run, since there is no response to pass it.
178
+ - **Without the hook:** a successful non-streamed response, like any successful response to a call that skips the hook, is forwarded with the gateway's bytes and `content-type` instead of being parsed and re-serialised, so a large image or video body is not held in memory.
135
179
 
136
- #### Returns
180
+ #### Result validation
137
181
 
138
- A handler function with named exports for HTTP methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`.
182
+ A result is rejected when it is not an object or when a field is missing or has the wrong type:
139
183
 
140
- ### `getGatewayAuthToken()`
184
+ - `content`: an array of objects, each with a string `type`
185
+ - `finishReason`: an object with a string `unified`, and `raw` a string or absent
186
+ - `usage`: absent, or an object whose `inputTokens` and `outputTokens` are objects with number or absent counts; when absent, the gateway's `usage` is sent
187
+ - `warnings`: absent or an array; `@ai-sdk/gateway` reads a missing one as none
188
+ - `providerMetadata`: absent or an object
141
189
 
142
- Retrieves the authentication token for the AI Gateway.
190
+ `null` is accepted wherever a field may be absent, since a body that went through JSON cannot hold `undefined`. Other fields are not checked and are forwarded as returned. A rejected result is logged with `console.error` as `afterResponse hook returned an invalid response: <problem>`, and on a non-streamed request the client gets the same message as a 500 `proxy_error`.
143
191
 
144
- ```typescript
145
- import { getGatewayAuthToken } from "ai-gateway-proxy";
192
+ ### `onError`
146
193
 
147
- const auth = await getGatewayAuthToken();
148
- // { token: "...", authMethod: "api-key" | "oidc" }
149
- ```
194
+ Receives `{ request, error, status }`, where `error` is the gateway's JSON error body. When the body is not JSON of the form `{ error: { message } }`, such as an HTML page from a load balancer, `error` is `{ error: { message: "AI Gateway returned <status>: <excerpt>", type: "gateway_error" } }`, with the first 200 characters of the body as the excerpt. Return a replacement body to send it as JSON with the upstream status, or return a `Response` to send that as is. Returning `undefined` or `null`, as a hook that only logs does, forwards the error body unchanged, and so does a result that is not of the form `{ error: { message, type?, code? }, generationId? }` with a string `message`, a string or absent `type`, a string, number or absent `code` and a string or absent `generationId`, which an AI SDK client could not parse; the problem is logged. A hook that throws is logged and the error body is forwarded too, so the client keeps the upstream status and `retry-after` rather than a 500 it would retry. A `generationId` you return reaches the client, which appends it to the error message as `[<id>]`. Without `onError`, the error body is sent with the upstream status.
150
195
 
151
- ### `StreamContentAggregator`
196
+ ### Example: cap, meter and mask
152
197
 
153
- Aggregates streaming content into a complete response. Used internally by the proxy for `afterResponse` hooks on streaming requests.
198
+ The three language model hooks together: cap output tokens, record usage, and keep upstream server errors from the client. `meter` stands for your own function.
154
199
 
155
200
  ```typescript
156
- import { StreamContentAggregator } from "ai-gateway-proxy";
157
-
158
- const aggregator = new StreamContentAggregator();
159
- aggregator.process(streamPart);
160
- const response = aggregator.getResponse();
201
+ createGatewayProxy({
202
+ beforeRequest: ({ request }) => ({
203
+ ...request,
204
+ maxOutputTokens: Math.min(request.maxOutputTokens ?? 1024, 1024),
205
+ }),
206
+ afterResponse: async ({ request, modelId, response, incomplete }) => {
207
+ // usage is undefined when a stream ended before its finish event.
208
+ await meter({
209
+ modelId,
210
+ inputTokens: response.usage?.inputTokens.total,
211
+ outputTokens: response.usage?.outputTokens.total,
212
+ maxOutputTokens: request.maxOutputTokens,
213
+ complete: incomplete === undefined,
214
+ });
215
+ return response;
216
+ },
217
+ onError: ({ error, status }) =>
218
+ status >= 500
219
+ ? { error: { message: "The model provider is unavailable", type: "gateway_error" } }
220
+ : error,
221
+ });
161
222
  ```
162
223
 
163
- ### `createStreamTransformer(body, requestBody, afterResponse)`
224
+ `afterResponse` returns `response` unchanged, so the client gets the gateway's body on a non-streamed call and the upstream `finish` event on a stream. `request` is the body after `beforeRequest`, so `maxOutputTokens` is the capped value.
225
+
226
+ ### Raw hooks
164
227
 
165
- Creates a transform stream that passes through SSE events while aggregating content and calling the `afterResponse` hook on completion.
228
+ `beforeRawRequest` and `afterRawResponse` run where the language model hooks do not: on a request with a body whose path is exactly one of the model call paths in the table above or `batch/start`. They are untyped, since each kind has its own body. They do not run on the job reads (`video-model/status`, `batch/status`, `batch/results`, `batch/cancel`) or on bodyless requests.
166
229
 
167
- ## Types
230
+ Both receive `kind` (`"embedding"`, `"image"`, `"video"`, `"speech"`, `"transcription"`, `"reranking"`, `"decision"` or `"batch"`), `path` (such as `video-model/start`), `modelId` (the client's `ai-model-id` header, which is the only place an embedding or image call names its model) and `request`, the parsed JSON body.
231
+
232
+ - **`beforeRawRequest`:** a returned value replaces the body sent upstream; `undefined` or `null` sends it as is.
233
+ - **`afterRawResponse`:** also receives `response`, the `Response` the proxy would send: the upstream status, `content-type` and the allowlisted headers below, with the upstream body on success (a video call's event stream included) and the `GatewayError` body described under `onError` on a failure. Return a `Response` to send that instead, or nothing to send `response`. The body can be read once, so read `response.clone()` when returning nothing; a hook that reads `response` itself and returns nothing, or returns anything but a `Response`, is a 500. When the hook sends another `Response`, throws or returns an invalid result, the proxy cancels `response`'s body unless the hook read it, piped it or built the returned `Response` on it, so the upstream request ends. It does not run on a redirect or when the upstream request fails.
168
234
 
169
235
  ```typescript
170
- import type {
171
- CreateGatewayProxyOptions,
172
- CreateGatewayProxyFn,
173
- CreateGatewayProxyResult,
174
- GatewayResponse,
175
- GatewayError,
176
- } from "ai-gateway-proxy";
236
+ createGatewayProxy({
237
+ afterRawResponse: async ({ kind, modelId, response }) => {
238
+ if (kind === "embedding" && response.ok) {
239
+ const { usage } = await response.clone().json();
240
+ await meter(modelId, usage);
241
+ }
242
+ },
243
+ });
177
244
  ```
178
245
 
246
+ ## Responses the proxy generates
247
+
248
+ Apart from forwarding the gateway's own responses, the handler can answer with a JSON `GatewayError` body, `{ "error": { "message", "type" } }`, with the `type` and `message` below (`499` has an empty body):
249
+
250
+ | Status | Type | Message | When |
251
+ | --- | --- | --- | --- |
252
+ | 500 | `proxy_error` | `AI Gateway authentication not configured: set AI_GATEWAY_API_KEY or deploy on Vercel with OIDC enabled` | `getGatewayAuthToken()` returned `null`. The type is not `authentication_error`, which `@ai-sdk/gateway` replaces with advice about the client's own key. |
253
+ | 400 | `invalid_request_error` | `Invalid path` | The path is not a safe relative path under `baseUrl`. |
254
+ | 400 | `invalid_request_error` | `Invalid JSON body` | A request other than `GET` or `HEAD` has a non-empty body that is not JSON. |
255
+ | 400 | `invalid_request_error` | `Invalid request body: a language model call must be a JSON object` | The body is JSON but not an object, such as an array or `null`, on a path that runs `beforeRequest`, `afterResponse` or `onError`, and one of them is set. No hook runs and the gateway is not called. |
256
+ | 499 | none | none | The client aborted the request (`request.signal`) before the handler returned. The signal is passed to the upstream `fetch`, so the abort also cancels the upstream request. |
257
+ | 500 | `proxy_error` | `Error proxying request to AI Gateway` | Anything else that throws: `extractPath`, `beforeRequest`, `beforeRawRequest`, the upstream `fetch`, a successful non-streamed response for `afterResponse` that is not a JSON object, `afterResponse` on a non-streamed request or `afterRawResponse`, or an `afterRawResponse` that read the body and returned no `Response`. |
258
+ | 500 | `proxy_error` | `beforeRequest hook returned an invalid request: the result must be an object` | `beforeRequest` returned a value other than an object, `undefined` or `null`. The gateway is not called. |
259
+ | 500 | `proxy_error` | `afterResponse hook returned an invalid response: <problem>` | `afterResponse` on a non-streamed request returned a result that fails the result validation above. |
260
+ | 500 | `proxy_error` | `No response body` | A successful streamed response arrived without a body. |
261
+ | 502 | `proxy_error` | `Upstream redirected, the proxy does not follow redirects` | The gateway answered with a 3xx status. The proxy sets `redirect: "manual"`, runs no `onError` and forwards no `Location`. |
262
+
263
+ ## Other exports
264
+
265
+ - `createStreamTransformer(body, request, afterResponse, signal?)` and `StreamContentAggregator`: the pieces the proxy uses to run `afterResponse` on a stream, for building your own streaming handler. Pass `signal` when the upstream `fetch` uses it too, so an abort is reported as `cancelled` rather than as an upstream failure.
266
+ - Types: `CreateGatewayProxyOptions`, `CreateGatewayProxyFn`, `CreateGatewayProxyResult`, `GatewayResponse`, `GatewayResponseMetadata`, `GatewayError`, `IncompleteStream`, `ModelCallKind`, `RawRequestContext`, `RawResponseContext` and `GatewayAuthToken`.
267
+
268
+ ## Documentation
269
+
270
+ More guides are on the [documentation site](https://sferadev.com/docs/packages/ai-gateway-proxy). Where it differs from this README, this README matches the code.
271
+
272
+ ## Development
273
+
274
+ The toolchain is pinned in `mise.toml`. With [mise](https://mise.jdx.dev) installed:
275
+
276
+ ```bash
277
+ mise install
278
+ pnpm install --frozen-lockfile
279
+ pnpm check
280
+ ```
281
+
282
+ The integration tests call the real AI Gateway and run only when `AI_GATEWAY_API_KEY` is set (`pnpm test:integration`). Add a changeset with `pnpm changeset` for any change that should be released.
283
+
179
284
  ## License
180
285
 
181
286
  MIT