@node-llm/monitor-otel 0.1.0 → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NodeLLM contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -75,6 +75,8 @@ function extractAIAttributes(span) {
75
75
  // Usage - AI SDK attributes
76
76
  promptTokens: getNumber(attrs, "ai.usage.promptTokens"),
77
77
  completionTokens: getNumber(attrs, "ai.usage.completionTokens"),
78
+ cachedInputTokens: getNumber(attrs, "ai.usage.cachedInputTokens"),
79
+ reasoningTokens: getNumber(attrs, "ai.usage.reasoningTokens"),
78
80
  // Timing (streaming)
79
81
  msToFirstChunk: getNumber(attrs, "ai.response.msToFirstChunk"),
80
82
  msToFinish: getNumber(attrs, "ai.response.msToFinish"),
@@ -90,6 +92,9 @@ function extractAIAttributes(span) {
90
92
  genAiResponseModel: getString(attrs, "gen_ai.response.model"),
91
93
  genAiInputTokens: getNumber(attrs, "gen_ai.usage.input_tokens"),
92
94
  genAiOutputTokens: getNumber(attrs, "gen_ai.usage.output_tokens"),
95
+ genAiCacheReadInputTokens: getNumber(attrs, "gen_ai.usage.cache_read.input_tokens"),
96
+ genAiCacheCreationInputTokens: getNumber(attrs, "gen_ai.usage.cache_creation.input_tokens"),
97
+ genAiReasoningOutputTokens: getNumber(attrs, "gen_ai.usage.reasoning.output_tokens"),
93
98
  // Custom metadata
94
99
  metadata: Object.keys(metadata).length > 0 ? metadata : void 0
95
100
  };
@@ -229,6 +234,9 @@ var NodeLLMSpanProcessor = class {
229
234
  const duration = calculateDurationMs(span.startTime, span.endTime);
230
235
  const promptTokens = attrs.promptTokens ?? attrs.genAiInputTokens ?? 0;
231
236
  const completionTokens = attrs.completionTokens ?? attrs.genAiOutputTokens ?? 0;
237
+ const cachedTokens = attrs.cachedInputTokens ?? attrs.genAiCacheReadInputTokens ?? 0;
238
+ const cacheCreationTokens = attrs.genAiCacheCreationInputTokens ?? 0;
239
+ const reasoningTokens = attrs.reasoningTokens ?? attrs.genAiReasoningOutputTokens ?? 0;
232
240
  const cost = estimateCost(provider, model, promptTokens, completionTokens);
233
241
  const requestId = this.options.sessionIdExtractor ? generateRequestId(span) : generateRequestId(span);
234
242
  const sessionId = this.options.sessionIdExtractor ? this.options.sessionIdExtractor(span) : extractSessionId(span);
@@ -239,7 +247,10 @@ var NodeLLMSpanProcessor = class {
239
247
  usage: {
240
248
  promptTokens,
241
249
  completionTokens,
242
- totalTokens: promptTokens + completionTokens
250
+ totalTokens: promptTokens + completionTokens,
251
+ ...cachedTokens > 0 && { cachedTokens },
252
+ ...cacheCreationTokens > 0 && { cacheCreationTokens },
253
+ ...reasoningTokens > 0 && { reasoningTokens }
243
254
  },
244
255
  // Streaming metrics
245
256
  ...attrs.msToFirstChunk !== void 0 && {
package/package.json CHANGED
@@ -1,20 +1,14 @@
1
1
  {
2
2
  "name": "@node-llm/monitor-otel",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "OpenTelemetry integration for NodeLLM Monitor",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "types": "dist/index.d.ts",
8
- "scripts": {
9
- "build": "tsup src/index.ts --format esm --dts --clean",
10
- "dev": "tsup src/index.ts --format esm --dts --watch",
11
- "lint": "eslint .",
12
- "test": "vitest run"
13
- },
14
8
  "dependencies": {
15
- "@node-llm/monitor": "workspace:*",
16
9
  "@opentelemetry/api": "^1.7.0",
17
- "@opentelemetry/sdk-trace-base": "^1.18.1"
10
+ "@opentelemetry/sdk-trace-base": "^1.18.1",
11
+ "@node-llm/monitor": "0.4.2"
18
12
  },
19
13
  "devDependencies": {
20
14
  "@types/node": "^25.2.0",
@@ -22,5 +16,11 @@
22
16
  "tsup": "^8.0.1",
23
17
  "typescript": "^5.3.3",
24
18
  "vitest": "^4.0.18"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm --dts --clean",
22
+ "dev": "tsup src/index.ts --format esm --dts --watch",
23
+ "lint": "eslint .",
24
+ "test": "vitest run"
25
25
  }
26
- }
26
+ }
@@ -137,6 +137,9 @@ export class NodeLLMSpanProcessor implements OTelSpanProcessor {
137
137
  // Calculate token counts (prefer AI SDK attributes, fallback to GenAI conventions)
138
138
  const promptTokens = attrs.promptTokens ?? attrs.genAiInputTokens ?? 0;
139
139
  const completionTokens = attrs.completionTokens ?? attrs.genAiOutputTokens ?? 0;
140
+ const cachedTokens = attrs.cachedInputTokens ?? attrs.genAiCacheReadInputTokens ?? 0;
141
+ const cacheCreationTokens = attrs.genAiCacheCreationInputTokens ?? 0;
142
+ const reasoningTokens = attrs.reasoningTokens ?? attrs.genAiReasoningOutputTokens ?? 0;
140
143
 
141
144
  // Estimate cost
142
145
  const cost = estimateCost(provider, model, promptTokens, completionTokens);
@@ -159,7 +162,10 @@ export class NodeLLMSpanProcessor implements OTelSpanProcessor {
159
162
  usage: {
160
163
  promptTokens,
161
164
  completionTokens,
162
- totalTokens: promptTokens + completionTokens
165
+ totalTokens: promptTokens + completionTokens,
166
+ ...(cachedTokens > 0 && { cachedTokens }),
167
+ ...(cacheCreationTokens > 0 && { cacheCreationTokens }),
168
+ ...(reasoningTokens > 0 && { reasoningTokens })
163
169
  },
164
170
  // Streaming metrics
165
171
  ...(attrs.msToFirstChunk !== undefined && {
package/src/types.ts CHANGED
@@ -81,6 +81,9 @@ export interface AISpanAttributes {
81
81
  promptTokens?: number;
82
82
  completionTokens?: number;
83
83
  totalTokens?: number;
84
+ // Cache/reasoning usage (forward-compatible AI SDK attributes - not yet
85
+ cachedInputTokens?: number;
86
+ reasoningTokens?: number;
84
87
 
85
88
  // Timing (streaming)
86
89
  msToFirstChunk?: number;
@@ -99,6 +102,9 @@ export interface AISpanAttributes {
99
102
  genAiResponseModel?: string;
100
103
  genAiInputTokens?: number;
101
104
  genAiOutputTokens?: number;
105
+ genAiCacheReadInputTokens?: number;
106
+ genAiCacheCreationInputTokens?: number;
107
+ genAiReasoningOutputTokens?: number;
102
108
 
103
109
  // Custom metadata
104
110
  metadata?: Record<string, unknown>;
package/src/utils.ts CHANGED
@@ -131,6 +131,8 @@ export function extractAIAttributes(span: OTelReadableSpan): AISpanAttributes {
131
131
  // Usage - AI SDK attributes
132
132
  promptTokens: getNumber(attrs, "ai.usage.promptTokens"),
133
133
  completionTokens: getNumber(attrs, "ai.usage.completionTokens"),
134
+ cachedInputTokens: getNumber(attrs, "ai.usage.cachedInputTokens"),
135
+ reasoningTokens: getNumber(attrs, "ai.usage.reasoningTokens"),
134
136
 
135
137
  // Timing (streaming)
136
138
  msToFirstChunk: getNumber(attrs, "ai.response.msToFirstChunk"),
@@ -149,6 +151,9 @@ export function extractAIAttributes(span: OTelReadableSpan): AISpanAttributes {
149
151
  genAiResponseModel: getString(attrs, "gen_ai.response.model"),
150
152
  genAiInputTokens: getNumber(attrs, "gen_ai.usage.input_tokens"),
151
153
  genAiOutputTokens: getNumber(attrs, "gen_ai.usage.output_tokens"),
154
+ genAiCacheReadInputTokens: getNumber(attrs, "gen_ai.usage.cache_read.input_tokens"),
155
+ genAiCacheCreationInputTokens: getNumber(attrs, "gen_ai.usage.cache_creation.input_tokens"),
156
+ genAiReasoningOutputTokens: getNumber(attrs, "gen_ai.usage.reasoning.output_tokens"),
152
157
 
153
158
  // Custom metadata
154
159
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined
@@ -72,6 +72,31 @@ describe("NodeLLMSpanProcessor", () => {
72
72
  expect(event.payload.usage.totalTokens).toBe(225);
73
73
  });
74
74
 
75
+ it("should include cache/reasoning token usage from GenAI spans", async () => {
76
+ const span = createMockSpan("gen_ai.chat", {
77
+ attributes: {
78
+ "ai.model.id": undefined,
79
+ "ai.model.provider": undefined,
80
+ "gen_ai.system": "anthropic",
81
+ "gen_ai.request.model": "claude-3-5-sonnet",
82
+ "gen_ai.usage.input_tokens": 150,
83
+ "gen_ai.usage.output_tokens": 75,
84
+ "gen_ai.usage.cache_read.input_tokens": 100,
85
+ "gen_ai.usage.cache_creation.input_tokens": 20,
86
+ "gen_ai.usage.reasoning.output_tokens": 15
87
+ }
88
+ });
89
+
90
+ processor.onEnd(span);
91
+ await processor.forceFlush();
92
+
93
+ const event = (mockStore.saveEvent as any).mock.calls[0][0];
94
+
95
+ expect(event.payload.usage.cachedTokens).toBe(100);
96
+ expect(event.payload.usage.cacheCreationTokens).toBe(20);
97
+ expect(event.payload.usage.reasoningTokens).toBe(15);
98
+ });
99
+
75
100
  it("should ignore non-AI spans", async () => {
76
101
  const span = createMockSpan("http.get");
77
102
 
@@ -106,6 +106,38 @@ describe("otel utils", () => {
106
106
  expect(attrs.genAiOutputTokens).toBe(25);
107
107
  });
108
108
 
109
+ it("should extract OTel GenAI cache/reasoning token attributes", () => {
110
+ const span = {
111
+ attributes: {
112
+ "gen_ai.usage.input_tokens": 100,
113
+ "gen_ai.usage.output_tokens": 50,
114
+ "gen_ai.usage.cache_read.input_tokens": 75,
115
+ "gen_ai.usage.cache_creation.input_tokens": 25,
116
+ "gen_ai.usage.reasoning.output_tokens": 10
117
+ }
118
+ } as any;
119
+
120
+ const attrs = extractAIAttributes(span);
121
+ expect(attrs.genAiCacheReadInputTokens).toBe(75);
122
+ expect(attrs.genAiCacheCreationInputTokens).toBe(25);
123
+ expect(attrs.genAiReasoningOutputTokens).toBe(10);
124
+ });
125
+
126
+ it("should extract Vercel AI SDK cache/reasoning token attributes", () => {
127
+ const span = {
128
+ attributes: {
129
+ "ai.usage.promptTokens": 100,
130
+ "ai.usage.completionTokens": 50,
131
+ "ai.usage.cachedInputTokens": 60,
132
+ "ai.usage.reasoningTokens": 12
133
+ }
134
+ } as any;
135
+
136
+ const attrs = extractAIAttributes(span);
137
+ expect(attrs.cachedInputTokens).toBe(60);
138
+ expect(attrs.reasoningTokens).toBe(12);
139
+ });
140
+
109
141
  it("should extract custom metadata", () => {
110
142
  const span = {
111
143
  attributes: {