@hebo-ai/gateway 0.4.0-beta.0 → 0.4.0-beta.2
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 +4 -4
- package/dist/endpoints/chat-completions/handler.js +2 -2
- package/dist/lifecycle.js +2 -1
- package/dist/middleware/matcher.js +2 -0
- package/dist/telemetry/span.d.ts +1 -0
- package/dist/telemetry/span.js +10 -1
- package/dist/telemetry/stream.d.ts +0 -2
- package/dist/telemetry/stream.js +2 -7
- package/dist/telemetry/utils.js +1 -1
- package/package.json +1 -1
- package/src/endpoints/chat-completions/handler.ts +2 -2
- package/src/lifecycle.ts +2 -1
- package/src/middleware/matcher.ts +2 -0
- package/src/telemetry/span.ts +11 -1
- package/src/telemetry/stream.ts +3 -12
- package/src/telemetry/utils.ts +3 -4
package/README.md
CHANGED
|
@@ -620,7 +620,7 @@ const gw = gateway({
|
|
|
620
620
|
// "required" = minimal baseline attributes
|
|
621
621
|
// "recommended" = practical operational attributes (request/response metadata, genai model/usage fields)
|
|
622
622
|
// "full" = also include body fields (e.g. genai input/output messages)
|
|
623
|
-
attributes: "
|
|
623
|
+
attributes: "full",
|
|
624
624
|
},
|
|
625
625
|
});
|
|
626
626
|
```
|
|
@@ -628,10 +628,10 @@ const gw = gateway({
|
|
|
628
628
|
Attribute names and span semantics follow OpenTelemetry GenAI semantic conventions:
|
|
629
629
|
https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/
|
|
630
630
|
|
|
631
|
-
To populate custom span attributes, the inbound W3C `baggage` header is supported. Keys in the `hebo.` namespace are mapped to span attributes, with the namespace stripped. For example: `baggage: hebo.user_id=u-123` becomes span attribute `user_id=u-123`.
|
|
632
|
-
|
|
633
631
|
> [!TIP]
|
|
634
|
-
>
|
|
632
|
+
> To populate custom span attributes, the inbound W3C `baggage` header is supported. Keys in the `hebo.` namespace are mapped to span attributes, with the namespace stripped. For example: `baggage: hebo.user_id=u-123` becomes span attribute `user_id=u-123`.
|
|
633
|
+
|
|
634
|
+
For observability integration that is not otel compliant, you can disable built-in telemetry and manually instrument requests during `before` / `after` hooks.
|
|
635
635
|
|
|
636
636
|
### Passing Framework State to Hooks
|
|
637
637
|
|
|
@@ -93,8 +93,8 @@ export const chatCompletions = (config) => {
|
|
|
93
93
|
onAbort: () => {
|
|
94
94
|
throw new DOMException("Upstream failed", "AbortError");
|
|
95
95
|
},
|
|
96
|
-
onFinish: (
|
|
97
|
-
ctx.streamResult = toChatCompletions(
|
|
96
|
+
onFinish: (result) => {
|
|
97
|
+
ctx.streamResult = toChatCompletions(result, ctx.resolvedModelId);
|
|
98
98
|
},
|
|
99
99
|
timeout: {
|
|
100
100
|
totalMs: 5 * 60 * 1000,
|
package/dist/lifecycle.js
CHANGED
|
@@ -2,7 +2,7 @@ import { parseConfig } from "./config";
|
|
|
2
2
|
import { toOpenAIErrorResponse } from "./errors/openai";
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
import { withOtel } from "./telemetry/otel";
|
|
5
|
-
import { addSpanEvent } from "./telemetry/span";
|
|
5
|
+
import { addSpanEvent, recordSpanError } from "./telemetry/span";
|
|
6
6
|
import { resolveRequestId } from "./utils/headers";
|
|
7
7
|
import { maybeApplyRequestPatch, prepareRequestHeaders } from "./utils/request";
|
|
8
8
|
import { prepareResponseInit, toResponse } from "./utils/response";
|
|
@@ -32,6 +32,7 @@ export const winterCgHandler = (run, config) => {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
|
+
recordSpanError(error);
|
|
35
36
|
logger.error({
|
|
36
37
|
requestId: resolveRequestId(ctx.request),
|
|
37
38
|
err: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { logger } from "../logger";
|
|
2
|
+
import { addSpanEvent } from "../telemetry/span";
|
|
2
3
|
import { forwardParamsEmbeddingMiddleware, forwardParamsMiddleware } from "./common";
|
|
3
4
|
class SimpleMatcher {
|
|
4
5
|
rules = [];
|
|
@@ -82,6 +83,7 @@ class ModelMiddlewareMatcher {
|
|
|
82
83
|
break;
|
|
83
84
|
}
|
|
84
85
|
logger.warn(`[middleware] cache eviction`);
|
|
86
|
+
addSpanEvent("hebo.middelware.cache.evicted");
|
|
85
87
|
}
|
|
86
88
|
this.cache.set(key, out);
|
|
87
89
|
return out;
|
package/dist/telemetry/span.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export declare const startSpan: (name: string, options?: SpanOptions, customTrac
|
|
|
7
7
|
};
|
|
8
8
|
export declare const withSpan: <T>(name: string, run: () => Promise<T> | T, options?: SpanOptions) => Promise<T>;
|
|
9
9
|
export declare const addSpanEvent: (name: string, attributes?: Attributes) => void;
|
|
10
|
+
export declare const recordSpanError: (error: unknown) => void;
|
package/dist/telemetry/span.js
CHANGED
|
@@ -59,5 +59,14 @@ export const withSpan = async (name, run, options) => {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
export const addSpanEvent = (name, attributes) => {
|
|
62
|
-
|
|
62
|
+
const allAttributes = Object.assign(attributes ?? {}, getMemoryAttributes());
|
|
63
|
+
trace.getActiveSpan()?.addEvent(name, allAttributes);
|
|
64
|
+
};
|
|
65
|
+
export const recordSpanError = (error) => {
|
|
66
|
+
const span = trace.getActiveSpan();
|
|
67
|
+
if (!span)
|
|
68
|
+
return;
|
|
69
|
+
const err = toError(error);
|
|
70
|
+
span.recordException(err);
|
|
71
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
63
72
|
};
|
package/dist/telemetry/stream.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const instrumentStream = (src, hooks, signal) => {
|
|
2
|
-
const stats = { bytes: 0
|
|
2
|
+
const stats = { bytes: 0 };
|
|
3
3
|
let done = false;
|
|
4
4
|
const finish = (status, reason) => {
|
|
5
5
|
if (done)
|
|
@@ -10,12 +10,7 @@ export const instrumentStream = (src, hooks, signal) => {
|
|
|
10
10
|
if (status >= 400) {
|
|
11
11
|
hooks.onError?.(reason, status);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
bytes: stats.bytes,
|
|
15
|
-
streamStart: stats.streamStart,
|
|
16
|
-
streamEnd: performance.now(),
|
|
17
|
-
};
|
|
18
|
-
hooks.onComplete?.(status, timing);
|
|
13
|
+
hooks.onComplete?.(status, stats);
|
|
19
14
|
};
|
|
20
15
|
return new ReadableStream({
|
|
21
16
|
async start(controller) {
|
package/dist/telemetry/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hebo-ai/gateway",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.2",
|
|
4
4
|
"description": "AI gateway as a framework. For full control over models, routing & lifecycle. OpenAI-compatible /chat/completions, /embeddings & /models.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -127,9 +127,9 @@ export const chatCompletions = (config: GatewayConfig): Endpoint => {
|
|
|
127
127
|
onAbort: () => {
|
|
128
128
|
throw new DOMException("Upstream failed", "AbortError");
|
|
129
129
|
},
|
|
130
|
-
onFinish: (
|
|
130
|
+
onFinish: (result) => {
|
|
131
131
|
ctx.streamResult = toChatCompletions(
|
|
132
|
-
|
|
132
|
+
result as unknown as GenerateTextResult<ToolSet, Output.Output>,
|
|
133
133
|
ctx.resolvedModelId!,
|
|
134
134
|
);
|
|
135
135
|
},
|
package/src/lifecycle.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { parseConfig } from "./config";
|
|
|
9
9
|
import { toOpenAIErrorResponse } from "./errors/openai";
|
|
10
10
|
import { logger } from "./logger";
|
|
11
11
|
import { withOtel } from "./telemetry/otel";
|
|
12
|
-
import { addSpanEvent } from "./telemetry/span";
|
|
12
|
+
import { addSpanEvent, recordSpanError } from "./telemetry/span";
|
|
13
13
|
import { resolveRequestId } from "./utils/headers";
|
|
14
14
|
import { maybeApplyRequestPatch, prepareRequestHeaders } from "./utils/request";
|
|
15
15
|
import { prepareResponseInit, toResponse } from "./utils/response";
|
|
@@ -46,6 +46,7 @@ export const winterCgHandler = (
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
} catch (error) {
|
|
49
|
+
recordSpanError(error);
|
|
49
50
|
logger.error({
|
|
50
51
|
requestId: resolveRequestId(ctx.request),
|
|
51
52
|
err: error instanceof Error ? error : new Error(String(error)),
|
|
@@ -4,6 +4,7 @@ import type { ModelId } from "../models/types";
|
|
|
4
4
|
import type { ProviderId } from "../providers/types";
|
|
5
5
|
|
|
6
6
|
import { logger } from "../logger";
|
|
7
|
+
import { addSpanEvent } from "../telemetry/span";
|
|
7
8
|
import { forwardParamsEmbeddingMiddleware, forwardParamsMiddleware } from "./common";
|
|
8
9
|
|
|
9
10
|
type MiddlewareEntries = {
|
|
@@ -117,6 +118,7 @@ class ModelMiddlewareMatcher {
|
|
|
117
118
|
if (--n === 0) break;
|
|
118
119
|
}
|
|
119
120
|
logger.warn(`[middleware] cache eviction`);
|
|
121
|
+
addSpanEvent("hebo.middelware.cache.evicted");
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
this.cache.set(key, out);
|
package/src/telemetry/span.ts
CHANGED
|
@@ -83,5 +83,15 @@ export const withSpan = async <T>(
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export const addSpanEvent = (name: string, attributes?: Attributes) => {
|
|
86
|
-
|
|
86
|
+
const allAttributes = Object.assign(attributes ?? {}, getMemoryAttributes());
|
|
87
|
+
trace.getActiveSpan()?.addEvent(name, allAttributes);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const recordSpanError = (error: unknown) => {
|
|
91
|
+
const span = trace.getActiveSpan();
|
|
92
|
+
if (!span) return;
|
|
93
|
+
|
|
94
|
+
const err = toError(error);
|
|
95
|
+
span.recordException(err);
|
|
96
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
87
97
|
};
|
package/src/telemetry/stream.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export type InstrumentStreamHooks = {
|
|
2
|
-
onComplete?: (
|
|
3
|
-
status: number,
|
|
4
|
-
stats: { bytes: number; streamStart: number; streamEnd: number },
|
|
5
|
-
) => void;
|
|
2
|
+
onComplete?: (status: number, stats: { bytes: number }) => void;
|
|
6
3
|
onError?: (error: unknown, status: number) => void;
|
|
7
4
|
};
|
|
8
5
|
|
|
@@ -11,7 +8,7 @@ export const instrumentStream = (
|
|
|
11
8
|
hooks: InstrumentStreamHooks,
|
|
12
9
|
signal?: AbortSignal,
|
|
13
10
|
): ReadableStream<Uint8Array> => {
|
|
14
|
-
const stats = { bytes: 0
|
|
11
|
+
const stats = { bytes: 0 };
|
|
15
12
|
let done = false;
|
|
16
13
|
|
|
17
14
|
const finish = (status: number, reason?: unknown) => {
|
|
@@ -24,13 +21,7 @@ export const instrumentStream = (
|
|
|
24
21
|
hooks.onError?.(reason, status);
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
bytes: stats.bytes,
|
|
29
|
-
streamStart: stats.streamStart,
|
|
30
|
-
streamEnd: performance.now(),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
hooks.onComplete?.(status, timing);
|
|
24
|
+
hooks.onComplete?.(status, stats);
|
|
34
25
|
};
|
|
35
26
|
|
|
36
27
|
return new ReadableStream<Uint8Array>({
|
package/src/telemetry/utils.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { Embeddings, EmbeddingsBody } from "#/endpoints/embeddings";
|
|
2
|
-
|
|
3
|
-
import { resolveRequestId } from "#/utils/headers";
|
|
4
|
-
|
|
5
1
|
import type {
|
|
6
2
|
ChatCompletions,
|
|
7
3
|
ChatCompletionsBody,
|
|
8
4
|
ChatCompletionsContentPart,
|
|
9
5
|
ChatCompletionsMessage,
|
|
10
6
|
} from "../endpoints/chat-completions/schema";
|
|
7
|
+
import type { Embeddings, EmbeddingsBody } from "../endpoints/embeddings";
|
|
8
|
+
|
|
9
|
+
import { resolveRequestId } from "../utils/headers";
|
|
11
10
|
|
|
12
11
|
type GenAIPart = Record<string, unknown>;
|
|
13
12
|
const DEFAULT_ATTRIBUTES_LEVEL = "recommended";
|