toad-eye 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +132 -87
  2. package/dist/alerts/channels.d.ts +2 -0
  3. package/dist/alerts/channels.js +81 -0
  4. package/dist/alerts/conditions.d.ts +29 -0
  5. package/dist/alerts/conditions.js +147 -0
  6. package/dist/alerts/grafana.d.ts +5 -0
  7. package/dist/alerts/grafana.js +19 -0
  8. package/dist/alerts/index.d.ts +5 -0
  9. package/dist/alerts/index.js +12 -0
  10. package/dist/alerts/manager.d.ts +13 -0
  11. package/dist/alerts/manager.js +87 -0
  12. package/dist/alerts/types.d.ts +46 -0
  13. package/dist/alerts/types.js +1 -0
  14. package/dist/cli.js +72 -1
  15. package/dist/index.d.ts +6 -2
  16. package/dist/index.js +3 -1
  17. package/dist/instrumentations/anthropic.d.ts +1 -0
  18. package/dist/instrumentations/anthropic.js +54 -0
  19. package/dist/instrumentations/create.d.ts +11 -0
  20. package/dist/instrumentations/create.js +67 -0
  21. package/dist/instrumentations/gemini.d.ts +1 -0
  22. package/dist/instrumentations/gemini.js +48 -0
  23. package/dist/instrumentations/openai.d.ts +1 -0
  24. package/dist/instrumentations/openai.js +58 -0
  25. package/dist/instrumentations/registry.d.ts +5 -0
  26. package/dist/instrumentations/registry.js +31 -0
  27. package/dist/instrumentations/types.d.ts +29 -0
  28. package/dist/instrumentations/types.js +1 -0
  29. package/dist/metrics.js +31 -16
  30. package/dist/pricing.d.ts +24 -0
  31. package/dist/pricing.js +52 -0
  32. package/dist/spans.d.ts +1 -1
  33. package/dist/spans.js +46 -20
  34. package/dist/tracer.d.ts +4 -1
  35. package/dist/tracer.js +9 -0
  36. package/dist/types/attributes.d.ts +36 -0
  37. package/dist/types/attributes.js +38 -0
  38. package/dist/types/config.d.ts +20 -0
  39. package/dist/types/config.js +1 -0
  40. package/dist/types/index.d.ts +17 -0
  41. package/dist/types/index.js +13 -0
  42. package/dist/types/metrics.d.ts +25 -0
  43. package/dist/types/metrics.js +23 -0
  44. package/dist/types/providers.d.ts +5 -0
  45. package/dist/types/providers.js +1 -0
  46. package/dist/types/spans.d.ts +19 -0
  47. package/dist/types/spans.js +1 -0
  48. package/dist/types.d.ts +60 -25
  49. package/dist/types.js +47 -21
  50. package/package.json +17 -6
  51. package/templates/alerts.example.yml +54 -0
  52. package/templates/docker-compose.yml +1 -2
  53. package/templates/grafana/dashboards/cost-breakdown.json +208 -0
  54. package/templates/grafana/dashboards/error-drilldown.json +188 -0
  55. package/templates/grafana/dashboards/latency-analysis.json +134 -0
  56. package/templates/grafana/dashboards/llm-overview.json +161 -30
  57. package/templates/grafana/dashboards/model-comparison.json +143 -0
