@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 +21 -0
- package/dist/index.js +12 -1
- package/package.json +10 -10
- package/src/NodeLLMSpanProcessor.ts +7 -1
- package/src/types.ts +6 -0
- package/src/utils.ts +5 -0
- package/test/NodeLLMSpanProcessor.test.ts +25 -0
- package/test/utils.test.ts +32 -0
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.
|
|
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
|
|
package/test/utils.test.ts
CHANGED
|
@@ -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: {
|