@hebo-ai/gateway 0.6.2-rc0 → 0.6.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.
Files changed (134) hide show
  1. package/README.md +3 -3
  2. package/dist/endpoints/chat-completions/converters.js +26 -21
  3. package/dist/endpoints/chat-completions/handler.js +2 -0
  4. package/dist/endpoints/chat-completions/otel.js +1 -1
  5. package/dist/endpoints/chat-completions/schema.d.ts +4 -18
  6. package/dist/endpoints/chat-completions/schema.js +14 -17
  7. package/dist/endpoints/embeddings/handler.js +2 -0
  8. package/dist/endpoints/embeddings/otel.js +5 -0
  9. package/dist/endpoints/embeddings/schema.d.ts +6 -0
  10. package/dist/endpoints/embeddings/schema.js +4 -1
  11. package/dist/endpoints/models/converters.js +3 -3
  12. package/dist/lifecycle.js +2 -2
  13. package/dist/logger/default.js +3 -3
  14. package/dist/logger/index.d.ts +2 -5
  15. package/dist/middleware/common.js +1 -0
  16. package/dist/middleware/utils.js +0 -3
  17. package/dist/models/amazon/middleware.js +8 -5
  18. package/dist/models/anthropic/middleware.js +13 -13
  19. package/dist/models/catalog.js +5 -1
  20. package/dist/models/cohere/middleware.js +7 -5
  21. package/dist/models/google/middleware.d.ts +1 -1
  22. package/dist/models/google/middleware.js +29 -25
  23. package/dist/models/openai/middleware.js +13 -9
  24. package/dist/models/voyage/middleware.js +2 -1
  25. package/dist/providers/bedrock/middleware.js +21 -23
  26. package/dist/providers/registry.js +3 -0
  27. package/dist/telemetry/fetch.js +7 -2
  28. package/dist/telemetry/gen-ai.js +15 -12
  29. package/dist/telemetry/memory.d.ts +1 -1
  30. package/dist/telemetry/memory.js +30 -14
  31. package/dist/telemetry/span.js +1 -1
  32. package/dist/telemetry/stream.js +30 -23
  33. package/dist/utils/env.js +4 -2
  34. package/dist/utils/preset.js +1 -0
  35. package/dist/utils/response.js +3 -1
  36. package/package.json +36 -50
  37. package/src/config.ts +0 -98
  38. package/src/endpoints/chat-completions/converters.test.ts +0 -631
  39. package/src/endpoints/chat-completions/converters.ts +0 -899
  40. package/src/endpoints/chat-completions/handler.test.ts +0 -391
  41. package/src/endpoints/chat-completions/handler.ts +0 -201
  42. package/src/endpoints/chat-completions/index.ts +0 -4
  43. package/src/endpoints/chat-completions/otel.test.ts +0 -315
  44. package/src/endpoints/chat-completions/otel.ts +0 -214
  45. package/src/endpoints/chat-completions/schema.ts +0 -364
  46. package/src/endpoints/embeddings/converters.ts +0 -51
  47. package/src/endpoints/embeddings/handler.test.ts +0 -133
  48. package/src/endpoints/embeddings/handler.ts +0 -137
  49. package/src/endpoints/embeddings/index.ts +0 -4
  50. package/src/endpoints/embeddings/otel.ts +0 -40
  51. package/src/endpoints/embeddings/schema.ts +0 -36
  52. package/src/endpoints/models/converters.ts +0 -56
  53. package/src/endpoints/models/handler.test.ts +0 -122
  54. package/src/endpoints/models/handler.ts +0 -37
  55. package/src/endpoints/models/index.ts +0 -3
  56. package/src/endpoints/models/schema.ts +0 -37
  57. package/src/errors/ai-sdk.ts +0 -99
  58. package/src/errors/gateway.ts +0 -17
  59. package/src/errors/openai.ts +0 -57
  60. package/src/errors/utils.ts +0 -47
  61. package/src/gateway.ts +0 -50
  62. package/src/index.ts +0 -19
  63. package/src/lifecycle.ts +0 -135
  64. package/src/logger/default.ts +0 -105
  65. package/src/logger/index.ts +0 -42
  66. package/src/middleware/common.test.ts +0 -215
  67. package/src/middleware/common.ts +0 -163
  68. package/src/middleware/debug.ts +0 -37
  69. package/src/middleware/matcher.ts +0 -161
  70. package/src/middleware/utils.ts +0 -34
  71. package/src/models/amazon/index.ts +0 -2
  72. package/src/models/amazon/middleware.test.ts +0 -133
  73. package/src/models/amazon/middleware.ts +0 -79
  74. package/src/models/amazon/presets.ts +0 -104
  75. package/src/models/anthropic/index.ts +0 -2
  76. package/src/models/anthropic/middleware.test.ts +0 -643
  77. package/src/models/anthropic/middleware.ts +0 -148
  78. package/src/models/anthropic/presets.ts +0 -191
  79. package/src/models/catalog.ts +0 -13
  80. package/src/models/cohere/index.ts +0 -2
  81. package/src/models/cohere/middleware.test.ts +0 -138
  82. package/src/models/cohere/middleware.ts +0 -76
  83. package/src/models/cohere/presets.ts +0 -186
  84. package/src/models/google/index.ts +0 -2
  85. package/src/models/google/middleware.test.ts +0 -298
  86. package/src/models/google/middleware.ts +0 -137
  87. package/src/models/google/presets.ts +0 -118
  88. package/src/models/meta/index.ts +0 -1
  89. package/src/models/meta/presets.ts +0 -143
  90. package/src/models/openai/index.ts +0 -2
  91. package/src/models/openai/middleware.test.ts +0 -189
  92. package/src/models/openai/middleware.ts +0 -103
  93. package/src/models/openai/presets.ts +0 -280
  94. package/src/models/types.ts +0 -114
  95. package/src/models/voyage/index.ts +0 -2
  96. package/src/models/voyage/middleware.test.ts +0 -28
  97. package/src/models/voyage/middleware.ts +0 -23
  98. package/src/models/voyage/presets.ts +0 -126
  99. package/src/providers/anthropic/canonical.ts +0 -17
  100. package/src/providers/anthropic/index.ts +0 -1
  101. package/src/providers/bedrock/canonical.ts +0 -87
  102. package/src/providers/bedrock/index.ts +0 -2
  103. package/src/providers/bedrock/middleware.test.ts +0 -303
  104. package/src/providers/bedrock/middleware.ts +0 -128
  105. package/src/providers/cohere/canonical.ts +0 -26
  106. package/src/providers/cohere/index.ts +0 -1
  107. package/src/providers/groq/canonical.ts +0 -21
  108. package/src/providers/groq/index.ts +0 -1
  109. package/src/providers/openai/canonical.ts +0 -16
  110. package/src/providers/openai/index.ts +0 -1
  111. package/src/providers/registry.test.ts +0 -44
  112. package/src/providers/registry.ts +0 -165
  113. package/src/providers/types.ts +0 -20
  114. package/src/providers/vertex/canonical.ts +0 -17
  115. package/src/providers/vertex/index.ts +0 -1
  116. package/src/providers/voyage/canonical.ts +0 -16
  117. package/src/providers/voyage/index.ts +0 -1
  118. package/src/telemetry/ai-sdk.ts +0 -46
  119. package/src/telemetry/baggage.ts +0 -27
  120. package/src/telemetry/fetch.ts +0 -62
  121. package/src/telemetry/gen-ai.ts +0 -113
  122. package/src/telemetry/http.ts +0 -62
  123. package/src/telemetry/index.ts +0 -1
  124. package/src/telemetry/memory.ts +0 -36
  125. package/src/telemetry/span.ts +0 -85
  126. package/src/telemetry/stream.ts +0 -64
  127. package/src/types.ts +0 -223
  128. package/src/utils/env.ts +0 -7
  129. package/src/utils/headers.ts +0 -27
  130. package/src/utils/preset.ts +0 -65
  131. package/src/utils/request.test.ts +0 -75
  132. package/src/utils/request.ts +0 -52
  133. package/src/utils/response.ts +0 -84
  134. package/src/utils/url.ts +0 -26
