ai-gateway-proxy 0.2.0 → 0.3.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 -4
- package/dist/index.cjs +18 -3
- package/dist/index.mjs +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ The path, whether from `extractPath` or the route param, must stay relative: one
|
|
|
171
171
|
|
|
172
172
|
### `afterResponse`
|
|
173
173
|
|
|
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
|
|
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 424 naming the problem instead of being sent. A hook that throws, or a gateway body that is not a JSON object, is a 424 too. The gateway has already answered, and billed, the call by then, and an AI SDK client retries a 5xx but not a 424, so the call is not paid for again.
|
|
175
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
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
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.
|
|
@@ -187,7 +187,7 @@ A result is rejected when it is not an object or when a field is missing or has
|
|
|
187
187
|
- `warnings`: absent or an array; `@ai-sdk/gateway` reads a missing one as none
|
|
188
188
|
- `providerMetadata`: absent or an object
|
|
189
189
|
|
|
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
|
|
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 424 `proxy_error`.
|
|
191
191
|
|
|
192
192
|
### `onError`
|
|
193
193
|
|
|
@@ -254,9 +254,10 @@ Apart from forwarding the gateway's own responses, the handler can answer with a
|
|
|
254
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
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
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
|
|
257
|
+
| 500 | `proxy_error` | `Error proxying request to AI Gateway` | Anything else that throws: `extractPath`, `beforeRequest`, `beforeRawRequest`, the upstream `fetch` or `afterRawResponse`, or an `afterRawResponse` that read the body and returned no `Response`. |
|
|
258
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
|
-
|
|
|
259
|
+
| 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` | `afterResponse hook returned an invalid response: <problem>` | `afterResponse` on a non-streamed request returned a result that fails the result validation above. |
|
|
260
261
|
| 500 | `proxy_error` | `No response body` | A successful streamed response arrived without a body. |
|
|
261
262
|
| 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
|
|
package/dist/index.cjs
CHANGED
|
@@ -511,6 +511,8 @@ const NULL_BODY_STATUS = new Set([
|
|
|
511
511
|
204,
|
|
512
512
|
205
|
|
513
513
|
]);
|
|
514
|
+
// AI SDK clients retry 408, 409, 429 and any 5xx, which would pay for the answered call again.
|
|
515
|
+
const ANSWERED_CALL_FAILURE_STATUS = 424;
|
|
514
516
|
// Successful bodies that are a sequence of values rather than one JSON value: event streams and JSON Lines.
|
|
515
517
|
const STREAM_CONTENT_TYPE = /^\s*(?:text\/event-stream|application\/(?:x-ndjson|jsonl|jsonlines|x-jsonlines))\s*(?:;|$)/i;
|
|
516
518
|
/**
|
|
@@ -773,7 +775,20 @@ function isGatewayError(value) {
|
|
|
773
775
|
}
|
|
774
776
|
/**
|
|
775
777
|
* Runs afterResponse on a non-streaming JSON response from the gateway.
|
|
776
|
-
|
|
778
|
+
* The gateway has answered, and billed, the call, so a failure here gets a status clients do not retry.
|
|
779
|
+
*/ async function handleJsonResponse(res, requestBody, afterResponse, signal) {
|
|
780
|
+
try {
|
|
781
|
+
return await runAfterResponse(res, requestBody, afterResponse);
|
|
782
|
+
} catch (error) {
|
|
783
|
+
// The handler answers a client abort with 499.
|
|
784
|
+
if (signal.aborted) {
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
787
|
+
console.error("AI Gateway proxy failed after the gateway answered:", error);
|
|
788
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", "Error processing the AI Gateway response");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function runAfterResponse(res, requestBody, afterResponse) {
|
|
777
792
|
const parsed = await res.json();
|
|
778
793
|
if (!isRecord(parsed)) {
|
|
779
794
|
throw new TypeError("AI Gateway returned a successful body that is not a JSON object");
|
|
@@ -793,7 +808,7 @@ function isGatewayError(value) {
|
|
|
793
808
|
if (problem) {
|
|
794
809
|
const message = `afterResponse hook returned an invalid response: ${problem}`;
|
|
795
810
|
console.error(message);
|
|
796
|
-
return proxyErrorResponse(
|
|
811
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", message);
|
|
797
812
|
}
|
|
798
813
|
// As on a stream's finish event: the AI SDK reads usage from every generate result, so it cannot be stripped.
|
|
799
814
|
responseBody = {
|
|
@@ -843,7 +858,7 @@ function isGatewayError(value) {
|
|
|
843
858
|
if (!afterResponse || STREAM_CONTENT_TYPE.test(res.headers.get("content-type") ?? "")) {
|
|
844
859
|
return passThroughResponse(res);
|
|
845
860
|
}
|
|
846
|
-
return await handleJsonResponse(res, requestBody, afterResponse);
|
|
861
|
+
return await handleJsonResponse(res, requestBody, afterResponse, signal);
|
|
847
862
|
}
|
|
848
863
|
/**
|
|
849
864
|
* Cancels a body nothing will read, since an unread fetch body holds its upstream connection.
|
package/dist/index.mjs
CHANGED
|
@@ -509,6 +509,8 @@ const NULL_BODY_STATUS = new Set([
|
|
|
509
509
|
204,
|
|
510
510
|
205
|
|
511
511
|
]);
|
|
512
|
+
// AI SDK clients retry 408, 409, 429 and any 5xx, which would pay for the answered call again.
|
|
513
|
+
const ANSWERED_CALL_FAILURE_STATUS = 424;
|
|
512
514
|
// Successful bodies that are a sequence of values rather than one JSON value: event streams and JSON Lines.
|
|
513
515
|
const STREAM_CONTENT_TYPE = /^\s*(?:text\/event-stream|application\/(?:x-ndjson|jsonl|jsonlines|x-jsonlines))\s*(?:;|$)/i;
|
|
514
516
|
/**
|
|
@@ -771,7 +773,20 @@ function isGatewayError(value) {
|
|
|
771
773
|
}
|
|
772
774
|
/**
|
|
773
775
|
* Runs afterResponse on a non-streaming JSON response from the gateway.
|
|
774
|
-
|
|
776
|
+
* The gateway has answered, and billed, the call, so a failure here gets a status clients do not retry.
|
|
777
|
+
*/ async function handleJsonResponse(res, requestBody, afterResponse, signal) {
|
|
778
|
+
try {
|
|
779
|
+
return await runAfterResponse(res, requestBody, afterResponse);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
// The handler answers a client abort with 499.
|
|
782
|
+
if (signal.aborted) {
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
console.error("AI Gateway proxy failed after the gateway answered:", error);
|
|
786
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", "Error processing the AI Gateway response");
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function runAfterResponse(res, requestBody, afterResponse) {
|
|
775
790
|
const parsed = await res.json();
|
|
776
791
|
if (!isRecord(parsed)) {
|
|
777
792
|
throw new TypeError("AI Gateway returned a successful body that is not a JSON object");
|
|
@@ -791,7 +806,7 @@ function isGatewayError(value) {
|
|
|
791
806
|
if (problem) {
|
|
792
807
|
const message = `afterResponse hook returned an invalid response: ${problem}`;
|
|
793
808
|
console.error(message);
|
|
794
|
-
return proxyErrorResponse(
|
|
809
|
+
return proxyErrorResponse(ANSWERED_CALL_FAILURE_STATUS, "proxy_error", message);
|
|
795
810
|
}
|
|
796
811
|
// As on a stream's finish event: the AI SDK reads usage from every generate result, so it cannot be stripped.
|
|
797
812
|
responseBody = {
|
|
@@ -841,7 +856,7 @@ function isGatewayError(value) {
|
|
|
841
856
|
if (!afterResponse || STREAM_CONTENT_TYPE.test(res.headers.get("content-type") ?? "")) {
|
|
842
857
|
return passThroughResponse(res);
|
|
843
858
|
}
|
|
844
|
-
return await handleJsonResponse(res, requestBody, afterResponse);
|
|
859
|
+
return await handleJsonResponse(res, requestBody, afterResponse, signal);
|
|
845
860
|
}
|
|
846
861
|
/**
|
|
847
862
|
* Cancels a body nothing will read, since an unread fetch body holds its upstream connection.
|