@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.
- package/README.md +3 -3
- package/dist/endpoints/chat-completions/converters.js +26 -21
- package/dist/endpoints/chat-completions/handler.js +2 -0
- package/dist/endpoints/chat-completions/otel.js +1 -1
- package/dist/endpoints/chat-completions/schema.d.ts +4 -18
- package/dist/endpoints/chat-completions/schema.js +14 -17
- package/dist/endpoints/embeddings/handler.js +2 -0
- package/dist/endpoints/embeddings/otel.js +5 -0
- package/dist/endpoints/embeddings/schema.d.ts +6 -0
- package/dist/endpoints/embeddings/schema.js +4 -1
- package/dist/endpoints/models/converters.js +3 -3
- package/dist/lifecycle.js +2 -2
- package/dist/logger/default.js +3 -3
- package/dist/logger/index.d.ts +2 -5
- package/dist/middleware/common.js +1 -0
- package/dist/middleware/utils.js +0 -3
- package/dist/models/amazon/middleware.js +8 -5
- package/dist/models/anthropic/middleware.js +13 -13
- package/dist/models/catalog.js +5 -1
- package/dist/models/cohere/middleware.js +7 -5
- package/dist/models/google/middleware.d.ts +1 -1
- package/dist/models/google/middleware.js +29 -25
- package/dist/models/openai/middleware.js +13 -9
- package/dist/models/voyage/middleware.js +2 -1
- package/dist/providers/bedrock/middleware.js +21 -23
- package/dist/providers/registry.js +3 -0
- package/dist/telemetry/fetch.js +7 -2
- package/dist/telemetry/gen-ai.js +15 -12
- package/dist/telemetry/memory.d.ts +1 -1
- package/dist/telemetry/memory.js +30 -14
- package/dist/telemetry/span.js +1 -1
- package/dist/telemetry/stream.js +30 -23
- package/dist/utils/env.js +4 -2
- package/dist/utils/preset.js +1 -0
- package/dist/utils/response.js +3 -1
- package/package.json +36 -50
- package/src/config.ts +0 -98
- package/src/endpoints/chat-completions/converters.test.ts +0 -631
- package/src/endpoints/chat-completions/converters.ts +0 -899
- package/src/endpoints/chat-completions/handler.test.ts +0 -391
- package/src/endpoints/chat-completions/handler.ts +0 -201
- package/src/endpoints/chat-completions/index.ts +0 -4
- package/src/endpoints/chat-completions/otel.test.ts +0 -315
- package/src/endpoints/chat-completions/otel.ts +0 -214
- package/src/endpoints/chat-completions/schema.ts +0 -364
- package/src/endpoints/embeddings/converters.ts +0 -51
- package/src/endpoints/embeddings/handler.test.ts +0 -133
- package/src/endpoints/embeddings/handler.ts +0 -137
- package/src/endpoints/embeddings/index.ts +0 -4
- package/src/endpoints/embeddings/otel.ts +0 -40
- package/src/endpoints/embeddings/schema.ts +0 -36
- package/src/endpoints/models/converters.ts +0 -56
- package/src/endpoints/models/handler.test.ts +0 -122
- package/src/endpoints/models/handler.ts +0 -37
- package/src/endpoints/models/index.ts +0 -3
- package/src/endpoints/models/schema.ts +0 -37
- package/src/errors/ai-sdk.ts +0 -99
- package/src/errors/gateway.ts +0 -17
- package/src/errors/openai.ts +0 -57
- package/src/errors/utils.ts +0 -47
- package/src/gateway.ts +0 -50
- package/src/index.ts +0 -19
- package/src/lifecycle.ts +0 -135
- package/src/logger/default.ts +0 -105
- package/src/logger/index.ts +0 -42
- package/src/middleware/common.test.ts +0 -215
- package/src/middleware/common.ts +0 -163
- package/src/middleware/debug.ts +0 -37
- package/src/middleware/matcher.ts +0 -161
- package/src/middleware/utils.ts +0 -34
- package/src/models/amazon/index.ts +0 -2
- package/src/models/amazon/middleware.test.ts +0 -133
- package/src/models/amazon/middleware.ts +0 -79
- package/src/models/amazon/presets.ts +0 -104
- package/src/models/anthropic/index.ts +0 -2
- package/src/models/anthropic/middleware.test.ts +0 -643
- package/src/models/anthropic/middleware.ts +0 -148
- package/src/models/anthropic/presets.ts +0 -191
- package/src/models/catalog.ts +0 -13
- package/src/models/cohere/index.ts +0 -2
- package/src/models/cohere/middleware.test.ts +0 -138
- package/src/models/cohere/middleware.ts +0 -76
- package/src/models/cohere/presets.ts +0 -186
- package/src/models/google/index.ts +0 -2
- package/src/models/google/middleware.test.ts +0 -298
- package/src/models/google/middleware.ts +0 -137
- package/src/models/google/presets.ts +0 -118
- package/src/models/meta/index.ts +0 -1
- package/src/models/meta/presets.ts +0 -143
- package/src/models/openai/index.ts +0 -2
- package/src/models/openai/middleware.test.ts +0 -189
- package/src/models/openai/middleware.ts +0 -103
- package/src/models/openai/presets.ts +0 -280
- package/src/models/types.ts +0 -114
- package/src/models/voyage/index.ts +0 -2
- package/src/models/voyage/middleware.test.ts +0 -28
- package/src/models/voyage/middleware.ts +0 -23
- package/src/models/voyage/presets.ts +0 -126
- package/src/providers/anthropic/canonical.ts +0 -17
- package/src/providers/anthropic/index.ts +0 -1
- package/src/providers/bedrock/canonical.ts +0 -87
- package/src/providers/bedrock/index.ts +0 -2
- package/src/providers/bedrock/middleware.test.ts +0 -303
- package/src/providers/bedrock/middleware.ts +0 -128
- package/src/providers/cohere/canonical.ts +0 -26
- package/src/providers/cohere/index.ts +0 -1
- package/src/providers/groq/canonical.ts +0 -21
- package/src/providers/groq/index.ts +0 -1
- package/src/providers/openai/canonical.ts +0 -16
- package/src/providers/openai/index.ts +0 -1
- package/src/providers/registry.test.ts +0 -44
- package/src/providers/registry.ts +0 -165
- package/src/providers/types.ts +0 -20
- package/src/providers/vertex/canonical.ts +0 -17
- package/src/providers/vertex/index.ts +0 -1
- package/src/providers/voyage/canonical.ts +0 -16
- package/src/providers/voyage/index.ts +0 -1
- package/src/telemetry/ai-sdk.ts +0 -46
- package/src/telemetry/baggage.ts +0 -27
- package/src/telemetry/fetch.ts +0 -62
- package/src/telemetry/gen-ai.ts +0 -113
- package/src/telemetry/http.ts +0 -62
- package/src/telemetry/index.ts +0 -1
- package/src/telemetry/memory.ts +0 -36
- package/src/telemetry/span.ts +0 -85
- package/src/telemetry/stream.ts +0 -64
- package/src/types.ts +0 -223
- package/src/utils/env.ts +0 -7
- package/src/utils/headers.ts +0 -27
- package/src/utils/preset.ts +0 -65
- package/src/utils/request.test.ts +0 -75
- package/src/utils/request.ts +0 -52
- package/src/utils/response.ts +0 -84
- 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
|
-
};
|
package/src/providers/types.ts
DELETED
|
@@ -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";
|
package/src/telemetry/ai-sdk.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/baggage.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/fetch.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/gen-ai.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/http.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./fetch";
|
package/src/telemetry/memory.ts
DELETED
|
@@ -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
|
-
};
|
package/src/telemetry/span.ts
DELETED
|
@@ -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
|
-
};
|