@@ -0,0 +1,46 @@
1
+ export type AlertChannelConfig = {
2
+ readonly type: "telegram";
3
+ readonly token: string;
4
+ readonly chatId: string;
5
+ } | {
6
+ readonly type: "slack_webhook";
7
+ readonly url: string;
8
+ } | {
9
+ readonly type: "webhook";
10
+ readonly url: string;
11
+ readonly headers?: Record<string, string> | undefined;
12
+ } | {
13
+ readonly type: "email";
14
+ readonly host: string;
15
+ readonly port: number;
16
+ readonly user: string;
17
+ readonly password: string;
18
+ readonly from: string;
19
+ readonly to: string | readonly string[];
20
+ };
21
+ export interface AlertRule {
22
+ readonly name: string;
23
+ readonly metric: string;
24
+ readonly condition: string;
25
+ readonly channels: readonly string[];
26
+ readonly cooldown?: number | undefined;
27
+ }
28
+ export interface AlertsConfig {
29
+ readonly prometheusUrl?: string | undefined;
30
+ readonly grafanaUrl?: string | undefined;
31
+ readonly grafanaApiKey?: string | undefined;
32
+ readonly evalIntervalSeconds?: number | undefined;
33
+ readonly cooldownMinutes?: number | undefined;
34
+ readonly channels?: Record<string, AlertChannelConfig> | undefined;
35
+ readonly alerts: readonly AlertRule[];
36
+ }
37
+ export interface FiredAlert {
38
+ readonly rule: AlertRule;
39
+ readonly value: number;
40
+ readonly threshold: number;
41
+ readonly topModels: ReadonlyArray<{
42
+ model: string;
43
+ value: number;
44
+ }>;
45
+ readonly firedAt: Date;
46
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
2
+ import { execSync, spawn } from "node:child_process";
3
3
  import { cpSync, existsSync, mkdirSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { initObservability, traceLLMCall, shutdown, calculateCost, } from "./index.js";
6
7
  const INFRA_DIR = "infra/toad-eye";
7
8
  const SERVICES = [
8
9
  { name: "Grafana", url: "http://localhost:3100", login: "admin / admin" },
@@ -84,6 +85,72 @@ function status() {
84
85
  console.log("Could not check container status. Is Docker running?");
85
86
  }
86
87
  }
88
+ const DEMO_MODELS = [
89
+ { provider: "anthropic", model: "claude-sonnet-4-20250514" },
90
+ { provider: "gemini", model: "gemini-2.5-flash" },
91
+ { provider: "openai", model: "gpt-4o" },
92
+ ];
93
+ function randomBetween(min, max) {
94
+ return Math.floor(Math.random() * (max - min + 1)) + min;
95
+ }
96
+ const DEMO_PROMPTS = [
97
+ "Explain quantum computing",
98
+ "Write a haiku about frogs",
99
+ "What is the capital of Uruguay?",
100
+ "How does TCP work?",
101
+ "Translate hello to Japanese",
102
+ ];
103
+ function pickRandom(arr) {
104
+ return arr[randomBetween(0, arr.length - 1)];
105
+ }
106
+ async function simulateLLMCall(provider, model, prompt) {
107
+ await new Promise((r) => setTimeout(r, randomBetween(200, 2000)));
108
+ if (Math.random() < 0.1) {
109
+ throw new Error(`${provider} API error: rate limit exceeded`);
110
+ }
111
+ const inputTokens = randomBetween(50, 500);
112
+ const outputTokens = randomBetween(20, 300);
113
+ return {
114
+ completion: `Mock response for: "${prompt}"`,
115
+ inputTokens,
116
+ outputTokens,
117
+ cost: calculateCost(model, inputTokens, outputTokens),
118
+ };
119
+ }
120
+ async function demo() {
121
+ const composeFile = getComposeFile();
122
+ if (!existsSync(composeFile)) {
123
+ console.error(`\u274c ${INFRA_DIR}/ not found. Run \`npx toad-eye init\` first.`);
124
+ process.exit(1);
125
+ }
126
+ initObservability({
127
+ serviceName: "toad-eye-demo",
128
+ endpoint: "http://localhost:4318",
129
+ });
130
+ console.log("\u{1f438}\u{1f441}\u{fe0f} toad-eye demo — sending mock LLM traffic");
131
+ console.log(" Press Ctrl+C to stop\n");
132
+ process.on("SIGINT", async () => {
133
+ console.log("\n\u{1f44b} Shutting down...");
134
+ await shutdown();
135
+ process.exit(0);
136
+ });
137
+ const tick = async () => {
138
+ const { provider, model } = pickRandom(DEMO_MODELS);
139
+ const prompt = pickRandom(DEMO_PROMPTS);
140
+ try {
141
+ await traceLLMCall({ provider, model, prompt, temperature: 0.7 }, () => simulateLLMCall(provider, model, prompt));
142
+ console.log(` \u2705 [${provider}/${model}] ${prompt}`);
143
+ }
144
+ catch (err) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ console.log(` \u274c [${provider}/${model}] ${msg}`);
147
+ }
148
+ };
149
+ const run = () => {
150
+ tick().then(() => setTimeout(run, 2000));
151
+ };
152
+ run();
153
+ }
87
154
  function help() {
88
155
  console.log(`
89
156
  \u{1f438} toad-eye CLI — observability stack for LLM services
@@ -93,6 +160,7 @@ Commands:
93
160
  up Start the stack (OTel Collector + Prometheus + Jaeger + Grafana)
94
161
  down Stop the stack
95
162
  status Show running services and URLs
163
+ demo Send mock LLM traffic to see data in Grafana
96
164
  help Show this message
97
165
  `);
98
166
  }
@@ -110,6 +178,9 @@ switch (command) {
110
178
  case "status":
111
179
  status();
112
180
  break;
181
+ case "demo":
182
+ demo();
183
+ break;
113
184
  case "help":
114
185
  case "--help":
115
186
  case "-h":
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { initObservability, shutdown, getConfig } from "./tracer.js";
2
+ export { AlertManager, startAlertsFromFile, parseCondition, } from "./alerts/index.js";
3
+ export type { AlertsConfig, AlertRule, AlertChannelConfig, FiredAlert, } from "./alerts/index.js";
2
4
  export { traceLLMCall } from "./spans.js";
5
+ export { calculateCost, setCustomPricing, getModelPricing } from "./pricing.js";
6
+ export type { ModelPricing } from "./pricing.js";
3
7
  export type { LLMCallInput, LLMCallOutput } from "./spans.js";
4
- export type { ToadEyeConfig, LLMSpanAttributes, LLMProvider, SpanStatus, MetricName, } from "./types.js";
5
- export { LLM_METRICS, LLM_ATTRS } from "./types.js";
8
+ export type { ToadEyeConfig, LLMSpanAttributes, LLMProvider, SpanStatus, MetricName, } from "./types/index.js";
9
+ export { GEN_AI_METRICS, GEN_AI_ATTRS, LLM_METRICS, LLM_ATTRS, } from "./types/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { initObservability, shutdown, getConfig } from "./tracer.js";
2
+ export { AlertManager, startAlertsFromFile, parseCondition, } from "./alerts/index.js";
2
3
  export { traceLLMCall } from "./spans.js";
3
- export { LLM_METRICS, LLM_ATTRS } from "./types.js";
4
+ export { calculateCost, setCustomPricing, getModelPricing } from "./pricing.js";
5
+ export { GEN_AI_METRICS, GEN_AI_ATTRS, LLM_METRICS, LLM_ATTRS, } from "./types/index.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { register } from "./registry.js";
2
+ import { createInstrumentation } from "./create.js";
3
+ function extractMessages(messages) {
4
+ if (!Array.isArray(messages))
5
+ return "";
6
+ return messages
7
+ .map((m) => {
8
+ if (typeof m.content === "string")
9
+ return m.content;
10
+ if (Array.isArray(m.content)) {
11
+ return m.content
12
+ .filter((b) => b.type === "text")
13
+ .map((b) => b.text)
14
+ .join("");
15
+ }
16
+ return "";
17
+ })
18
+ .filter(Boolean)
19
+ .join("\n");
20
+ }
21
+ function extractCompletion(content) {
22
+ if (!Array.isArray(content))
23
+ return "";
24
+ return content
25
+ .filter((b) => b.type === "text")
26
+ .map((b) => b.text ?? "")
27
+ .join("");
28
+ }
29
+ const messagesCreate = {
30
+ getPrototype: (sdk) => sdk?.Messages?.prototype,
31
+ method: "create",
32
+ shouldSkip: (body) => !!body?.stream,
33
+ extractRequest: (body) => {
34
+ const b = body;
35
+ return {
36
+ prompt: extractMessages(b?.messages),
37
+ model: b?.model ?? "unknown",
38
+ temperature: b?.temperature ?? 1.0,
39
+ };
40
+ },
41
+ extractResponse: (response) => {
42
+ const r = response;
43
+ return {
44
+ completion: extractCompletion(r?.content),
45
+ inputTokens: r?.usage?.input_tokens ?? 0,
46
+ outputTokens: r?.usage?.output_tokens ?? 0,
47
+ };
48
+ },
49
+ };
50
+ register(createInstrumentation({
51
+ name: "anthropic",
52
+ moduleName: "@anthropic-ai/sdk",
53
+ patches: [messagesCreate],
54
+ }));
@@ -0,0 +1,11 @@
1
+ import type { LLMProvider } from "../types/index.js";
2
+ import type { Instrumentation, PatchTarget } from "./types.js";
3
+ /**
4
+ * Factory for creating SDK instrumentations.
5
+ * Eliminates boilerplate: SDK resolution, prototype patching, error handling, cleanup.
6
+ */
7
+ export declare function createInstrumentation(config: {
8
+ name: LLMProvider;
9
+ moduleName: string;
10
+ patches: PatchTarget[];
11
+ }): Instrumentation;
@@ -0,0 +1,67 @@
1
+ import { createRequire } from "node:module";
2
+ import { diag } from "@opentelemetry/api";
3
+ import { traceLLMCall } from "../spans.js";
4
+ import { calculateCost } from "../pricing.js";
5
+ const require = createRequire(import.meta.url);
6
+ /**
7
+ * Factory for creating SDK instrumentations.
8
+ * Eliminates boilerplate: SDK resolution, prototype patching, error handling, cleanup.
9
+ */
10
+ export function createInstrumentation(config) {
11
+ const activePatches = [];
12
+ return {
13
+ name: config.name,
14
+ enable() {
15
+ try {
16
+ require.resolve(config.moduleName);
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ try {
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const sdk = require(config.moduleName);
24
+ const mod = sdk.default ?? sdk;
25
+ for (const patch of config.patches) {
26
+ const proto = patch.getPrototype(mod);
27
+ if (!proto?.[patch.method])
28
+ continue;
29
+ const original = proto[patch.method];
30
+ proto[patch.method] = function patchedMethod(body, ...rest) {
31
+ if (patch.shouldSkip?.(body)) {
32
+ return original.call(this, body, ...rest);
33
+ }
34
+ const req = patch.extractRequest(body);
35
+ return traceLLMCall({
36
+ provider: config.name,
37
+ model: req.model,
38
+ prompt: req.prompt,
39
+ temperature: req.temperature,
40
+ }, async () => {
41
+ const response = await original.call(this, body, ...rest);
42
+ const res = patch.extractResponse(response, req.model);
43
+ return {
44
+ completion: res.completion,
45
+ inputTokens: res.inputTokens,
46
+ outputTokens: res.outputTokens,
47
+ cost: calculateCost(req.model, res.inputTokens, res.outputTokens),
48
+ };
49
+ });
50
+ };
51
+ activePatches.push({ proto, method: patch.method, original });
52
+ }
53
+ return activePatches.length > 0;
54
+ }
55
+ catch (err) {
56
+ diag.warn(`toad-eye: failed to patch ${config.name}: ${err}`);
57
+ return false;
58
+ }
59
+ },
60
+ disable() {
61
+ for (const { proto, method, original } of activePatches) {
62
+ proto[method] = original;
63
+ }
64
+ activePatches.length = 0;
65
+ },
66
+ };
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { register } from "./registry.js";
2
+ import { createInstrumentation } from "./create.js";
3
+ function extractPrompt(request) {
4
+ if (typeof request === "string")
5
+ return request;
6
+ if (typeof request === "object" && request !== null) {
7
+ const req = request;
8
+ if (Array.isArray(req.contents)) {
9
+ return req.contents
10
+ .flatMap((c) => c.parts ?? [])
11
+ .map((p) => p.text ?? "")
12
+ .filter(Boolean)
13
+ .join("\n");
14
+ }
15
+ }
16
+ return "";
17
+ }
18
+ const generateContent = {
19
+ getPrototype: (sdk) => sdk?.GenerativeModel?.prototype,
20
+ method: "generateContent",
21
+ extractRequest(body) {
22
+ return {
23
+ prompt: extractPrompt(body),
24
+ // model comes from `this.model` — injected by create.ts via the patched function's `this`
25
+ model: "unknown",
26
+ };
27
+ },
28
+ extractResponse: (response) => {
29
+ const r = response;
30
+ let completion = "";
31
+ try {
32
+ completion = r?.response?.text?.() ?? "";
33
+ }
34
+ catch {
35
+ // text() may throw if response is blocked
36
+ }
37
+ return {
38
+ completion,
39
+ inputTokens: r?.response?.usageMetadata?.promptTokenCount ?? 0,
40
+ outputTokens: r?.response?.usageMetadata?.candidatesTokenCount ?? 0,
41
+ };
42
+ },
43
+ };
44
+ register(createInstrumentation({
45
+ name: "gemini",
46
+ moduleName: "@google/generative-ai",
47
+ patches: [generateContent],
48
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { register } from "./registry.js";
2
+ import { createInstrumentation } from "./create.js";
3
+ function extractMessages(messages) {
4
+ if (!Array.isArray(messages))
5
+ return "";
6
+ return messages
7
+ .map((m) => typeof m.content === "string" ? m.content : "")
8
+ .filter(Boolean)
9
+ .join("\n");
10
+ }
11
+ const chatCompletions = {
12
+ getPrototype: (sdk) => (sdk?.Chat?.Completions ?? sdk?.Completions)?.prototype,
13
+ method: "create",
14
+ shouldSkip: (body) => !!body?.stream,
15
+ extractRequest: (body) => {
16
+ const b = body;
17
+ return {
18
+ prompt: extractMessages(b?.messages),
19
+ model: b?.model ?? "unknown",
20
+ temperature: b?.temperature ?? 1.0,
21
+ };
22
+ },
23
+ extractResponse: (response) => {
24
+ const r = response;
25
+ return {
26
+ completion: r?.choices?.[0]?.message?.content ?? "",
27
+ inputTokens: r?.usage?.prompt_tokens ?? 0,
28
+ outputTokens: r?.usage?.completion_tokens ?? 0,
29
+ };
30
+ },
31
+ };
32
+ const embeddings = {
33
+ getPrototype: (sdk) => sdk?.Embeddings?.prototype,
34
+ method: "create",
35
+ extractRequest: (body) => {
36
+ const b = body;
37
+ const input = b?.input;
38
+ const prompt = typeof input === "string"
39
+ ? input
40
+ : Array.isArray(input)
41
+ ? input.join("\n")
42
+ : "";
43
+ return { prompt, model: b?.model ?? "unknown" };
44
+ },
45
+ extractResponse: (response) => {
46
+ const r = response;
47
+ return {
48
+ completion: "[embedding]",
49
+ inputTokens: r?.usage?.prompt_tokens ?? 0,
50
+ outputTokens: 0,
51
+ };
52
+ },
53
+ };
54
+ register(createInstrumentation({
55
+ name: "openai",
56
+ moduleName: "openai",
57
+ patches: [chatCompletions, embeddings],
58
+ }));
@@ -0,0 +1,5 @@
1
+ import type { LLMProvider } from "../types/index.js";
2
+ import type { Instrumentation } from "./types.js";
3
+ export declare function register(inst: Instrumentation): void;
4
+ export declare function enableAll(providers: readonly LLMProvider[]): void;
5
+ export declare function disableAll(): void;
@@ -0,0 +1,31 @@
1
+ import { diag } from "@opentelemetry/api";
2
+ const instrumentations = new Map();
3
+ const active = new Set();
4
+ export function register(inst) {
5
+ instrumentations.set(inst.name, inst);
6
+ }
7
+ export function enableAll(providers) {
8
+ for (const name of providers) {
9
+ if (active.has(name))
10
+ continue;
11
+ const inst = instrumentations.get(name);
12
+ if (!inst) {
13
+ diag.warn(`toad-eye: unknown provider "${name}", skipping`);
14
+ continue;
15
+ }
16
+ const patched = inst.enable();
17
+ if (patched) {
18
+ active.add(name);
19
+ diag.debug(`toad-eye: auto-instrumented ${name}`);
20
+ }
21
+ else {
22
+ diag.debug(`toad-eye: ${name} SDK not found, skipping`);
23
+ }
24
+ }
25
+ }
26
+ export function disableAll() {
27
+ for (const name of active) {
28
+ instrumentations.get(name)?.disable();
29
+ }
30
+ active.clear();
31
+ }
@@ -0,0 +1,29 @@
1
+ import type { LLMProvider } from "../types/index.js";
2
+ export interface Instrumentation {
3
+ readonly name: LLMProvider;
4
+ /** Try to find and patch the SDK. Returns true if patched, false if SDK not found. */
5
+ enable(): boolean;
6
+ /** Restore original methods. */
7
+ disable(): void;
8
+ }
9
+ /** Describes a single method to monkey-patch on an SDK prototype. */
10
+ export interface PatchTarget {
11
+ /** How to find the prototype from the loaded SDK module */
12
+ getPrototype: (sdk: any) => any | undefined;
13
+ /** Method name on the prototype to patch */
14
+ method: string;
15
+ /** Extract LLMCallInput fields from the request arguments */
16
+ extractRequest: (body: unknown) => {
17
+ prompt: string;
18
+ model: string;
19
+ temperature?: number;
20
+ };
21
+ /** Extract LLMCallOutput fields from the SDK response */
22
+ extractResponse: (response: unknown, model: string) => {
23
+ completion: string;
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ };
27
+ /** Return true if this call should skip patching (e.g. streaming) */
28
+ shouldSkip?: (body: unknown) => boolean;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/metrics.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { metrics } from "@opentelemetry/api";
2
- import { LLM_METRICS, INSTRUMENTATION_NAME } from "./types.js";
2
+ import { GEN_AI_ATTRS, GEN_AI_METRICS, INSTRUMENTATION_NAME, } from "./types/index.js";
3
3
  let requestDuration;
4
4
  let requestCost;
5
- let tokensTotal;
5
+ let tokenUsage;
6
6
  let requestsTotal;
7
7
  let errorsTotal;
8
8
  let initialized = false;
@@ -10,37 +10,52 @@ export function initMetrics() {
10
10
  if (initialized)
11
11
  return;
12
12
  const meter = metrics.getMeter(INSTRUMENTATION_NAME);
13
- requestDuration = meter.createHistogram(LLM_METRICS.REQUEST_DURATION, {
14
- description: "LLM request duration in milliseconds",
13
+ requestDuration = meter.createHistogram(GEN_AI_METRICS.REQUEST_DURATION, {
14
+ description: "GenAI operation duration in milliseconds",
15
15
  unit: "ms",
16
16
  });
17
- requestCost = meter.createHistogram(LLM_METRICS.REQUEST_COST, {
18
- description: "LLM request cost in USD",
17
+ requestCost = meter.createHistogram(GEN_AI_METRICS.REQUEST_COST, {
18
+ description: "GenAI request cost in USD",
19
19
  unit: "USD",
20
20
  });
21
- tokensTotal = meter.createCounter(LLM_METRICS.TOKENS, {
21
+ tokenUsage = meter.createCounter(GEN_AI_METRICS.TOKEN_USAGE, {
22
22
  description: "Total tokens consumed",
23
23
  });
24
- requestsTotal = meter.createCounter(LLM_METRICS.REQUESTS, {
25
- description: "Total LLM requests",
24
+ requestsTotal = meter.createCounter(GEN_AI_METRICS.REQUESTS, {
25
+ description: "Total GenAI requests",
26
26
  });
27
- errorsTotal = meter.createCounter(LLM_METRICS.ERRORS, {
28
- description: "Total failed LLM requests",
27
+ errorsTotal = meter.createCounter(GEN_AI_METRICS.ERRORS, {
28
+ description: "Total failed GenAI requests",
29
29
  });
30
30
  initialized = true;
31
31
  }
32
32
  export function recordRequestDuration(ms, provider, model) {
33
- requestDuration.record(ms, { provider, model });
33
+ requestDuration.record(ms, {
34
+ [GEN_AI_ATTRS.PROVIDER]: provider,
35
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
36
+ });
34
37
  }
35
38
  export function recordRequestCost(usd, provider, model) {
36
- requestCost.record(usd, { provider, model });
39
+ requestCost.record(usd, {
40
+ [GEN_AI_ATTRS.PROVIDER]: provider,
41
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
42
+ });
37
43
  }
38
44
  export function recordTokens(count, provider, model) {
39
- tokensTotal.add(count, { provider, model });
45
+ tokenUsage.add(count, {
46
+ [GEN_AI_ATTRS.PROVIDER]: provider,
47
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
48
+ });
40
49
  }
41
50
  export function recordRequest(provider, model) {
42
- requestsTotal.add(1, { provider, model });
51
+ requestsTotal.add(1, {
52
+ [GEN_AI_ATTRS.PROVIDER]: provider,
53
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
54
+ });
43
55
  }
44
56
  export function recordError(provider, model) {
45
- errorsTotal.add(1, { provider, model });
57
+ errorsTotal.add(1, {
58
+ [GEN_AI_ATTRS.PROVIDER]: provider,
59
+ [GEN_AI_ATTRS.REQUEST_MODEL]: model,
60
+ });
46
61
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Built-in pricing table for major LLM models.
3
+ * Prices in USD per 1M tokens (input / output).
4
+ * Updated: March 2026.
5
+ */
6
+ export interface ModelPricing {
7
+ readonly inputPer1M: number;
8
+ readonly outputPer1M: number;
9
+ }
10
+ /**
11
+ * Set custom pricing for models (overrides built-in prices).
12
+ * Useful for enterprise contracts or custom/fine-tuned models.
13
+ */
14
+ export declare function setCustomPricing(pricing: Record<string, ModelPricing>): void;
15
+ /**
16
+ * Get pricing for a model. Custom pricing takes precedence.
17
+ * Returns undefined if model is not in any pricing table.
18
+ */
19
+ export declare function getModelPricing(model: string): ModelPricing | undefined;
20
+ /**
21
+ * Calculate cost for a given model and token counts.
22
+ * Returns 0 if model pricing is not found.
23
+ */
24
+ export declare function calculateCost(model: string, inputTokens: number, outputTokens: number): number;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Built-in pricing table for major LLM models.
3
+ * Prices in USD per 1M tokens (input / output).
4
+ * Updated: March 2026.
5
+ */
6
+ const BUILT_IN_PRICING = {
7
+ // OpenAI
8
+ "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
9
+ "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6 },
10
+ "gpt-4-turbo": { inputPer1M: 10, outputPer1M: 30 },
11
+ "gpt-4.1": { inputPer1M: 2, outputPer1M: 8 },
12
+ "gpt-4.1-mini": { inputPer1M: 0.4, outputPer1M: 1.6 },
13
+ "gpt-4.1-nano": { inputPer1M: 0.1, outputPer1M: 0.4 },
14
+ o3: { inputPer1M: 10, outputPer1M: 40 },
15
+ "o3-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
16
+ "o4-mini": { inputPer1M: 1.1, outputPer1M: 4.4 },
17
+ // Anthropic
18
+ "claude-opus-4-20250514": { inputPer1M: 15, outputPer1M: 75 },
19
+ "claude-sonnet-4-20250514": { inputPer1M: 3, outputPer1M: 15 },
20
+ "claude-haiku-3-5-20241022": { inputPer1M: 0.8, outputPer1M: 4 },
21
+ // Google Gemini
22
+ "gemini-2.5-pro": { inputPer1M: 1.25, outputPer1M: 10 },
23
+ "gemini-2.5-flash": { inputPer1M: 0.15, outputPer1M: 0.6 },
24
+ "gemini-2.0-flash": { inputPer1M: 0.1, outputPer1M: 0.4 },
25
+ };
26
+ let customPricing = {};
27
+ /**
28
+ * Set custom pricing for models (overrides built-in prices).
29
+ * Useful for enterprise contracts or custom/fine-tuned models.
30
+ */
31
+ export function setCustomPricing(pricing) {
32
+ customPricing = { ...pricing };
33
+ }
34
+ /**
35
+ * Get pricing for a model. Custom pricing takes precedence.
36
+ * Returns undefined if model is not in any pricing table.
37
+ */
38
+ export function getModelPricing(model) {
39
+ return customPricing[model] ?? BUILT_IN_PRICING[model];
40
+ }
41
+ /**
42
+ * Calculate cost for a given model and token counts.
43
+ * Returns 0 if model pricing is not found.
44
+ */
45
+ export function calculateCost(model, inputTokens, outputTokens) {
46
+ const pricing = getModelPricing(model);
47
+ if (!pricing)
48
+ return 0;
49
+ const cost = (inputTokens / 1_000_000) * pricing.inputPer1M +
50
+ (outputTokens / 1_000_000) * pricing.outputPer1M;
51
+ return Math.round(cost * 1_000_000) / 1_000_000;
52
+ }
package/dist/spans.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { LLMSpanAttributes } from "./types.js";
1
+ import type { LLMSpanAttributes } from "./types/index.js";
2
2
  /** Input for traceLLMCall — what the user knows before calling the LLM */
3
3
  export interface LLMCallInput {
4
4
  readonly provider: LLMSpanAttributes["provider"];