ai-gateway-proxy 0.4.0 → 0.6.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 +5 -5
- package/dist/index.cjs +41 -3
- package/dist/index.mjs +41 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ The proxy gets a credential on every request with `getGatewayAuthToken()`, in th
|
|
|
155
155
|
| `beforeRawRequest` | none | `({ kind, path, modelId, request }) => unknown`. Runs on the calls that are not language model calls; see below. |
|
|
156
156
|
| `afterRawResponse` | none | `({ kind, path, modelId, request, response }) => Response \| undefined`. Runs on the response to those calls; see below. |
|
|
157
157
|
|
|
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.
|
|
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 and a gateway `authentication_error` is retyped 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.
|
|
159
159
|
|
|
160
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:
|
|
161
161
|
|
|
@@ -230,7 +230,7 @@ createGatewayProxy({
|
|
|
230
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
231
|
|
|
232
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.
|
|
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 failure like a throw. A failure on a successful `response` is a 424, since the gateway has already answered, and billed, the call and an AI SDK client retries a 5xx but not a 424; on an error `response` it 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.
|
|
234
234
|
|
|
235
235
|
```typescript
|
|
236
236
|
createGatewayProxy({
|
|
@@ -250,14 +250,14 @@ Apart from forwarding the gateway's own responses, the handler can answer with a
|
|
|
250
250
|
| Status | Type | Message | When |
|
|
251
251
|
| --- | --- | --- | --- |
|
|
252
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
|
-
| upstream, such as 401 | `proxy_error` | `AI Gateway rejected the proxy's credential: <message>` | The gateway answered a
|
|
253
|
+
| upstream, such as 401 | `proxy_error` | `AI Gateway rejected the proxy's credential: <message>` | The gateway answered with an error typed `authentication_error`, for a call with a body or a bodyless one such as `GET /config`; see `onError` above. The other fields of the error are kept. |
|
|
254
254
|
| 400 | `invalid_request_error` | `Invalid path` | The path is not a safe relative path under `baseUrl`. |
|
|
255
255
|
| 400 | `invalid_request_error` | `Invalid JSON body` | A request other than `GET` or `HEAD` has a non-empty body that is not JSON. |
|
|
256
256
|
| 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. |
|
|
257
257
|
| 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. |
|
|
258
|
-
| 500 | `proxy_error` | `Error proxying request to AI Gateway` | Anything else that throws: `extractPath`, `beforeRequest`, `beforeRawRequest`, the upstream `fetch
|
|
258
|
+
| 500 | `proxy_error` | `Error proxying request to AI Gateway` | Anything else that throws: `extractPath`, `beforeRequest`, `beforeRawRequest`, the upstream `fetch`, or an `afterRawResponse` failure on an error response. |
|
|
259
259
|
| 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. |
|
|
260
|
-
| 424 | `proxy_error` | `Error processing the AI Gateway response` | After the gateway answered a non-streamed call with `afterResponse` set: `afterResponse` threw, or the success body was not a JSON object. The status is one AI SDK clients do not retry, since the gateway has billed the call. |
|
|
260
|
+
| 424 | `proxy_error` | `Error processing the AI Gateway response` | After the gateway answered a non-streamed call with `afterResponse` set: `afterResponse` threw, or the success body was not a JSON object. After a successful call with `afterRawResponse` set: the hook threw, returned anything but a `Response` or nothing, or read the body and returned no `Response`. The status is one AI SDK clients do not retry, since the gateway has billed the call. |
|
|
261
261
|
| 424 | `proxy_error` | `afterResponse hook returned an invalid response: <problem>` | `afterResponse` on a non-streamed request returned a result that fails the result validation above. |
|
|
262
262
|
| 500 | `proxy_error` | `No response body` | A successful streamed response arrived without a body. |
|
|
263
263
|
| 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`. |
|
package/dist/index.cjs
CHANGED
|
@@ -515,6 +515,7 @@ const NULL_BODY_STATUS = new Set([
|
|
|
515
515
|
const ANSWERED_CALL_FAILURE_STATUS = 424;
|
|
516
516
|
// Successful bodies that are a sequence of values rather than one JSON value: event streams and JSON Lines.
|
|
517
517
|
const STREAM_CONTENT_TYPE = /^\s*(?:text\/event-stream|application\/(?:x-ndjson|jsonl|jsonlines|x-jsonlines))\s*(?:;|$)/i;
|
|
518
|
+
const JSON_CONTENT_TYPE = /^\s*application\/(?:[\w.+-]+\+)?json\s*(?:;|$)/i;
|
|
518
519
|
/**
|
|
519
520
|
* Extracts the path from the request using either a custom extractor or route params.
|
|
520
521
|
*/ async function extractPathFromRequest(request, context, options) {
|
|
@@ -652,11 +653,35 @@ function proxyFailureResponse(request, error) {
|
|
|
652
653
|
if (isRedirect(res)) {
|
|
653
654
|
return redirectRefusedResponse(res);
|
|
654
655
|
}
|
|
655
|
-
return
|
|
656
|
+
return await bodylessUpstreamResponse(res);
|
|
656
657
|
} catch (error) {
|
|
657
658
|
return proxyFailureResponse(request, error);
|
|
658
659
|
}
|
|
659
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* Passes a bodyless request's response through, except that a JSON error typed
|
|
663
|
+
* `authentication_error` is retyped as for a call with a body. Only JSON error bodies are read.
|
|
664
|
+
*/ async function bodylessUpstreamResponse(res) {
|
|
665
|
+
const contentType = res.headers.get("content-type") ?? "application/json";
|
|
666
|
+
if (res.ok || !res.body || !JSON_CONTENT_TYPE.test(contentType)) {
|
|
667
|
+
return passThroughResponse(res);
|
|
668
|
+
}
|
|
669
|
+
const text = await res.text();
|
|
670
|
+
const parsed = parseJsonOrUndefined(text);
|
|
671
|
+
const body = isGatewayError(parsed) && parsed.error.type === "authentication_error" ? JSON.stringify(retypeProxyCredentialError(parsed)) : text;
|
|
672
|
+
return new Response(body, {
|
|
673
|
+
status: res.status,
|
|
674
|
+
statusText: res.statusText,
|
|
675
|
+
headers: responseHeaders(res, contentType)
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function parseJsonOrUndefined(text) {
|
|
679
|
+
try {
|
|
680
|
+
return JSON.parse(text);
|
|
681
|
+
} catch {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
660
685
|
function isGatewayError(value) {
|
|
661
686
|
if (typeof value !== "object" || value === null) {
|
|
662
687
|
return false;
|
|
@@ -886,7 +911,20 @@ async function runAfterResponse(res, requestBody, afterResponse) {
|
|
|
886
911
|
}
|
|
887
912
|
/**
|
|
888
913
|
* Runs afterRawResponse and returns the response to send.
|
|
889
|
-
|
|
914
|
+
* The gateway has answered, and billed, a successful call, so a failure there gets a status clients do not retry.
|
|
915
|
+
*/ async function applyAfterRawResponse(afterRawResponse, context, signal) {
|
|
916
|
+
try {
|
|
917
|
+
return await runAfterRawResponse(afterRawResponse, context);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
// The handler answers a client abort with 499, and a failed call costs nothing to retry.
|
|
920
|
+
if (signal.aborted || !context.response.ok) {
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
console.error("AI Gateway proxy failed after the gateway answered:", error);
|
|
924
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", "Error processing the AI Gateway response");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function runAfterRawResponse(afterRawResponse, context) {
|
|
890
928
|
let result;
|
|
891
929
|
try {
|
|
892
930
|
result = await afterRawResponse(context);
|
|
@@ -1014,7 +1052,7 @@ async function runAfterResponse(res, requestBody, afterResponse) {
|
|
|
1014
1052
|
...rawContext,
|
|
1015
1053
|
request: requestBody,
|
|
1016
1054
|
response
|
|
1017
|
-
});
|
|
1055
|
+
}, request.signal);
|
|
1018
1056
|
}
|
|
1019
1057
|
return response;
|
|
1020
1058
|
} catch (error) {
|
package/dist/index.mjs
CHANGED
|
@@ -513,6 +513,7 @@ const NULL_BODY_STATUS = new Set([
|
|
|
513
513
|
const ANSWERED_CALL_FAILURE_STATUS = 424;
|
|
514
514
|
// Successful bodies that are a sequence of values rather than one JSON value: event streams and JSON Lines.
|
|
515
515
|
const STREAM_CONTENT_TYPE = /^\s*(?:text\/event-stream|application\/(?:x-ndjson|jsonl|jsonlines|x-jsonlines))\s*(?:;|$)/i;
|
|
516
|
+
const JSON_CONTENT_TYPE = /^\s*application\/(?:[\w.+-]+\+)?json\s*(?:;|$)/i;
|
|
516
517
|
/**
|
|
517
518
|
* Extracts the path from the request using either a custom extractor or route params.
|
|
518
519
|
*/ async function extractPathFromRequest(request, context, options) {
|
|
@@ -650,11 +651,35 @@ function proxyFailureResponse(request, error) {
|
|
|
650
651
|
if (isRedirect(res)) {
|
|
651
652
|
return redirectRefusedResponse(res);
|
|
652
653
|
}
|
|
653
|
-
return
|
|
654
|
+
return await bodylessUpstreamResponse(res);
|
|
654
655
|
} catch (error) {
|
|
655
656
|
return proxyFailureResponse(request, error);
|
|
656
657
|
}
|
|
657
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Passes a bodyless request's response through, except that a JSON error typed
|
|
661
|
+
* `authentication_error` is retyped as for a call with a body. Only JSON error bodies are read.
|
|
662
|
+
*/ async function bodylessUpstreamResponse(res) {
|
|
663
|
+
const contentType = res.headers.get("content-type") ?? "application/json";
|
|
664
|
+
if (res.ok || !res.body || !JSON_CONTENT_TYPE.test(contentType)) {
|
|
665
|
+
return passThroughResponse(res);
|
|
666
|
+
}
|
|
667
|
+
const text = await res.text();
|
|
668
|
+
const parsed = parseJsonOrUndefined(text);
|
|
669
|
+
const body = isGatewayError(parsed) && parsed.error.type === "authentication_error" ? JSON.stringify(retypeProxyCredentialError(parsed)) : text;
|
|
670
|
+
return new Response(body, {
|
|
671
|
+
status: res.status,
|
|
672
|
+
statusText: res.statusText,
|
|
673
|
+
headers: responseHeaders(res, contentType)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
function parseJsonOrUndefined(text) {
|
|
677
|
+
try {
|
|
678
|
+
return JSON.parse(text);
|
|
679
|
+
} catch {
|
|
680
|
+
return undefined;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
658
683
|
function isGatewayError(value) {
|
|
659
684
|
if (typeof value !== "object" || value === null) {
|
|
660
685
|
return false;
|
|
@@ -884,7 +909,20 @@ async function runAfterResponse(res, requestBody, afterResponse) {
|
|
|
884
909
|
}
|
|
885
910
|
/**
|
|
886
911
|
* Runs afterRawResponse and returns the response to send.
|
|
887
|
-
|
|
912
|
+
* The gateway has answered, and billed, a successful call, so a failure there gets a status clients do not retry.
|
|
913
|
+
*/ async function applyAfterRawResponse(afterRawResponse, context, signal) {
|
|
914
|
+
try {
|
|
915
|
+
return await runAfterRawResponse(afterRawResponse, context);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
// The handler answers a client abort with 499, and a failed call costs nothing to retry.
|
|
918
|
+
if (signal.aborted || !context.response.ok) {
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
console.error("AI Gateway proxy failed after the gateway answered:", error);
|
|
922
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", "Error processing the AI Gateway response");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async function runAfterRawResponse(afterRawResponse, context) {
|
|
888
926
|
let result;
|
|
889
927
|
try {
|
|
890
928
|
result = await afterRawResponse(context);
|
|
@@ -1012,7 +1050,7 @@ async function runAfterResponse(res, requestBody, afterResponse) {
|
|
|
1012
1050
|
...rawContext,
|
|
1013
1051
|
request: requestBody,
|
|
1014
1052
|
response
|
|
1015
|
-
});
|
|
1053
|
+
}, request.signal);
|
|
1016
1054
|
}
|
|
1017
1055
|
return response;
|
|
1018
1056
|
} catch (error) {
|