@@ -1,165 +0,0 @@
1
- import type { ProviderV3 } from "@ai-sdk/provider";
2
-
3
- import { customProvider } from "ai";
4
-
5
- import type { ModelCatalog, ModelId } from "../models/types";
6
- import type { ProviderRegistry } from "./types";
7
-
8
- import { GatewayError } from "../errors/gateway";
9
- import { logger } from "../logger";
10
-
11
- export const resolveProvider = (args: {
12
- providers: ProviderRegistry;
13
- models: ModelCatalog;
14
- modelId: ModelId;
15
- operation: "chat" | "embeddings";
16
- }): ProviderV3 => {
17
- const { providers, models, modelId, operation } = args;
18
-
19
- const catalogModel = models[modelId];
20
-
21
- if (!catalogModel) {
22
- throw new GatewayError(`Model '${modelId}' not found in catalog`, 422, "MODEL_NOT_FOUND");
23
- }
24
-
25
- const modality = operation === "embeddings" ? "embedding" : "text";
26
- if (catalogModel.modalities && !catalogModel.modalities.output.includes(modality)) {
27
- throw new GatewayError(
28
- `Model '${modelId}' does not support '${modality}' output`,
29
- 422,
30
- "MODEL_UNSUPPORTED_OPERATION",
31
- );
32
- }
33
-
34
- // FUTURE: implement fallback logic [e.g. runtime config invalid]
35
- const resolvedProviderId = catalogModel.providers[0];
36
-
37
- if (!resolvedProviderId) {
38
- throw new GatewayError(`No providers configured for model '${modelId}'`, 422, "NO_PROVIDERS");
39
- }
40
-
41
- const provider = providers[resolvedProviderId];
42
- if (!provider) {
43
- throw new GatewayError(
44
- `Provider '${resolvedProviderId}' not configured`,
45
- 422,
46
- "PROVIDER_NOT_CONFIGURED",
47
- );
48
- }
49
-
50
- return provider;
51
- };
52
-
53
- export type CanonicalIdsOptions = {
54
- mapping?: Partial<Record<ModelId, string>>;
55
- options?: {
56
- /** @default true */
57
- stripNamespace?: boolean;
58
- /** @default false */
59
- normalizeDelimiters?: boolean | readonly string[];
60
- prefix?: string;
61
- template?: Record<string, string | undefined>;
62
- postfix?: string;
63
- /** @default "/" */
64
- namespaceSeparator?: "/" | "." | ":";
65
- };
66
- };
67
-
68
- export const withCanonicalIds = (
69
- provider: ProviderV3,
70
- config: CanonicalIdsOptions = {},
71
- ): ProviderV3 => {
72
- const {
73
- mapping,
74
- options: {
75
- stripNamespace = true,
76
- normalizeDelimiters = false,
77
- template,
78
- prefix,
79
- postfix,
80
- namespaceSeparator = "/",
81
- } = {},
82
- } = config;
83
-
84
- const shouldNormalizeDelimiters = (canonicalId: string) => {
85
- if (typeof normalizeDelimiters === "boolean") return normalizeDelimiters;
86
- return normalizeDelimiters.some((x) => canonicalId.startsWith(`${x}/`));
87
- };
88
-
89
- const normalizeId = (canonicalId: string) => {
90
- let out = canonicalId;
91
-
92
- if (shouldNormalizeDelimiters(canonicalId)) {
93
- out = out.replaceAll(".", "-");
94
- }
95
- if (stripNamespace) {
96
- out = out.replace(/^[^/]+\//, "");
97
- } else if (namespaceSeparator !== "/") {
98
- out = out.replace("/", namespaceSeparator);
99
- }
100
-
101
- return out;
102
- };
103
-
104
- const applyTemplate = (input: string) => {
105
- if (!template) return input;
106
- return Object.entries(template).reduce((out, [k, v]) => out.replace(`{${k}}`, v ?? ""), input);
107
- };
108
-
109
- const applyFallbackAffixes = (v: string) => {
110
- let out = prefix && !v.startsWith(prefix) ? `${prefix}${v}` : v;
111
- if (postfix && !out.endsWith(postfix)) out = `${out}${postfix}`;
112
- return out;
113
- };
114
-
115
- const needsFallbackWrap =
116
- stripNamespace || normalizeDelimiters || namespaceSeparator !== "/" || !!prefix || !!postfix;
117
-
118
- // FUTURE: use embeddingModel instead of textEmbeddingModel once voyage supports it
119
- const languageModel = provider.languageModel;
120
- const embeddingModel = provider.textEmbeddingModel!;
121
-
122
- const fallbackProvider = needsFallbackWrap
123
- ? ({
124
- ...provider,
125
- specificationVersion: "v3",
126
- languageModel: (id: string) => {
127
- const mapped = applyFallbackAffixes(normalizeId(id));
128
- logger.debug(`[canonical] mapped ${id} to ${mapped}`);
129
- return languageModel(mapped);
130
- },
131
- embeddingModel: (id: string) => {
132
- const mapped = applyFallbackAffixes(normalizeId(id));
133
- logger.debug(`[canonical] mapped ${id} to ${mapped}`);
134
- return embeddingModel(mapped);
135
- },
136
- } satisfies ProviderV3)
137
- : provider;
138
-
139
- const mapModels = <T>(fn?: (id: string) => T) => {
140
- const out = {} as Record<string, T>;
141
-
142
- // Some providers don't have languageModel / embeddingModel
143
- if (fn === undefined) return out;
144
-
145
- for (const [k, v] of Object.entries(mapping ?? {})) {
146
- if (v === undefined) continue;
147
- // This is lazy so that provider is only create once called
148
- Object.defineProperty(out, k, {
149
- get: () => {
150
- const mapped = applyTemplate(v);
151
- logger.debug(`[canonical] mapped ${k} to ${mapped}`);
152
- return fn(mapped);
153
- },
154
- });
155
- }
156
-
157
- return out;
158
- };
159
-
160
- return customProvider({
161
- languageModels: mapModels(languageModel),
162
- embeddingModels: mapModels(embeddingModel),
163
- fallbackProvider,
164
- });
165
- };
@@ -1,20 +0,0 @@
1
- import { type ProviderV3 } from "@ai-sdk/provider";
2
-
3
- export const CANONICAL_PROVIDER_IDS = [
4
- "anthropic",
5
- "azure",
6
- "bedrock",
7
- "cohere",
8
- "groq",
9
- "openai",
10
- "vertex",
11
- "voyage",
12
- ] as const;
13
-
14
- export type CanonicalProviderId = (typeof CANONICAL_PROVIDER_IDS)[number];
15
- // oxlint-disable-next-line ban-types
16
- export type ProviderId = CanonicalProviderId | (string & {});
17
-
18
- export type ProviderRegistry = {
19
- [K in ProviderId]?: ProviderV3;
20
- };
@@ -1,17 +0,0 @@
1
- import type { GoogleVertexProvider } from "@ai-sdk/google-vertex";
2
-
3
- import type { ModelId } from "../../models/types";
4
-
5
- import { withCanonicalIds } from "../registry";
6
-
7
- export const withCanonicalIdsForVertex = (
8
- provider: GoogleVertexProvider,
9
- extraMapping?: Record<ModelId, string>,
10
- ) =>
11
- withCanonicalIds(provider, {
12
- mapping: extraMapping,
13
- options: {
14
- stripNamespace: true,
15
- normalizeDelimiters: ["anthropic"],
16
- },
17
- });
@@ -1 +0,0 @@
1
- export * from "./canonical";
@@ -1,16 +0,0 @@
1
- import type { VoyageProvider } from "voyage-ai-provider";
2
-
3
- import type { ModelId } from "../../models/types";
4
-
5
- import { withCanonicalIds } from "../registry";
6
-
7
- export const withCanonicalIdsForVoyage = (
8
- provider: VoyageProvider,
9
- extraMapping?: Record<ModelId, string>,
10
- ) =>
11
- withCanonicalIds(provider, {
12
- mapping: extraMapping,
13
- options: {
14
- stripNamespace: true,
15
- },
16
- });
@@ -1 +0,0 @@
1
- export * from "./canonical";
@@ -1,46 +0,0 @@
1
- import type { LogWarningsFunction } from "ai";
2
-
3
- import type { TelemetrySignalLevel } from "../types";
4
-
5
- import { logger } from "../logger";
6
- import { addSpanEvent, setSpanAttributes } from "./span";
7
-
8
- type GlobalWithAiSdkWarningLogger = typeof globalThis & {
9
- AI_SDK_LOG_WARNINGS?: LogWarningsFunction | false;
10
- };
11
-
12
- export const installAiSdkWarningLogger = (genAiSignalLevel?: TelemetrySignalLevel) => {
13
- const logWarnings: LogWarningsFunction = ({ warnings, provider, model }) => {
14
- if (warnings.length === 0) return;
15
-
16
- for (const warning of warnings) {
17
- logger.warn(
18
- {
19
- provider,
20
- model,
21
- warning,
22
- },
23
- `[ai-sdk] ${warning.type}`,
24
- );
25
- }
26
-
27
- if (!(genAiSignalLevel === "recommended" || genAiSignalLevel === "full")) return;
28
-
29
- setSpanAttributes({
30
- "gen_ai.response.warning_count": warnings.length,
31
- });
32
-
33
- for (const warning of warnings) {
34
- addSpanEvent("gen_ai.warning", {
35
- "gen_ai.provider.name": provider,
36
- "gen_ai.response.model": model,
37
- "gen_ai.warning.type": warning.type,
38
- "gen_ai.warning.feature": "feature" in warning ? warning.feature : undefined,
39
- "gen_ai.warning.details": "details" in warning ? warning.details : undefined,
40
- "gen_ai.warning.message": "message" in warning ? warning.message : undefined,
41
- });
42
- }
43
- };
44
-
45
- (globalThis as GlobalWithAiSdkWarningLogger).AI_SDK_LOG_WARNINGS = logWarnings;
46
- };
@@ -1,27 +0,0 @@
1
- const HEBO_BAGGAGE_PREFIX = "hebo.";
2
-
3
- export const getBaggageAttributes = (request?: Request) => {
4
- const h = request?.headers.get("baggage");
5
- if (!h) return {};
6
-
7
- const attrs: Record<string, string> = {};
8
-
9
- for (const part of h.split(",")) {
10
- const [k, v] = part.trim().split("=", 2);
11
- if (!k || !v) continue;
12
-
13
- const [rawValue] = v.split(";", 1);
14
- if (!rawValue) continue;
15
-
16
- let value = rawValue;
17
- try {
18
- value = decodeURIComponent(rawValue);
19
- } catch {}
20
-
21
- if (k.startsWith(HEBO_BAGGAGE_PREFIX)) {
22
- attrs[k.slice(HEBO_BAGGAGE_PREFIX.length)] = value;
23
- }
24
- }
25
-
26
- return attrs;
27
- };
@@ -1,62 +0,0 @@
1
- import { SpanKind, type Attributes } from "@opentelemetry/api";
2
-
3
- import type { TelemetrySignalLevel } from "../types";
4
-
5
- import { setSpanAttributes, withSpan } from "./span";
6
-
7
- const ORIGINAL_FETCH_KEY = Symbol.for("@hebo/fetch/original-fetch");
8
-
9
- type GlobalFetchState = typeof globalThis & {
10
- [ORIGINAL_FETCH_KEY]?: typeof fetch;
11
- };
12
-
13
- const g = globalThis as GlobalFetchState;
14
- let fetchTracingEnabled = false;
15
-
16
- const isRequest = (value: unknown): value is Request =>
17
- typeof Request !== "undefined" && value instanceof Request;
18
-
19
- const getRequestAttributes = (input: RequestInfo | URL, init?: RequestInit): Attributes => {
20
- const attrs: Attributes = {
21
- "http.request.method": init?.method ?? (isRequest(input) ? input.method : "GET"),
22
- };
23
-
24
- if (input instanceof URL) attrs["url.full"] = input.href;
25
- else if (typeof input === "string") attrs["url.full"] = input;
26
- else if (isRequest(input)) attrs["url.full"] = input.url;
27
-
28
- return attrs;
29
- };
30
-
31
- const shouldTraceFetch = (init?: RequestInit): boolean =>
32
- typeof (init?.headers as any)?.["user-agent"] === "string" &&
33
- (init!.headers as any)["user-agent"].indexOf("ai-sdk/provider-utils") !== -1;
34
-
35
- const otelFetch = (input: RequestInfo | URL, init?: RequestInit) => {
36
- const original = g[ORIGINAL_FETCH_KEY]!;
37
-
38
- if (!fetchTracingEnabled) return original(input, init);
39
- if (!shouldTraceFetch(init)) return original(input, init);
40
-
41
- return withSpan(
42
- "fetch",
43
- async () => {
44
- const response = await original(input, init);
45
- setSpanAttributes({ "http.response.status_code": response.status });
46
- return response;
47
- },
48
- {
49
- kind: SpanKind.CLIENT,
50
- attributes: getRequestAttributes(input, init),
51
- },
52
- );
53
- };
54
-
55
- export const instrumentFetch = (level?: TelemetrySignalLevel) => {
56
- fetchTracingEnabled = level === "full";
57
- if (!fetchTracingEnabled) return;
58
- if (g[ORIGINAL_FETCH_KEY]) return;
59
-
60
- g[ORIGINAL_FETCH_KEY] = globalThis.fetch.bind(globalThis);
61
- globalThis.fetch = otelFetch as typeof fetch;
62
- };
@@ -1,113 +0,0 @@
1
- import { metrics, type Attributes } from "@opentelemetry/api";
2
-
3
- import type { GatewayContext, TelemetrySignalLevel } from "../types";
4
-
5
- import { STATUS_CODE } from "../errors/utils";
6
-
7
- const meter = metrics.getMeter("@hebo/gateway");
8
-
9
- const requestDurationHistogram = meter.createHistogram("gen_ai.server.request.duration", {
10
- description: "End-to-end gateway request duration",
11
- unit: "s",
12
- advice: {
13
- explicitBucketBoundaries: [
14
- 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 240,
15
- ],
16
- },
17
- });
18
-
19
- const timePerOutputTokenHistogram = meter.createHistogram("gen_ai.server.time_per_output_token", {
20
- description: "End-to-end gateway request duration per output token",
21
- unit: "s",
22
- advice: {
23
- explicitBucketBoundaries: [
24
- 0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1.0, 2.5,
25
- ],
26
- },
27
- });
28
-
29
- const tokenUsageHistogram = meter.createHistogram("gen_ai.client.token.usage", {
30
- description: "Token usage reported by upstream model responses",
31
- unit: "{token}",
32
- advice: {
33
- explicitBucketBoundaries: [
34
- 1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144,
35
- 524288, 1048576,
36
- ],
37
- },
38
- });
39
-
40
- export const getGenAiGeneralAttributes = (
41
- ctx: GatewayContext,
42
- signalLevel?: TelemetrySignalLevel,
43
- ): Attributes => {
44
- if (!signalLevel || signalLevel === "off") return {};
45
-
46
- const requestModel = typeof ctx.body?.model === "string" ? ctx.body.model : ctx.modelId;
47
-
48
- return {
49
- "gen_ai.operation.name": ctx.operation,
50
- "gen_ai.request.model": requestModel,
51
- "gen_ai.response.model": ctx.resolvedModelId,
52
- "gen_ai.provider.name": ctx.resolvedProviderId,
53
- };
54
- };
55
-
56
- export const recordRequestDuration = (
57
- duration: number,
58
- status: number,
59
- ctx: GatewayContext,
60
- signalLevel?: TelemetrySignalLevel,
61
- ) => {
62
- if (!signalLevel || signalLevel === "off") return;
63
-
64
- const attrs = getGenAiGeneralAttributes(ctx, signalLevel);
65
-
66
- if (status !== 200) {
67
- attrs["error.type"] = `${status} ${STATUS_CODE(status).toLowerCase()}`;
68
- }
69
-
70
- requestDurationHistogram.record(duration / 1000, attrs);
71
- };
72
-
73
- // FUTURE: record unsuccessful calls
74
- export const recordTimePerOutputToken = (
75
- start: number,
76
- tokenAttrs: Attributes,
77
- metricAttrs: Attributes,
78
- signalLevel?: TelemetrySignalLevel,
79
- ) => {
80
- if (!signalLevel || (signalLevel !== "recommended" && signalLevel !== "full")) return;
81
-
82
- const outputTokens = tokenAttrs["gen_ai.usage.output_tokens"];
83
- if (typeof outputTokens !== "number" || outputTokens <= 0) return;
84
-
85
- timePerOutputTokenHistogram.record(
86
- (performance.now() - start) / 1000 / outputTokens,
87
- metricAttrs,
88
- );
89
- };
90
-
91
- // FUTURE: record unsuccessful calls
92
- export const recordTokenUsage = (
93
- tokenAttrs: Attributes,
94
- metricAttrs: Attributes,
95
- signalLevel?: TelemetrySignalLevel,
96
- ) => {
97
- if (!signalLevel || (signalLevel !== "recommended" && signalLevel !== "full")) return;
98
-
99
- const record = (value: unknown, tokenType: string) => {
100
- if (typeof value !== "number") return;
101
- tokenUsageHistogram.record(
102
- value,
103
- Object.assign({}, metricAttrs, { "gen_ai.token.type": tokenType }),
104
- );
105
- };
106
-
107
- record(tokenAttrs["gen_ai.usage.input_tokens"], "input");
108
- record(tokenAttrs["gen_ai.usage.output_tokens"], "output");
109
-
110
- // FUTURE: Monitor otel for emerging cached / reasoning tokens standard:
111
- // https://github.com/open-telemetry/semantic-conventions/issues/1959
112
- // https://github.com/open-telemetry/semantic-conventions/issues/3341
113
- };
@@ -1,62 +0,0 @@
1
- import { type TelemetrySignalLevel } from "../types";
2
-
3
- const headerArr = (h: Headers, k: string) => (h.has(k) ? [h.get(k)!] : undefined);
4
-
5
- export const getRequestAttributes = (request: Request, signalLevel?: TelemetrySignalLevel) => {
6
- if (!signalLevel || signalLevel === "off") return {};
7
-
8
- let url;
9
- try {
10
- // FUTURE: reuse URL from lifecycle
11
- url = new URL(request.url);
12
- } catch {}
13
-
14
- const attrs = {
15
- "http.request.method": request.method,
16
- "url.full": request.url,
17
- "url.path": url?.pathname,
18
- "url.scheme": url?.protocol.replace(":", ""),
19
- "server.address": url?.hostname,
20
- "server.port": url
21
- ? url.port
22
- ? Number(url.port)
23
- : url.protocol === "https:"
24
- ? 443
25
- : 80
26
- : undefined,
27
- };
28
-
29
- if (signalLevel !== "required") {
30
- Object.assign(attrs, {
31
- "user_agent.original": request.headers.get("user-agent") ?? undefined,
32
- });
33
- }
34
-
35
- if (signalLevel === "full") {
36
- Object.assign(attrs, {
37
- // FUTURE: "url.query"
38
- "http.request.header.content-type": headerArr(request.headers, "content-type"),
39
- "http.request.header.content-length": headerArr(request.headers, "content-length"),
40
- // FUTURE: "client.address"
41
- });
42
- }
43
-
44
- return attrs;
45
- };
46
-
47
- export const getResponseAttributes = (response: Response, signalLevel?: TelemetrySignalLevel) => {
48
- if (!signalLevel || signalLevel === "off") return {};
49
-
50
- const attrs = {
51
- "http.response.status_code": response.status,
52
- };
53
-
54
- if (signalLevel === "full") {
55
- Object.assign(attrs, {
56
- "http.response.header.content-type": [headerArr(response.headers, "content-type")],
57
- "http.response.header.content-length": [headerArr(response.headers, "content-length")],
58
- });
59
- }
60
-
61
- return attrs;
62
- };
@@ -1 +0,0 @@
1
- export * from "./fetch";
@@ -1,36 +0,0 @@
1
- import { metrics } from "@opentelemetry/api";
2
-
3
- import type { TelemetrySignalLevel } from "../types";
4
-
5
- const meter = metrics.getMeter("@hebo/gateway");
6
- const defaultHeapSpaceAttrs = { "v8js.heap.space.name": "total" } as const;
7
-
8
- const heapUsedCounter = meter.createUpDownCounter("v8js.memory.heap.used", {
9
- description: "Used bytes in the V8 heap",
10
- unit: "By",
11
- });
12
-
13
- const heapSpacePhysicalSizeCounter = meter.createUpDownCounter(
14
- "v8js.memory.heap.space.physical_size",
15
- {
16
- description: "Physical bytes allocated for the V8 heap space",
17
- unit: "By",
18
- },
19
- );
20
-
21
- const isEnabled = (level?: TelemetrySignalLevel) => level === "recommended" || level === "full";
22
-
23
- export const recordV8jsMemory = (level?: TelemetrySignalLevel) => {
24
- if (!isEnabled(level)) return;
25
-
26
- let usage;
27
- try {
28
- usage = globalThis.process?.memoryUsage?.();
29
- } catch {
30
- return;
31
- }
32
- if (!usage) return;
33
-
34
- heapUsedCounter.add(usage.heapUsed, defaultHeapSpaceAttrs);
35
- heapSpacePhysicalSizeCounter.add(usage.rss, defaultHeapSpaceAttrs);
36
- };
@@ -1,85 +0,0 @@
1
- import type { Attributes, SpanOptions, Tracer } from "@opentelemetry/api";
2
-
3
- import { INVALID_SPAN_CONTEXT, SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
4
-
5
- import type { TelemetrySignalLevel } from "../types";
6
-
7
- const DEFAULT_TRACER_NAME = "@hebo/gateway";
8
-
9
- let spanTracer: Tracer | undefined;
10
- let spanEventsEnabled = false;
11
-
12
- const NOOP_SPAN = {
13
- runWithContext: <T>(fn: () => Promise<T> | T) => fn(),
14
- recordError: (_error: unknown) => {},
15
- finish: () => {},
16
- isExisting: true,
17
- };
18
-
19
- export const setSpanTracer = (tracer?: Tracer) => {
20
- spanTracer = tracer ?? trace.getTracer(DEFAULT_TRACER_NAME);
21
- };
22
-
23
- export const setSpanEventsEnabled = (level?: TelemetrySignalLevel) => {
24
- spanEventsEnabled = level === "recommended" || level === "full";
25
- };
26
-
27
- export const startSpan = (name: string, options?: SpanOptions) => {
28
- if (!spanTracer) {
29
- return Object.assign(trace.wrapSpanContext(INVALID_SPAN_CONTEXT), NOOP_SPAN);
30
- }
31
-
32
- const parentContext = context.active();
33
- const activeSpan = trace.getActiveSpan();
34
-
35
- const span = spanTracer.startSpan(
36
- name,
37
- { kind: activeSpan ? SpanKind.INTERNAL : SpanKind.SERVER, ...options },
38
- parentContext,
39
- );
40
-
41
- const runWithContext = <T>(fn: () => Promise<T> | T) =>
42
- context.with(trace.setSpan(parentContext, span), fn);
43
-
44
- const recordError = (error: unknown) => {
45
- const err = error instanceof Error ? error : new Error(String(error));
46
- span.recordException(err);
47
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
48
- };
49
-
50
- const finish = () => {
51
- span.end();
52
- };
53
-
54
- return Object.assign(span, { runWithContext, recordError, finish, isExisting: !!activeSpan });
55
- };
56
-
57
- export const withSpan = async <T>(
58
- name: string,
59
- run: () => Promise<T> | T,
60
- options?: SpanOptions,
61
- ): Promise<T> => {
62
- if (!spanTracer) {
63
- return await run();
64
- }
65
-
66
- const started = startSpan(name, options);
67
- try {
68
- return await started.runWithContext(run);
69
- } catch (error) {
70
- started.recordError(error);
71
- throw error;
72
- } finally {
73
- started.finish();
74
- }
75
- };
76
-
77
- export const addSpanEvent = (name: string, attributes?: Attributes) => {
78
- if (!spanEventsEnabled) return;
79
- trace.getActiveSpan()?.addEvent(name, attributes);
80
- };
81
-
82
- export const setSpanAttributes = (attributes?: Attributes) => {
83
- if (!attributes) return;
84
- trace.getActiveSpan()?.setAttributes(attributes);
85
- };