@hebo-ai/gateway 0.10.5 → 0.10.7
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/dist/endpoints/chat-completions/handler.js +2 -0
- package/dist/endpoints/chat-completions/schema.d.ts +5 -5
- package/dist/endpoints/embeddings/handler.js +2 -0
- package/dist/endpoints/messages/handler.js +2 -0
- package/dist/endpoints/responses/handler.js +2 -0
- package/dist/errors/ai-sdk.js +15 -7
- package/dist/errors/anthropic.d.ts +3 -2
- package/dist/errors/anthropic.js +10 -11
- package/dist/errors/gateway.d.ts +3 -2
- package/dist/errors/gateway.js +10 -4
- package/dist/errors/openai.d.ts +3 -2
- package/dist/errors/openai.js +8 -9
- package/dist/errors/utils.d.ts +4 -4
- package/dist/errors/utils.js +12 -12
- package/dist/lifecycle.js +9 -10
- package/dist/telemetry/gen-ai.js +2 -2
- package/dist/types.d.ts +3 -1
- package/dist/utils/headers.d.ts +5 -0
- package/dist/utils/headers.js +54 -7
- package/dist/utils/response.d.ts +3 -3
- package/dist/utils/response.js +13 -9
- package/dist/utils/stream.d.ts +1 -1
- package/dist/utils/stream.js +3 -11
- package/package.json +1 -1
|
@@ -133,6 +133,8 @@ export const chatCompletions = (config) => {
|
|
|
133
133
|
});
|
|
134
134
|
logger.trace({ requestId: ctx.requestId, result }, "[chat] AI SDK result");
|
|
135
135
|
addSpanEvent("hebo.ai-sdk.completed");
|
|
136
|
+
if (result.response.headers)
|
|
137
|
+
ctx.response = { headers: result.response.headers };
|
|
136
138
|
recordTimeToFirstToken(performance.now() - start, genAiGeneralAttrs, ctx.trace);
|
|
137
139
|
// Transform result.
|
|
138
140
|
ctx.result = toChatCompletions(result, ctx.resolvedModelId);
|
|
@@ -1055,8 +1055,8 @@ export declare const ChatCompletionsBodySchema: z.ZodObject<{
|
|
|
1055
1055
|
export type ChatCompletionsBody = z.infer<typeof ChatCompletionsBodySchema>;
|
|
1056
1056
|
export declare const ChatCompletionsFinishReasonSchema: z.ZodEnum<{
|
|
1057
1057
|
length: "length";
|
|
1058
|
-
stop: "stop";
|
|
1059
1058
|
tool_calls: "tool_calls";
|
|
1059
|
+
stop: "stop";
|
|
1060
1060
|
content_filter: "content_filter";
|
|
1061
1061
|
}>;
|
|
1062
1062
|
export type ChatCompletionsFinishReason = z.infer<typeof ChatCompletionsFinishReasonSchema>;
|
|
@@ -1109,8 +1109,8 @@ export declare const ChatCompletionsChoiceSchema: z.ZodObject<{
|
|
|
1109
1109
|
}, z.core.$strip>;
|
|
1110
1110
|
finish_reason: z.ZodEnum<{
|
|
1111
1111
|
length: "length";
|
|
1112
|
-
stop: "stop";
|
|
1113
1112
|
tool_calls: "tool_calls";
|
|
1113
|
+
stop: "stop";
|
|
1114
1114
|
content_filter: "content_filter";
|
|
1115
1115
|
}>;
|
|
1116
1116
|
logprobs: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -1183,8 +1183,8 @@ export declare const ChatCompletionsSchema: z.ZodObject<{
|
|
|
1183
1183
|
}, z.core.$strip>;
|
|
1184
1184
|
finish_reason: z.ZodEnum<{
|
|
1185
1185
|
length: "length";
|
|
1186
|
-
stop: "stop";
|
|
1187
1186
|
tool_calls: "tool_calls";
|
|
1187
|
+
stop: "stop";
|
|
1188
1188
|
content_filter: "content_filter";
|
|
1189
1189
|
}>;
|
|
1190
1190
|
logprobs: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -1319,8 +1319,8 @@ export declare const ChatCompletionsChoiceDeltaSchema: z.ZodObject<{
|
|
|
1319
1319
|
}, z.core.$strip>;
|
|
1320
1320
|
finish_reason: z.ZodNullable<z.ZodEnum<{
|
|
1321
1321
|
length: "length";
|
|
1322
|
-
stop: "stop";
|
|
1323
1322
|
tool_calls: "tool_calls";
|
|
1323
|
+
stop: "stop";
|
|
1324
1324
|
content_filter: "content_filter";
|
|
1325
1325
|
}>>;
|
|
1326
1326
|
logprobs: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -1381,8 +1381,8 @@ export declare const ChatCompletionsChunkSchema: z.ZodObject<{
|
|
|
1381
1381
|
}, z.core.$strip>;
|
|
1382
1382
|
finish_reason: z.ZodNullable<z.ZodEnum<{
|
|
1383
1383
|
length: "length";
|
|
1384
|
-
stop: "stop";
|
|
1385
1384
|
tool_calls: "tool_calls";
|
|
1385
|
+
stop: "stop";
|
|
1386
1386
|
content_filter: "content_filter";
|
|
1387
1387
|
}>>;
|
|
1388
1388
|
logprobs: z.ZodOptional<z.ZodUnknown>;
|
|
@@ -81,6 +81,8 @@ export const embeddings = (config) => {
|
|
|
81
81
|
});
|
|
82
82
|
logger.trace({ requestId: ctx.requestId, result }, "[embeddings] AI SDK result");
|
|
83
83
|
addSpanEvent("hebo.ai-sdk.completed");
|
|
84
|
+
if (result.responses?.[0]?.headers)
|
|
85
|
+
ctx.response = { headers: result.responses[0].headers };
|
|
84
86
|
// Transform result.
|
|
85
87
|
ctx.result = toEmbeddings(result, ctx.modelId);
|
|
86
88
|
logger.trace({ requestId: ctx.requestId, result: ctx.result }, "[chat] Embeddings");
|
|
@@ -124,6 +124,8 @@ export const messages = (config) => {
|
|
|
124
124
|
});
|
|
125
125
|
logger.trace({ requestId: ctx.requestId, result }, "[messages] AI SDK result");
|
|
126
126
|
addSpanEvent("hebo.ai-sdk.completed");
|
|
127
|
+
if (result.response.headers)
|
|
128
|
+
ctx.response = { headers: result.response.headers };
|
|
127
129
|
recordTimeToFirstToken(performance.now() - start, genAiGeneralAttrs, ctx.trace);
|
|
128
130
|
ctx.result = toMessages(result, ctx.resolvedModelId);
|
|
129
131
|
logger.trace({ requestId: ctx.requestId, result: ctx.result }, "[messages] Messages");
|
|
@@ -123,6 +123,8 @@ export const responses = (config) => {
|
|
|
123
123
|
});
|
|
124
124
|
logger.trace({ requestId: ctx.requestId, result }, "[responses] AI SDK result");
|
|
125
125
|
addSpanEvent("hebo.ai-sdk.completed");
|
|
126
|
+
if (result.response.headers)
|
|
127
|
+
ctx.response = { headers: result.response.headers };
|
|
126
128
|
recordTimeToFirstToken(performance.now() - start, genAiGeneralAttrs, ctx.trace);
|
|
127
129
|
ctx.result = toResponses(result, ctx.resolvedModelId, ctx.body.metadata);
|
|
128
130
|
logger.trace({ requestId: ctx.requestId, result: ctx.result }, "[responses] Responses");
|
package/dist/errors/ai-sdk.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { AISDKError, APICallError, DownloadError, EmptyResponseBodyError, InvalidArgumentError, InvalidDataContentError, InvalidMessageRoleError, InvalidPromptError, InvalidResponseDataError, InvalidStreamPartError, InvalidToolApprovalError, InvalidToolInputError, JSONParseError, LoadAPIKeyError, LoadSettingError, MessageConversionError, MissingToolResultsError, NoContentGeneratedError, NoImageGeneratedError, NoObjectGeneratedError, NoOutputGeneratedError, NoSpeechGeneratedError, NoSuchModelError, NoSuchProviderError, NoSuchToolError, NoTranscriptGeneratedError, NoVideoGeneratedError, RetryError, ToolCallNotFoundForApprovalError, ToolCallRepairError, TooManyEmbeddingValuesForCallError, TypeValidationError, UIMessageStreamError, UnsupportedModelVersionError, UnsupportedFunctionalityError, } from "ai";
|
|
2
2
|
import { GatewayError } from "./gateway";
|
|
3
|
-
import {
|
|
3
|
+
import { STATUS_TEXT } from "./utils";
|
|
4
|
+
const normalizeApiCallError = (error) => {
|
|
5
|
+
const status = error.statusCode ?? (error.isRetryable ? 502 : 422);
|
|
6
|
+
const statusText = `UPSTREAM_${STATUS_TEXT(status)}`;
|
|
7
|
+
return new GatewayError(error, status, statusText, undefined, error.responseHeaders ?? undefined);
|
|
8
|
+
};
|
|
4
9
|
export const normalizeAiSdkError = (error) => {
|
|
5
10
|
if (APICallError.isInstance(error)) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
return normalizeApiCallError(error);
|
|
12
|
+
}
|
|
13
|
+
if (RetryError.isInstance(error)) {
|
|
14
|
+
if (APICallError.isInstance(error.lastError)) {
|
|
15
|
+
return normalizeApiCallError(error.lastError);
|
|
16
|
+
}
|
|
17
|
+
return new GatewayError(error, 502, `UPSTREAM_${STATUS_TEXT(502)}`);
|
|
9
18
|
}
|
|
10
19
|
if (JSONParseError.isInstance(error) ||
|
|
11
20
|
InvalidResponseDataError.isInstance(error) ||
|
|
@@ -15,7 +24,6 @@ export const normalizeAiSdkError = (error) => {
|
|
|
15
24
|
NoOutputGeneratedError.isInstance(error) ||
|
|
16
25
|
InvalidStreamPartError.isInstance(error) ||
|
|
17
26
|
UIMessageStreamError.isInstance(error) ||
|
|
18
|
-
RetryError.isInstance(error) ||
|
|
19
27
|
DownloadError.isInstance(error) ||
|
|
20
28
|
ToolCallRepairError.isInstance(error) ||
|
|
21
29
|
NoImageGeneratedError.isInstance(error) ||
|
|
@@ -23,7 +31,7 @@ export const normalizeAiSdkError = (error) => {
|
|
|
23
31
|
NoSpeechGeneratedError.isInstance(error) ||
|
|
24
32
|
NoTranscriptGeneratedError.isInstance(error) ||
|
|
25
33
|
NoVideoGeneratedError.isInstance(error)) {
|
|
26
|
-
return new GatewayError(error, 502, `UPSTREAM_${
|
|
34
|
+
return new GatewayError(error, 502, `UPSTREAM_${STATUS_TEXT(502)}`);
|
|
27
35
|
}
|
|
28
36
|
if (InvalidArgumentError.isInstance(error) ||
|
|
29
37
|
InvalidPromptError.isInstance(error) ||
|
|
@@ -40,7 +48,7 @@ export const normalizeAiSdkError = (error) => {
|
|
|
40
48
|
TooManyEmbeddingValuesForCallError.isInstance(error) ||
|
|
41
49
|
NoSuchModelError.isInstance(error) ||
|
|
42
50
|
NoSuchProviderError.isInstance(error)) {
|
|
43
|
-
return new GatewayError(error, 422, `UPSTREAM_${
|
|
51
|
+
return new GatewayError(error, 422, `UPSTREAM_${STATUS_TEXT(422)}`);
|
|
44
52
|
}
|
|
45
53
|
if (LoadSettingError.isInstance(error) || LoadAPIKeyError.isInstance(error)) {
|
|
46
54
|
return new GatewayError(error, 500);
|
|
@@ -9,7 +9,8 @@ export declare const AnthropicErrorSchema: z.ZodObject<{
|
|
|
9
9
|
export declare class AnthropicError {
|
|
10
10
|
readonly type: "error";
|
|
11
11
|
readonly error: z.infer<typeof AnthropicErrorSchema>["error"];
|
|
12
|
+
status: number;
|
|
12
13
|
constructor(message: string, type?: string);
|
|
13
14
|
}
|
|
14
|
-
export declare function toAnthropicError(error: unknown): AnthropicError;
|
|
15
|
-
export declare function toAnthropicErrorResponse(error: unknown,
|
|
15
|
+
export declare function toAnthropicError(error: unknown, requestId?: string): AnthropicError;
|
|
16
|
+
export declare function toAnthropicErrorResponse(error: unknown, init: ResponseInit): Response;
|
package/dist/errors/anthropic.js
CHANGED
|
@@ -14,6 +14,8 @@ export class AnthropicError {
|
|
|
14
14
|
error;
|
|
15
15
|
constructor(message, type = "api_error") {
|
|
16
16
|
this.error = { type, message };
|
|
17
|
+
// internal property to derive status from error handlers without breaking official format
|
|
18
|
+
Object.defineProperty(this, "status", { value: 500, writable: true });
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
const mapType = (status) => {
|
|
@@ -22,12 +24,12 @@ const mapType = (status) => {
|
|
|
22
24
|
return "invalid_request_error";
|
|
23
25
|
case 401:
|
|
24
26
|
return "authentication_error";
|
|
27
|
+
case 402:
|
|
28
|
+
return "billing_error";
|
|
25
29
|
case 403:
|
|
26
30
|
return "permission_error";
|
|
27
31
|
case 404:
|
|
28
32
|
return "not_found_error";
|
|
29
|
-
case 402:
|
|
30
|
-
return "billing_error";
|
|
31
33
|
case 413:
|
|
32
34
|
return "request_too_large";
|
|
33
35
|
case 429:
|
|
@@ -40,15 +42,12 @@ const mapType = (status) => {
|
|
|
40
42
|
return status >= 500 ? "api_error" : "invalid_request_error";
|
|
41
43
|
}
|
|
42
44
|
};
|
|
43
|
-
export function toAnthropicError(error) {
|
|
45
|
+
export function toAnthropicError(error, requestId) {
|
|
44
46
|
const meta = getErrorMeta(error);
|
|
45
|
-
|
|
47
|
+
const anthropicError = new AnthropicError(maybeMaskMessage(error instanceof Error ? error.message : String(error), meta.status, requestId), mapType(meta.status));
|
|
48
|
+
anthropicError.status = meta.status;
|
|
49
|
+
return anthropicError;
|
|
46
50
|
}
|
|
47
|
-
export function toAnthropicErrorResponse(error,
|
|
48
|
-
|
|
49
|
-
return toResponse(new AnthropicError(maybeMaskMessage(meta, resolveRequestId(responseInit)), mapType(meta.status)), {
|
|
50
|
-
status: meta.status,
|
|
51
|
-
statusText: meta.code,
|
|
52
|
-
headers: responseInit?.headers,
|
|
53
|
-
});
|
|
51
|
+
export function toAnthropicErrorResponse(error, init) {
|
|
52
|
+
return toResponse(new AnthropicError(maybeMaskMessage(error instanceof Error ? error.message : String(error), init.status ?? 500, resolveRequestId(init)), mapType(init.status ?? 500)), init);
|
|
54
53
|
}
|
package/dist/errors/gateway.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare class GatewayError extends Error {
|
|
2
2
|
readonly status: number;
|
|
3
|
-
readonly
|
|
4
|
-
|
|
3
|
+
readonly statusText: string;
|
|
4
|
+
readonly headers: Record<string, string> | undefined;
|
|
5
|
+
constructor(error: unknown, status: number, statusText?: string, cause?: unknown, headers?: Record<string, string>);
|
|
5
6
|
}
|
package/dist/errors/gateway.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { X_SHOULD_RETRY_HEADER } from "../utils/headers";
|
|
2
|
+
import { STATUS_TEXT } from "./utils";
|
|
2
3
|
export class GatewayError extends Error {
|
|
3
4
|
status;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
statusText;
|
|
6
|
+
headers;
|
|
7
|
+
constructor(error, status, statusText, cause, headers) {
|
|
6
8
|
const isError = error instanceof Error;
|
|
7
9
|
super(isError ? error.message : String(error));
|
|
8
10
|
this.name = "GatewayError";
|
|
9
11
|
this.cause = cause ?? (isError ? error : undefined);
|
|
10
12
|
this.status = status;
|
|
11
|
-
this.
|
|
13
|
+
this.statusText = statusText ?? STATUS_TEXT(status);
|
|
14
|
+
this.headers = headers;
|
|
15
|
+
if (!this.statusText.startsWith("UPSTREAM_")) {
|
|
16
|
+
(this.headers ??= {})[X_SHOULD_RETRY_HEADER] = "false";
|
|
17
|
+
}
|
|
12
18
|
}
|
|
13
19
|
}
|
package/dist/errors/openai.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ export declare const OpenAIErrorSchema: z.ZodObject<{
|
|
|
9
9
|
}, z.core.$strip>;
|
|
10
10
|
export declare class OpenAIError {
|
|
11
11
|
readonly error: z.infer<typeof OpenAIErrorSchema>["error"];
|
|
12
|
+
status: number;
|
|
12
13
|
constructor(message: string, type?: string, code?: string, param?: string);
|
|
13
14
|
}
|
|
14
|
-
export declare function toOpenAIError(error: unknown): OpenAIError;
|
|
15
|
-
export declare function toOpenAIErrorResponse(error: unknown,
|
|
15
|
+
export declare function toOpenAIError(error: unknown, requestId?: string): OpenAIError;
|
|
16
|
+
export declare function toOpenAIErrorResponse(error: unknown, init: ResponseInit): Response;
|
package/dist/errors/openai.js
CHANGED
|
@@ -14,18 +14,17 @@ export class OpenAIError {
|
|
|
14
14
|
error;
|
|
15
15
|
constructor(message, type = "server_error", code, param = "") {
|
|
16
16
|
this.error = { message, type, code: code?.toLowerCase(), param };
|
|
17
|
+
// internal property to derive status from error handlers without breaking official format
|
|
18
|
+
Object.defineProperty(this, "status", { value: 500, writable: true });
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
const mapType = (status) => (status < 500 ? "invalid_request_error" : "server_error");
|
|
20
|
-
export function toOpenAIError(error) {
|
|
22
|
+
export function toOpenAIError(error, requestId) {
|
|
21
23
|
const meta = getErrorMeta(error);
|
|
22
|
-
|
|
24
|
+
const openAIError = new OpenAIError(maybeMaskMessage(error instanceof Error ? error.message : String(error), meta.status, requestId), mapType(meta.status), meta.statusText);
|
|
25
|
+
openAIError.status = meta.status;
|
|
26
|
+
return openAIError;
|
|
23
27
|
}
|
|
24
|
-
export function toOpenAIErrorResponse(error,
|
|
25
|
-
|
|
26
|
-
return toResponse(new OpenAIError(maybeMaskMessage(meta, resolveRequestId(responseInit)), mapType(meta.status), meta.code), {
|
|
27
|
-
...responseInit,
|
|
28
|
-
status: meta.status,
|
|
29
|
-
statusText: meta.code,
|
|
30
|
-
});
|
|
28
|
+
export function toOpenAIErrorResponse(error, init) {
|
|
29
|
+
return toResponse(new OpenAIError(maybeMaskMessage(error instanceof Error ? error.message : String(error), init.status ?? 500, resolveRequestId(init)), mapType(init.status ?? 500), init.statusText ?? "INTERNAL_SERVER_ERROR"), init);
|
|
31
30
|
}
|
package/dist/errors/utils.d.ts
CHANGED
|
@@ -16,11 +16,11 @@ export declare const STATUS_CODES: {
|
|
|
16
16
|
readonly 503: "SERVICE_UNAVAILABLE";
|
|
17
17
|
readonly 504: "GATEWAY_TIMEOUT";
|
|
18
18
|
};
|
|
19
|
-
export declare const
|
|
19
|
+
export declare const STATUS_TEXT: (status: number) => "BAD_REQUEST" | "UNAUTHORIZED" | "PAYMENT_REQUIRED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_ALLOWED" | "CONFLICT" | "PAYLOAD_TOO_LARGE" | "UNSUPPORTED_MEDIA_TYPE" | "UNPROCESSABLE_ENTITY" | "TOO_MANY_REQUESTS" | "CLIENT_CLOSED_REQUEST" | "INTERNAL_SERVER_ERROR" | "BAD_GATEWAY" | "SERVICE_UNAVAILABLE" | "GATEWAY_TIMEOUT";
|
|
20
20
|
export type ErrorMeta = {
|
|
21
21
|
status: number;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
statusText: string;
|
|
23
|
+
headers: Record<string, string>;
|
|
24
24
|
};
|
|
25
25
|
export declare function getErrorMeta(error: unknown): ErrorMeta;
|
|
26
|
-
export declare function maybeMaskMessage(
|
|
26
|
+
export declare function maybeMaskMessage(message: string, status: number, requestId?: string): string;
|
package/dist/errors/utils.js
CHANGED
|
@@ -19,37 +19,37 @@ export const STATUS_CODES = {
|
|
|
19
19
|
503: "SERVICE_UNAVAILABLE",
|
|
20
20
|
504: "GATEWAY_TIMEOUT",
|
|
21
21
|
};
|
|
22
|
-
export const
|
|
22
|
+
export const STATUS_TEXT = (status) => {
|
|
23
23
|
const label = STATUS_CODES[status];
|
|
24
24
|
if (label)
|
|
25
25
|
return label;
|
|
26
26
|
return status >= 400 && status < 500 ? STATUS_CODES[400] : STATUS_CODES[500];
|
|
27
27
|
};
|
|
28
|
-
// FUTURE: always return a wrapped GatewayError?
|
|
29
28
|
export function getErrorMeta(error) {
|
|
30
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
31
29
|
let status;
|
|
32
|
-
let
|
|
30
|
+
let statusText;
|
|
31
|
+
let headers;
|
|
33
32
|
if (error instanceof GatewayError) {
|
|
34
|
-
({ status,
|
|
33
|
+
({ status, statusText, headers } = error);
|
|
35
34
|
}
|
|
36
35
|
else {
|
|
37
36
|
const normalized = normalizeAiSdkError(error);
|
|
38
37
|
if (normalized) {
|
|
39
|
-
({ status,
|
|
38
|
+
({ status, statusText, headers } = normalized);
|
|
40
39
|
}
|
|
41
40
|
else {
|
|
42
41
|
status = 500;
|
|
43
|
-
|
|
42
|
+
statusText = STATUS_TEXT(status);
|
|
43
|
+
headers = {};
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
return { status,
|
|
46
|
+
return { status, statusText, headers: headers ?? {} };
|
|
47
47
|
}
|
|
48
|
-
export function maybeMaskMessage(
|
|
48
|
+
export function maybeMaskMessage(message, status, requestId) {
|
|
49
49
|
// FUTURE: consider masking all upstream errors, also 4xx
|
|
50
|
-
if (!(isProduction() &&
|
|
51
|
-
return
|
|
50
|
+
if (!(isProduction() && status >= 500)) {
|
|
51
|
+
return message;
|
|
52
52
|
}
|
|
53
53
|
// FUTURE: always attach requestId to errors (masked and unmasked)
|
|
54
|
-
return `${
|
|
54
|
+
return `${STATUS_TEXT(status)} (${requestId ?? "see requestId in response headers"})`;
|
|
55
55
|
}
|
package/dist/lifecycle.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseConfig } from "./config";
|
|
|
2
2
|
import { toAnthropicError, toAnthropicErrorResponse } from "./errors/anthropic";
|
|
3
3
|
import { GatewayError } from "./errors/gateway";
|
|
4
4
|
import { toOpenAIError, toOpenAIErrorResponse } from "./errors/openai";
|
|
5
|
+
import { getErrorMeta } from "./errors/utils";
|
|
5
6
|
import { logger } from "./logger";
|
|
6
7
|
import { getBaggageAttributes } from "./telemetry/baggage";
|
|
7
8
|
import { instrumentFetch } from "./telemetry/fetch";
|
|
@@ -53,8 +54,7 @@ export const winterCgHandler = (run, config) => {
|
|
|
53
54
|
requestId: ctx.requestId,
|
|
54
55
|
err: reason ?? ctx.request.signal.reason,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
-
span.recordError(reason, realStatus >= 500 || isUpstreamError);
|
|
57
|
+
span.recordError(reason, true);
|
|
58
58
|
}
|
|
59
59
|
span.setAttributes({ "http.response.status_code_effective": realStatus });
|
|
60
60
|
if (ctx.operation === "chat" ||
|
|
@@ -76,10 +76,10 @@ export const winterCgHandler = (run, config) => {
|
|
|
76
76
|
}
|
|
77
77
|
if (!ctx.response) {
|
|
78
78
|
ctx.result = (await run(ctx, parsedConfig));
|
|
79
|
-
const
|
|
80
|
-
ctx.response = toResponse(ctx.result, prepareResponseInit(ctx.requestId), {
|
|
79
|
+
const toError = ctx.operation === "messages" ? toAnthropicError : toOpenAIError;
|
|
80
|
+
ctx.response = toResponse(ctx.result, prepareResponseInit(ctx.requestId, ctx.response), {
|
|
81
81
|
onDone: finalize,
|
|
82
|
-
|
|
82
|
+
toError: (error) => toError(error, ctx.requestId),
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
85
|
if (parsedConfig.hooks?.onResponse) {
|
|
@@ -111,11 +111,10 @@ export const winterCgHandler = (run, config) => {
|
|
|
111
111
|
const errorPayload = ctx.request.signal.aborted
|
|
112
112
|
? new GatewayError(error ?? ctx.request.signal.reason, 499)
|
|
113
113
|
: error;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
ctx.
|
|
117
|
-
|
|
118
|
-
: toOpenAIErrorResponse(errorPayload, errorResponseInit);
|
|
114
|
+
if (!(ctx.response instanceof Response)) {
|
|
115
|
+
const toErrorResponse = ctx.operation === "messages" ? toAnthropicErrorResponse : toOpenAIErrorResponse;
|
|
116
|
+
ctx.response = toErrorResponse(errorPayload, prepareResponseInit(ctx.requestId, getErrorMeta(errorPayload)));
|
|
117
|
+
}
|
|
119
118
|
finalize(ctx.response.status, error);
|
|
120
119
|
}
|
|
121
120
|
});
|
package/dist/telemetry/gen-ai.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { metrics } from "@opentelemetry/api";
|
|
2
|
-
import {
|
|
2
|
+
import { STATUS_TEXT } from "../errors/utils";
|
|
3
3
|
const getMeter = () => metrics.getMeter("@hebo/gateway");
|
|
4
4
|
let requestDurationHistogram;
|
|
5
5
|
let timePerOutputTokenHistogram;
|
|
@@ -75,7 +75,7 @@ export const recordRequestDuration = (duration, status, ctx, signalLevel) => {
|
|
|
75
75
|
return;
|
|
76
76
|
const attrs = getGenAiGeneralAttributes(ctx, signalLevel);
|
|
77
77
|
if (status !== 200) {
|
|
78
|
-
attrs["error.type"] = `${status} ${
|
|
78
|
+
attrs["error.type"] = `${status} ${STATUS_TEXT(status).toLowerCase()}`;
|
|
79
79
|
}
|
|
80
80
|
getRequestDurationHistogram().record(duration / 1000, attrs);
|
|
81
81
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -69,8 +69,10 @@ export type GatewayContext = {
|
|
|
69
69
|
result?: ChatCompletions | ChatCompletionsStream | Embeddings | Messages | MessagesStream | Model | ModelList | Responses | ResponsesStream;
|
|
70
70
|
/**
|
|
71
71
|
* Response object returned by the handler.
|
|
72
|
+
* Handlers may set this to a `ResponseInit` containing upstream response
|
|
73
|
+
* headers; the lifecycle merges allowlisted headers into the final `Response`.
|
|
72
74
|
*/
|
|
73
|
-
response?: Response;
|
|
75
|
+
response?: Response | ResponseInit;
|
|
74
76
|
/**
|
|
75
77
|
* Per-request telemetry signal level override.
|
|
76
78
|
* When set (via body parameter or hook), overrides `cfg.telemetry.signals.gen_ai`
|
package/dist/utils/headers.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
2
|
+
export declare const RETRY_AFTER_HEADER = "retry-after";
|
|
3
|
+
export declare const RETRY_AFTER_MS_HEADER = "retry-after-ms";
|
|
4
|
+
export declare const X_SHOULD_RETRY_HEADER = "x-should-retry";
|
|
2
5
|
type HeaderSource = Request | ResponseInit | undefined;
|
|
3
6
|
export declare const resolveRequestId: (source: HeaderSource) => string | undefined;
|
|
7
|
+
export declare const filterResponseHeaders: (upstream?: HeadersInit) => Record<string, string>;
|
|
8
|
+
export declare const buildRetryHeaders: (status: number, upstream?: Record<string, string>) => Record<string, string>;
|
|
4
9
|
export {};
|
package/dist/utils/headers.js
CHANGED
|
@@ -1,22 +1,69 @@
|
|
|
1
1
|
export const REQUEST_ID_HEADER = "x-request-id";
|
|
2
|
+
export const RETRY_AFTER_HEADER = "retry-after";
|
|
3
|
+
export const RETRY_AFTER_MS_HEADER = "retry-after-ms";
|
|
4
|
+
export const X_SHOULD_RETRY_HEADER = "x-should-retry";
|
|
5
|
+
const RESPONSE_HEADER_ALLOWLIST = [
|
|
6
|
+
RETRY_AFTER_HEADER,
|
|
7
|
+
RETRY_AFTER_MS_HEADER,
|
|
8
|
+
X_SHOULD_RETRY_HEADER,
|
|
9
|
+
];
|
|
10
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 409, 429, 500, 502, 503, 504]);
|
|
11
|
+
const DEFAULT_RETRY_AFTER_MS = 1000;
|
|
2
12
|
export const resolveRequestId = (source) => {
|
|
3
13
|
if (!source)
|
|
4
14
|
return undefined;
|
|
5
15
|
if (source instanceof Request) {
|
|
6
16
|
return source.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
7
17
|
}
|
|
8
|
-
|
|
9
|
-
if (!headers)
|
|
18
|
+
if (!source.headers)
|
|
10
19
|
return undefined;
|
|
20
|
+
return getHeader(source.headers, REQUEST_ID_HEADER);
|
|
21
|
+
};
|
|
22
|
+
function getHeader(headers, key) {
|
|
11
23
|
if (headers instanceof Headers) {
|
|
12
|
-
return headers.get(
|
|
24
|
+
return headers.get(key) ?? undefined;
|
|
13
25
|
}
|
|
14
26
|
if (Array.isArray(headers)) {
|
|
15
|
-
for (const [
|
|
16
|
-
if (
|
|
17
|
-
return
|
|
27
|
+
for (const [k, v] of headers) {
|
|
28
|
+
if (k.toLowerCase() === key.toLowerCase()) {
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return headers[key] ?? headers[key.toLowerCase()];
|
|
35
|
+
}
|
|
36
|
+
export const filterResponseHeaders = (upstream) => {
|
|
37
|
+
if (!upstream)
|
|
38
|
+
return {};
|
|
39
|
+
const filtered = {};
|
|
40
|
+
for (const key of RESPONSE_HEADER_ALLOWLIST) {
|
|
41
|
+
const value = getHeader(upstream, key);
|
|
42
|
+
if (value !== undefined) {
|
|
43
|
+
filtered[key] = value;
|
|
18
44
|
}
|
|
45
|
+
}
|
|
46
|
+
return filtered;
|
|
47
|
+
};
|
|
48
|
+
function deriveRetryAfterMs(retryAfter) {
|
|
49
|
+
if (retryAfter === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
const num = Number(retryAfter);
|
|
52
|
+
if (Number.isFinite(num) && num > 0)
|
|
53
|
+
return num * 1000;
|
|
54
|
+
const dateMs = Date.parse(retryAfter);
|
|
55
|
+
if (!Number.isFinite(dateMs))
|
|
19
56
|
return undefined;
|
|
57
|
+
const deltaMs = dateMs - Date.now();
|
|
58
|
+
return deltaMs > 0 ? deltaMs : undefined;
|
|
59
|
+
}
|
|
60
|
+
export const buildRetryHeaders = (status, upstream = {}) => {
|
|
61
|
+
if (!RETRYABLE_STATUS_CODES.has(status)) {
|
|
62
|
+
upstream[X_SHOULD_RETRY_HEADER] = "false";
|
|
63
|
+
return upstream;
|
|
20
64
|
}
|
|
21
|
-
|
|
65
|
+
upstream[RETRY_AFTER_MS_HEADER] ??= String(deriveRetryAfterMs(upstream[RETRY_AFTER_HEADER]) ?? DEFAULT_RETRY_AFTER_MS);
|
|
66
|
+
upstream[RETRY_AFTER_HEADER] = String(Math.ceil((Number(upstream[RETRY_AFTER_MS_HEADER]) || DEFAULT_RETRY_AFTER_MS) / 1000));
|
|
67
|
+
upstream[X_SHOULD_RETRY_HEADER] ??= "true";
|
|
68
|
+
return upstream;
|
|
22
69
|
};
|
package/dist/utils/response.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SseFrame } from "./stream";
|
|
2
|
-
export declare const prepareResponseInit: (requestId: string) => ResponseInit;
|
|
3
|
-
export declare const mergeResponseInit: (
|
|
2
|
+
export declare const prepareResponseInit: (requestId: string, upstream?: ResponseInit) => ResponseInit;
|
|
3
|
+
export declare const mergeResponseInit: (headers: Record<string, string>, responseInit?: ResponseInit) => ResponseInit;
|
|
4
4
|
export declare const toResponse: (result: ReadableStream<SseFrame> | Uint8Array<ArrayBuffer> | object | string, responseInit?: ResponseInit, streamOptions?: {
|
|
5
5
|
onDone?: (status: number, reason?: unknown) => void;
|
|
6
|
-
|
|
6
|
+
toError?: (error: unknown) => unknown;
|
|
7
7
|
}) => Response;
|
package/dist/utils/response.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { REQUEST_ID_HEADER } from "./headers";
|
|
1
|
+
import { buildRetryHeaders, filterResponseHeaders, REQUEST_ID_HEADER } from "./headers";
|
|
2
2
|
import { toSseStream } from "./stream";
|
|
3
3
|
const TEXT_ENCODER = new TextEncoder();
|
|
4
|
-
export const prepareResponseInit = (requestId) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export const prepareResponseInit = (requestId, upstream) => {
|
|
5
|
+
const init = upstream ?? {};
|
|
6
|
+
init.headers = filterResponseHeaders(upstream?.headers);
|
|
7
|
+
if (init.status && init.status >= 400)
|
|
8
|
+
init.headers = buildRetryHeaders(init.status, init.headers);
|
|
9
|
+
init.headers[REQUEST_ID_HEADER] = requestId;
|
|
10
|
+
return init;
|
|
11
|
+
};
|
|
12
|
+
export const mergeResponseInit = (headers, responseInit) => {
|
|
13
|
+
if (!responseInit)
|
|
14
|
+
return { headers };
|
|
9
15
|
const override = responseInit?.headers;
|
|
10
16
|
if (override) {
|
|
11
17
|
new Headers(override).forEach((value, key) => {
|
|
12
|
-
headers
|
|
18
|
+
headers[key] = value;
|
|
13
19
|
});
|
|
14
20
|
}
|
|
15
|
-
if (!responseInit)
|
|
16
|
-
return { headers };
|
|
17
21
|
return {
|
|
18
22
|
status: responseInit.status,
|
|
19
23
|
statusText: responseInit.statusText,
|
package/dist/utils/stream.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ export type SseFrame<T = unknown, E extends string | undefined = string | undefi
|
|
|
5
5
|
export type SseErrorFrame = SseFrame<Error, "error" | undefined>;
|
|
6
6
|
export declare function toSseStream(src: ReadableStream<SseFrame>, options?: {
|
|
7
7
|
onDone?: (status: number, reason?: unknown) => void;
|
|
8
|
+
toError?: (error: unknown) => unknown;
|
|
8
9
|
keepAliveMs?: number;
|
|
9
|
-
formatError?: (error: unknown) => unknown;
|
|
10
10
|
}): ReadableStream<Uint8Array>;
|
package/dist/utils/stream.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { toOpenAIError } from "../errors/openai";
|
|
2
1
|
const TEXT_ENCODER = new TextEncoder();
|
|
3
2
|
const SSE_DONE_CHUNK = TEXT_ENCODER.encode("data: [DONE]\n\n");
|
|
4
3
|
const SSE_KEEP_ALIVE_CHUNK = TEXT_ENCODER.encode(": keep-alive\n\n");
|
|
@@ -59,13 +58,9 @@ export function toSseStream(src, options = {}) {
|
|
|
59
58
|
}
|
|
60
59
|
const value = result.value;
|
|
61
60
|
if (value.event === "error" || value.data instanceof Error) {
|
|
62
|
-
const error = options.
|
|
63
|
-
? options.formatError(value.data)
|
|
64
|
-
: toOpenAIError(value.data);
|
|
61
|
+
const error = options.toError?.(value.data) ?? value.data;
|
|
65
62
|
controller.enqueue(TEXT_ENCODER.encode(serializeSseFrame({ event: value.event, data: error })));
|
|
66
|
-
|
|
67
|
-
const errorStatus = openAiError?.error.type === "invalid_request_error" ? 422 : 502;
|
|
68
|
-
done(controller, errorStatus, value.data);
|
|
63
|
+
done(controller, error["status"] ?? 502, value.data);
|
|
69
64
|
reader.cancel(value.data).catch(() => { });
|
|
70
65
|
return;
|
|
71
66
|
}
|
|
@@ -74,12 +69,9 @@ export function toSseStream(src, options = {}) {
|
|
|
74
69
|
}
|
|
75
70
|
catch (error) {
|
|
76
71
|
try {
|
|
77
|
-
const errorPayload = options.formatError
|
|
78
|
-
? options.formatError(error)
|
|
79
|
-
: toOpenAIError(error);
|
|
80
72
|
controller.enqueue(TEXT_ENCODER.encode(serializeSseFrame({
|
|
81
73
|
event: "error",
|
|
82
|
-
data:
|
|
74
|
+
data: options.toError?.(error) ?? error,
|
|
83
75
|
})));
|
|
84
76
|
}
|
|
85
77
|
catch { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hebo-ai/gateway",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "AI gateway as a framework. For full control over models, routing & lifecycle. OpenAI /chat/completions, OpenResponses /responses & Anthropic /messages.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|