@node-llm/monitor-otel 0.1.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.
- package/README.md +68 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +325 -0
- package/package.json +26 -0
- package/src/NodeLLMSpanProcessor.ts +257 -0
- package/src/index.ts +1 -0
- package/src/types.ts +141 -0
- package/src/utils.ts +314 -0
- package/test/NodeLLMSpanProcessor.test.ts +165 -0
- package/test/fixtures/vercel-ai-spans.ts +379 -0
- package/test/integration.test.ts +432 -0
- package/test/utils.test.ts +278 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { NodeLLMSpanProcessor } from "../src/NodeLLMSpanProcessor.js";
|
|
3
|
+
import { MonitoringStore, MonitoringEvent } from "@node-llm/monitor";
|
|
4
|
+
|
|
5
|
+
describe("NodeLLMSpanProcessor", () => {
|
|
6
|
+
let mockStore: MonitoringStore;
|
|
7
|
+
let processor: NodeLLMSpanProcessor;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockStore = {
|
|
11
|
+
saveEvent: vi.fn().mockResolvedValue(undefined)
|
|
12
|
+
} as any;
|
|
13
|
+
processor = new NodeLLMSpanProcessor(mockStore);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const createMockSpan = (name: string, overrides = {}) => {
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
spanContext: () => ({ traceId: "trace-1", spanId: "span-2" }),
|
|
20
|
+
parentSpanId: "parent-0",
|
|
21
|
+
startTime: [1000, 0],
|
|
22
|
+
endTime: [1001, 0],
|
|
23
|
+
status: { code: 1 },
|
|
24
|
+
attributes: {
|
|
25
|
+
"ai.model.id": "gpt-4o",
|
|
26
|
+
"ai.model.provider": "openai",
|
|
27
|
+
"ai.usage.promptTokens": 100,
|
|
28
|
+
"ai.usage.completionTokens": 50,
|
|
29
|
+
...overrides
|
|
30
|
+
},
|
|
31
|
+
...overrides
|
|
32
|
+
} as any;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
it("should process top-level AI spans", async () => {
|
|
36
|
+
const span = createMockSpan("ai.generateText");
|
|
37
|
+
|
|
38
|
+
processor.onEnd(span);
|
|
39
|
+
await processor.forceFlush();
|
|
40
|
+
|
|
41
|
+
expect(mockStore.saveEvent).toHaveBeenCalled();
|
|
42
|
+
const event = (mockStore.saveEvent as any).mock.calls[0][0] as MonitoringEvent;
|
|
43
|
+
|
|
44
|
+
expect(event.provider).toBe("openai");
|
|
45
|
+
expect(event.model).toBe("gpt-4o");
|
|
46
|
+
expect(event.eventType).toBe("request.end");
|
|
47
|
+
expect(event.payload.usage.promptTokens).toBe(100);
|
|
48
|
+
expect(event.payload.usage.completionTokens).toBe(50);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should process standard (Non-Vercel) GenAI spans", async () => {
|
|
52
|
+
const span = createMockSpan("gen_ai.chat", {
|
|
53
|
+
attributes: {
|
|
54
|
+
"ai.model.id": undefined, // No Vercel attributes
|
|
55
|
+
"ai.model.provider": undefined,
|
|
56
|
+
"gen_ai.system": "anthropic",
|
|
57
|
+
"gen_ai.request.model": "claude-3-5-sonnet",
|
|
58
|
+
"gen_ai.usage.input_tokens": 150,
|
|
59
|
+
"gen_ai.usage.output_tokens": 75
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
processor.onEnd(span);
|
|
64
|
+
await processor.forceFlush();
|
|
65
|
+
|
|
66
|
+
expect(mockStore.saveEvent).toHaveBeenCalled();
|
|
67
|
+
const event = (mockStore.saveEvent as any).mock.calls[0][0];
|
|
68
|
+
|
|
69
|
+
expect(event.provider).toBe("anthropic");
|
|
70
|
+
expect(event.model).toBe("claude-3-5-sonnet");
|
|
71
|
+
expect(event.payload.usage.promptTokens).toBe(150);
|
|
72
|
+
expect(event.payload.usage.totalTokens).toBe(225);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should ignore non-AI spans", async () => {
|
|
76
|
+
const span = createMockSpan("http.get");
|
|
77
|
+
|
|
78
|
+
processor.onEnd(span);
|
|
79
|
+
await processor.forceFlush();
|
|
80
|
+
|
|
81
|
+
expect(mockStore.saveEvent).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should ignore internal (.doGenerate) AI spans", async () => {
|
|
85
|
+
const span = createMockSpan("ai.generateText.doGenerate");
|
|
86
|
+
|
|
87
|
+
processor.onEnd(span);
|
|
88
|
+
await processor.forceFlush();
|
|
89
|
+
|
|
90
|
+
expect(mockStore.saveEvent).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should apply custom filter if provided", async () => {
|
|
94
|
+
const filter = (span: any) => span.attributes.isAllowed === true;
|
|
95
|
+
processor = new NodeLLMSpanProcessor(mockStore, { filter });
|
|
96
|
+
|
|
97
|
+
const span1 = createMockSpan("ai.generateText", { attributes: { isAllowed: false } });
|
|
98
|
+
const span2 = createMockSpan("ai.generateText", { attributes: { isAllowed: true } });
|
|
99
|
+
|
|
100
|
+
processor.onEnd(span1);
|
|
101
|
+
processor.onEnd(span2);
|
|
102
|
+
await processor.forceFlush();
|
|
103
|
+
|
|
104
|
+
expect(mockStore.saveEvent).toHaveBeenCalledTimes(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should apply custom transform if provided", async () => {
|
|
108
|
+
const transform = (event: MonitoringEvent) => {
|
|
109
|
+
event.payload.customField = "modified";
|
|
110
|
+
return event;
|
|
111
|
+
};
|
|
112
|
+
processor = new NodeLLMSpanProcessor(mockStore, { transform });
|
|
113
|
+
|
|
114
|
+
const span = createMockSpan("ai.generateText");
|
|
115
|
+
processor.onEnd(span);
|
|
116
|
+
await processor.forceFlush();
|
|
117
|
+
|
|
118
|
+
const event = (mockStore.saveEvent as any).mock.calls[0][0];
|
|
119
|
+
expect(event.payload.customField).toBe("modified");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should capture content only when enabled", async () => {
|
|
123
|
+
// Enabled by default
|
|
124
|
+
const span = createMockSpan("ai.generateText", {
|
|
125
|
+
attributes: { "ai.prompt": "secret message" }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
processor.onEnd(span);
|
|
129
|
+
await processor.forceFlush();
|
|
130
|
+
expect((mockStore.saveEvent as any).mock.calls[0][0].payload.prompt).toBe("secret message");
|
|
131
|
+
|
|
132
|
+
// Disabled
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
processor = new NodeLLMSpanProcessor(mockStore, { captureContent: false });
|
|
135
|
+
processor.onEnd(span);
|
|
136
|
+
await processor.forceFlush();
|
|
137
|
+
expect((mockStore.saveEvent as any).mock.calls[0][0].payload.prompt).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle errors using onError callback", async () => {
|
|
141
|
+
const error = new Error("Database down");
|
|
142
|
+
(mockStore.saveEvent as any).mockRejectedValue(error);
|
|
143
|
+
|
|
144
|
+
const onError = vi.fn();
|
|
145
|
+
processor = new NodeLLMSpanProcessor(mockStore, { onError });
|
|
146
|
+
|
|
147
|
+
const span = createMockSpan("ai.generateText");
|
|
148
|
+
processor.onEnd(span);
|
|
149
|
+
|
|
150
|
+
// We need to wait for the async processSpan to fail
|
|
151
|
+
await vi.waitFor(() => expect(onError).toHaveBeenCalledWith(error, span));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should respect custom sessionIdExtractor", async () => {
|
|
155
|
+
const sessionIdExtractor = (span: any) => "custom-session-id";
|
|
156
|
+
processor = new NodeLLMSpanProcessor(mockStore, { sessionIdExtractor });
|
|
157
|
+
|
|
158
|
+
const span = createMockSpan("ai.generateText");
|
|
159
|
+
processor.onEnd(span);
|
|
160
|
+
await processor.forceFlush();
|
|
161
|
+
|
|
162
|
+
const event = (mockStore.saveEvent as any).mock.calls[0][0];
|
|
163
|
+
expect(event.sessionId).toBe("custom-session-id");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded span fixtures from Vercel AI SDK
|
|
3
|
+
*
|
|
4
|
+
* These represent real-world span data captured from various
|
|
5
|
+
* Vercel AI SDK operations for integration testing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OTelReadableSpan } from "../../src/types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper to create a mock span with realistic structure
|
|
12
|
+
*/
|
|
13
|
+
function createSpanFixture(
|
|
14
|
+
name: string,
|
|
15
|
+
attributes: Record<string, unknown>,
|
|
16
|
+
overrides: Partial<{
|
|
17
|
+
traceId: string;
|
|
18
|
+
spanId: string;
|
|
19
|
+
parentSpanId: string;
|
|
20
|
+
startTime: [number, number];
|
|
21
|
+
endTime: [number, number];
|
|
22
|
+
statusCode: number;
|
|
23
|
+
statusMessage: string;
|
|
24
|
+
}> = {}
|
|
25
|
+
): OTelReadableSpan {
|
|
26
|
+
const {
|
|
27
|
+
traceId = "a1b2c3d4e5f6789012345678901234ab",
|
|
28
|
+
spanId = "12345678abcdef01",
|
|
29
|
+
parentSpanId,
|
|
30
|
+
startTime = [1706918400, 0], // 2024-02-03 00:00:00
|
|
31
|
+
endTime = [1706918401, 500000000], // 1.5s later
|
|
32
|
+
statusCode = 1,
|
|
33
|
+
statusMessage
|
|
34
|
+
} = overrides;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
spanContext: () => ({ traceId, spanId }),
|
|
39
|
+
parentSpanId,
|
|
40
|
+
startTime,
|
|
41
|
+
endTime,
|
|
42
|
+
status: { code: statusCode, message: statusMessage },
|
|
43
|
+
attributes
|
|
44
|
+
} as OTelReadableSpan;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// OpenAI Spans
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ai.generateText with OpenAI gpt-4o-mini
|
|
53
|
+
* Recorded from: generateText({ model: openai("gpt-4o-mini"), prompt: "Hello" })
|
|
54
|
+
*/
|
|
55
|
+
export const openaiGenerateText = createSpanFixture(
|
|
56
|
+
"ai.generateText",
|
|
57
|
+
{
|
|
58
|
+
"ai.operationId": "ai.generateText",
|
|
59
|
+
"ai.model.id": "gpt-4o-mini",
|
|
60
|
+
"ai.model.provider": "openai.responses",
|
|
61
|
+
"ai.usage.promptTokens": 12,
|
|
62
|
+
"ai.usage.completionTokens": 28,
|
|
63
|
+
"ai.prompt": "Hello, how are you?",
|
|
64
|
+
"ai.response.text": "I'm doing well, thank you for asking! How can I help you today?",
|
|
65
|
+
"ai.response.finishReason": "stop",
|
|
66
|
+
"ai.telemetry.functionId": "chat-completion"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
traceId: "openai-trace-001",
|
|
70
|
+
spanId: "openai-span-001"
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ai.streamText with OpenAI gpt-4o
|
|
76
|
+
* Includes streaming metrics
|
|
77
|
+
*/
|
|
78
|
+
export const openaiStreamText = createSpanFixture(
|
|
79
|
+
"ai.streamText",
|
|
80
|
+
{
|
|
81
|
+
"ai.operationId": "ai.streamText",
|
|
82
|
+
"ai.model.id": "gpt-4o",
|
|
83
|
+
"ai.model.provider": "openai.chat",
|
|
84
|
+
"ai.usage.promptTokens": 156,
|
|
85
|
+
"ai.usage.completionTokens": 423,
|
|
86
|
+
"ai.prompt.messages": JSON.stringify([
|
|
87
|
+
{ role: "system", content: "You are a helpful assistant." },
|
|
88
|
+
{ role: "user", content: "Explain quantum computing in simple terms." }
|
|
89
|
+
]),
|
|
90
|
+
"ai.response.text": "Quantum computing is a type of computation...",
|
|
91
|
+
"ai.response.finishReason": "stop",
|
|
92
|
+
"ai.response.msToFirstChunk": 145,
|
|
93
|
+
"ai.response.msToFinish": 2340,
|
|
94
|
+
"ai.response.avgCompletionTokensPerSecond": 180.5,
|
|
95
|
+
"ai.telemetry.metadata.userId": "user-123",
|
|
96
|
+
"ai.telemetry.metadata.environment": "production"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
traceId: "openai-trace-002",
|
|
100
|
+
spanId: "openai-span-002",
|
|
101
|
+
startTime: [1706918500, 0],
|
|
102
|
+
endTime: [1706918502, 340000000]
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ai.generateObject with OpenAI - structured output
|
|
108
|
+
*/
|
|
109
|
+
export const openaiGenerateObject = createSpanFixture(
|
|
110
|
+
"ai.generateObject",
|
|
111
|
+
{
|
|
112
|
+
"ai.operationId": "ai.generateObject",
|
|
113
|
+
"ai.model.id": "gpt-4o-mini",
|
|
114
|
+
"ai.model.provider": "openai.responses",
|
|
115
|
+
"ai.usage.promptTokens": 89,
|
|
116
|
+
"ai.usage.completionTokens": 156,
|
|
117
|
+
"ai.prompt": "Extract the person's name and age from: John Smith is 32 years old.",
|
|
118
|
+
"ai.response.object": JSON.stringify({ name: "John Smith", age: 32 }),
|
|
119
|
+
"ai.response.finishReason": "stop"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
traceId: "openai-trace-003",
|
|
123
|
+
spanId: "openai-span-003"
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Anthropic Spans
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* ai.generateText with Anthropic Claude
|
|
133
|
+
*/
|
|
134
|
+
export const anthropicGenerateText = createSpanFixture(
|
|
135
|
+
"ai.generateText",
|
|
136
|
+
{
|
|
137
|
+
"ai.operationId": "ai.generateText",
|
|
138
|
+
"ai.model.id": "claude-3-5-sonnet-20241022",
|
|
139
|
+
"ai.model.provider": "anthropic.messages",
|
|
140
|
+
"ai.usage.promptTokens": 45,
|
|
141
|
+
"ai.usage.completionTokens": 312,
|
|
142
|
+
"ai.prompt": "Write a haiku about programming.",
|
|
143
|
+
"ai.response.text": "Code flows like water\nBugs emerge from the shadows\nDebug, test, repeat",
|
|
144
|
+
"ai.response.finishReason": "end_turn"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
traceId: "anthropic-trace-001",
|
|
148
|
+
spanId: "anthropic-span-001"
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* ai.streamText with Anthropic Claude (using genAi attributes)
|
|
154
|
+
*/
|
|
155
|
+
export const anthropicGenAiFormat = createSpanFixture(
|
|
156
|
+
"gen_ai.chat",
|
|
157
|
+
{
|
|
158
|
+
"gen_ai.system": "anthropic",
|
|
159
|
+
"gen_ai.request.model": "claude-3-opus",
|
|
160
|
+
"gen_ai.usage.input_tokens": 234,
|
|
161
|
+
"gen_ai.usage.output_tokens": 567
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
traceId: "anthropic-trace-002",
|
|
165
|
+
spanId: "anthropic-span-002"
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Google Gemini Spans
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ai.generateText with Google Gemini
|
|
175
|
+
*/
|
|
176
|
+
export const geminiGenerateText = createSpanFixture(
|
|
177
|
+
"ai.generateText",
|
|
178
|
+
{
|
|
179
|
+
"ai.operationId": "ai.generateText",
|
|
180
|
+
"ai.model.id": "gemini-1.5-pro",
|
|
181
|
+
"ai.model.provider": "google.generativeai",
|
|
182
|
+
"ai.usage.promptTokens": 78,
|
|
183
|
+
"ai.usage.completionTokens": 245,
|
|
184
|
+
"ai.prompt": "What is the capital of France?",
|
|
185
|
+
"ai.response.text": "The capital of France is Paris...",
|
|
186
|
+
"ai.response.finishReason": "STOP"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
traceId: "google-trace-001",
|
|
190
|
+
spanId: "google-span-001"
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Tool Call Spans
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* ai.toolCall span - weather tool
|
|
200
|
+
*/
|
|
201
|
+
export const toolCallWeather = createSpanFixture(
|
|
202
|
+
"ai.toolCall",
|
|
203
|
+
{
|
|
204
|
+
"ai.operationId": "ai.toolCall",
|
|
205
|
+
"ai.toolCall.name": "getWeather",
|
|
206
|
+
"ai.toolCall.id": "call_abc123",
|
|
207
|
+
"ai.toolCall.args": JSON.stringify({ city: "London", units: "celsius" }),
|
|
208
|
+
"ai.toolCall.result": JSON.stringify({ temperature: 18, condition: "cloudy" })
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
traceId: "tool-trace-001",
|
|
212
|
+
spanId: "tool-span-001",
|
|
213
|
+
parentSpanId: "parent-span-001"
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* ai.toolCall span - database query
|
|
219
|
+
*/
|
|
220
|
+
export const toolCallDatabase = createSpanFixture(
|
|
221
|
+
"ai.toolCall",
|
|
222
|
+
{
|
|
223
|
+
"ai.operationId": "ai.toolCall",
|
|
224
|
+
"ai.toolCall.name": "queryDatabase",
|
|
225
|
+
"ai.toolCall.id": "call_def456",
|
|
226
|
+
"ai.toolCall.args": JSON.stringify({ query: "SELECT * FROM users WHERE active = true" }),
|
|
227
|
+
"ai.toolCall.result": JSON.stringify({ rows: 42, duration: "12ms" })
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
traceId: "tool-trace-002",
|
|
231
|
+
spanId: "tool-span-002",
|
|
232
|
+
parentSpanId: "parent-span-002"
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Error Spans
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* ai.generateText with rate limit error
|
|
242
|
+
*/
|
|
243
|
+
export const errorRateLimit = createSpanFixture(
|
|
244
|
+
"ai.generateText",
|
|
245
|
+
{
|
|
246
|
+
"ai.operationId": "ai.generateText",
|
|
247
|
+
"ai.model.id": "gpt-4o",
|
|
248
|
+
"ai.model.provider": "openai.responses",
|
|
249
|
+
"ai.prompt": "This request will fail"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
traceId: "error-trace-001",
|
|
253
|
+
spanId: "error-span-001",
|
|
254
|
+
statusCode: 2,
|
|
255
|
+
statusMessage: "Rate limit exceeded. Please retry after 60 seconds."
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* ai.generateText with context length error
|
|
261
|
+
*/
|
|
262
|
+
export const errorContextLength = createSpanFixture(
|
|
263
|
+
"ai.generateText",
|
|
264
|
+
{
|
|
265
|
+
"ai.operationId": "ai.generateText",
|
|
266
|
+
"ai.model.id": "gpt-4o-mini",
|
|
267
|
+
"ai.model.provider": "openai.chat",
|
|
268
|
+
"ai.prompt": "Very long prompt that exceeds context..."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
traceId: "error-trace-002",
|
|
272
|
+
spanId: "error-span-002",
|
|
273
|
+
statusCode: 2,
|
|
274
|
+
statusMessage: "This model's maximum context length is 128000 tokens."
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Internal Spans (should be filtered out)
|
|
280
|
+
// ============================================================================
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Internal .doGenerate span (should NOT be processed)
|
|
284
|
+
*/
|
|
285
|
+
export const internalDoGenerate = createSpanFixture(
|
|
286
|
+
"ai.generateText.doGenerate",
|
|
287
|
+
{
|
|
288
|
+
"ai.model.id": "gpt-4o-mini",
|
|
289
|
+
"ai.model.provider": "openai.responses",
|
|
290
|
+
"ai.usage.promptTokens": 12,
|
|
291
|
+
"ai.usage.completionTokens": 28
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
traceId: "internal-trace-001",
|
|
295
|
+
spanId: "internal-span-001",
|
|
296
|
+
parentSpanId: "parent-span-001"
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* HTTP span (should NOT be processed)
|
|
302
|
+
*/
|
|
303
|
+
export const httpRequest = createSpanFixture(
|
|
304
|
+
"HTTP POST",
|
|
305
|
+
{
|
|
306
|
+
"http.method": "POST",
|
|
307
|
+
"http.url": "https://api.openai.com/v1/chat/completions",
|
|
308
|
+
"http.status_code": 200
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
traceId: "http-trace-001",
|
|
312
|
+
spanId: "http-span-001"
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Embedding Spans
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* ai.embed span
|
|
322
|
+
*/
|
|
323
|
+
export const openaiEmbed = createSpanFixture(
|
|
324
|
+
"ai.embed",
|
|
325
|
+
{
|
|
326
|
+
"ai.operationId": "ai.embed",
|
|
327
|
+
"ai.model.id": "text-embedding-3-small",
|
|
328
|
+
"ai.model.provider": "openai.embedding",
|
|
329
|
+
"ai.usage.promptTokens": 8
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
traceId: "embed-trace-001",
|
|
333
|
+
spanId: "embed-span-001"
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* ai.embedMany span
|
|
339
|
+
*/
|
|
340
|
+
export const openaiEmbedMany = createSpanFixture(
|
|
341
|
+
"ai.embedMany",
|
|
342
|
+
{
|
|
343
|
+
"ai.operationId": "ai.embedMany",
|
|
344
|
+
"ai.model.id": "text-embedding-3-large",
|
|
345
|
+
"ai.model.provider": "openai.embedding",
|
|
346
|
+
"ai.usage.promptTokens": 156
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
traceId: "embed-trace-002",
|
|
350
|
+
spanId: "embed-span-002"
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Export all fixtures for easy iteration
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Valid spans that should be processed (excludes tool calls which need special handling)
|
|
360
|
+
*/
|
|
361
|
+
export const allValidSpans = [
|
|
362
|
+
openaiGenerateText,
|
|
363
|
+
openaiStreamText,
|
|
364
|
+
openaiGenerateObject,
|
|
365
|
+
anthropicGenerateText,
|
|
366
|
+
anthropicGenAiFormat,
|
|
367
|
+
geminiGenerateText,
|
|
368
|
+
openaiEmbed,
|
|
369
|
+
openaiEmbedMany
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Tool call spans - these are separate as they may need special handling
|
|
374
|
+
*/
|
|
375
|
+
export const toolCallSpans = [toolCallWeather, toolCallDatabase];
|
|
376
|
+
|
|
377
|
+
export const errorSpans = [errorRateLimit, errorContextLength];
|
|
378
|
+
|
|
379
|
+
export const filteredOutSpans = [internalDoGenerate, httpRequest];
|