@overmind-lab/trace-sdk 0.0.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.
@@ -0,0 +1,546 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { type Attributes, type Span, SpanKind, type trace } from "@opentelemetry/api";
3
+ import {
4
+ ATTR_GEN_AI_COMPLETION,
5
+ ATTR_GEN_AI_PROMPT,
6
+ ATTR_GEN_AI_REQUEST_MODEL,
7
+ ATTR_GEN_AI_SYSTEM,
8
+ ATTR_GEN_AI_USAGE_COMPLETION_TOKENS,
9
+ ATTR_GEN_AI_USAGE_PROMPT_TOKENS,
10
+ } from "@opentelemetry/semantic-conventions/incubating";
11
+ import { SpanAttributes } from "@traceloop/ai-semantic-conventions";
12
+ import type {
13
+ ImageCreateVariationParams,
14
+ ImageEditParams,
15
+ ImageGenerateParams,
16
+ ImagesResponse,
17
+ } from "openai/resources/images";
18
+ import type { ImageUploadCallback } from "./types";
19
+
20
+ /**
21
+ * Calculate completion tokens for image generation based on OpenAI's actual token costs
22
+ *
23
+ * Token costs based on OpenAI documentation:
24
+ * For gpt-image-1: Square (1024×1024) Portrait (1024×1536) Landscape (1536×1024)
25
+ * Low 272 tokens 408 tokens 400 tokens
26
+ * Medium 1056 tokens 1584 tokens 1568 tokens
27
+ * High 4160 tokens 6240 tokens 6208 tokens
28
+ *
29
+ * For DALL-E 3:
30
+ * Standard 1056 tokens 1584 tokens 1568 tokens
31
+ * HD 4160 tokens 6240 tokens 6208 tokens
32
+ */
33
+ function calculateImageGenerationTokens(params: any, imageCount: number): number {
34
+ const size = params?.size || "1024x1024";
35
+ const model = params?.model || "dall-e-2";
36
+ const quality = params?.quality || "standard";
37
+
38
+ // Token costs for different models and sizes
39
+ let tokensPerImage: number;
40
+
41
+ if (model === "dall-e-2") {
42
+ // DALL-E 2 has fixed costs regardless of quality
43
+ const dalle2Costs: Record<string, number> = {
44
+ "256x256": 68,
45
+ "512x512": 272,
46
+ "1024x1024": 1056,
47
+ };
48
+ tokensPerImage = dalle2Costs[size] || 1056;
49
+ } else if (model === "dall-e-3") {
50
+ // DALL-E 3 costs depend on quality and size
51
+ const dalle3Costs: Record<string, Record<string, number>> = {
52
+ standard: {
53
+ "1024x1024": 1056,
54
+ "1024x1792": 1584,
55
+ "1792x1024": 1568,
56
+ },
57
+ hd: {
58
+ "1024x1024": 4160,
59
+ "1024x1792": 6240,
60
+ "1792x1024": 6208,
61
+ },
62
+ };
63
+ tokensPerImage = dalle3Costs[quality]?.[size] || dalle3Costs["standard"]["1024x1024"];
64
+ } else {
65
+ // Default fallback for unknown models
66
+ tokensPerImage = 1056;
67
+ }
68
+
69
+ return tokensPerImage * imageCount;
70
+ }
71
+
72
+ async function processImageInRequest(
73
+ image: any,
74
+ traceId: string,
75
+ spanId: string,
76
+ uploadCallback: ImageUploadCallback,
77
+ index = 0
78
+ ): Promise<string | null> {
79
+ try {
80
+ let base64Data: string;
81
+ let filename: string;
82
+
83
+ if (typeof image === "string") {
84
+ // Could be a file path, base64 string, or URL
85
+ if (image.startsWith("data:image/")) {
86
+ const commaIndex = image.indexOf(",");
87
+ base64Data = image.substring(commaIndex + 1);
88
+ filename = `input_image_${index}.png`;
89
+ } else if (image.startsWith("http")) {
90
+ return null;
91
+ } else {
92
+ base64Data = image;
93
+ filename = `input_image_${index}.png`;
94
+ }
95
+ } else if (image && typeof image === "object") {
96
+ // Handle Node.js Buffer objects and ReadStream
97
+ if (Buffer.isBuffer(image)) {
98
+ base64Data = image.toString("base64");
99
+ filename = `input_image_${index}.png`;
100
+ } else if (image.read && typeof image.read === "function") {
101
+ const chunks: Buffer[] = [];
102
+ return new Promise((resolve) => {
103
+ image.on("data", (chunk: Buffer) => chunks.push(chunk));
104
+ image.on("end", async () => {
105
+ try {
106
+ const buffer = Buffer.concat(chunks);
107
+ const base64Data = buffer.toString("base64");
108
+ const filename = image.path || `input_image_${index}.png`;
109
+ const url = await uploadCallback(traceId, spanId, filename, base64Data);
110
+ resolve(url);
111
+ } catch (error) {
112
+ console.error("Error processing stream image:", error);
113
+ resolve(null);
114
+ }
115
+ });
116
+ image.on("error", (error: Error) => {
117
+ console.error("Error reading image stream:", error);
118
+ resolve(null);
119
+ });
120
+ });
121
+ } else {
122
+ return null;
123
+ }
124
+ } else {
125
+ return null;
126
+ }
127
+
128
+ const url = await uploadCallback(traceId, spanId, filename, base64Data);
129
+ return url;
130
+ } catch (error) {
131
+ console.error("Error processing image in request:", error);
132
+ return null;
133
+ }
134
+ }
135
+
136
+ export function setImageGenerationRequestAttributes(span: Span, params: ImageGenerateParams): void {
137
+ const attributes: Attributes = {};
138
+
139
+ if (params.model) {
140
+ attributes[ATTR_GEN_AI_REQUEST_MODEL] = params.model;
141
+ }
142
+
143
+ if (params.size) {
144
+ attributes["gen_ai.request.image.size"] = params.size;
145
+ }
146
+
147
+ if (params.quality) {
148
+ attributes["gen_ai.request.image.quality"] = params.quality;
149
+ }
150
+
151
+ if (params.style) {
152
+ attributes["gen_ai.request.image.style"] = params.style;
153
+ }
154
+
155
+ if (params.n) {
156
+ attributes["gen_ai.request.image.count"] = params.n;
157
+ }
158
+
159
+ if (params.prompt) {
160
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt;
161
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
162
+ }
163
+
164
+ Object.entries(attributes).forEach(([key, value]) => {
165
+ if (value !== undefined) {
166
+ span.setAttribute(key, value);
167
+ }
168
+ });
169
+ }
170
+
171
+ export async function setImageEditRequestAttributes(
172
+ span: Span,
173
+ params: ImageEditParams,
174
+ uploadCallback?: ImageUploadCallback
175
+ ): Promise<void> {
176
+ const attributes: Attributes = {};
177
+
178
+ if (params.model) {
179
+ attributes[ATTR_GEN_AI_REQUEST_MODEL] = params.model;
180
+ }
181
+
182
+ if (params.size) {
183
+ attributes["gen_ai.request.image.size"] = params.size;
184
+ }
185
+
186
+ if (params.n) {
187
+ attributes["gen_ai.request.image.count"] = params.n;
188
+ }
189
+
190
+ if (params.prompt) {
191
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt;
192
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
193
+ }
194
+
195
+ // Process input image if upload callback is available
196
+ if (params.image && uploadCallback && span.spanContext().traceId && span.spanContext().spanId) {
197
+ const traceId = span.spanContext().traceId;
198
+ const spanId = span.spanContext().spanId;
199
+
200
+ const imageUrl = await processImageInRequest(params.image, traceId, spanId, uploadCallback, 0);
201
+
202
+ if (imageUrl) {
203
+ attributes[`${ATTR_GEN_AI_PROMPT}.1.content`] = JSON.stringify([
204
+ { type: "image_url", image_url: { url: imageUrl } },
205
+ ]);
206
+ attributes[`${ATTR_GEN_AI_PROMPT}.1.role`] = "user";
207
+ }
208
+ }
209
+
210
+ Object.entries(attributes).forEach(([key, value]) => {
211
+ if (value !== undefined) {
212
+ span.setAttribute(key, value);
213
+ }
214
+ });
215
+ }
216
+
217
+ export async function setImageVariationRequestAttributes(
218
+ span: Span,
219
+ params: ImageCreateVariationParams,
220
+ uploadCallback?: ImageUploadCallback
221
+ ): Promise<void> {
222
+ const attributes: Attributes = {};
223
+
224
+ if (params.model) {
225
+ attributes[ATTR_GEN_AI_REQUEST_MODEL] = params.model;
226
+ }
227
+
228
+ if (params.size) {
229
+ attributes["gen_ai.request.image.size"] = params.size;
230
+ }
231
+
232
+ if (params.n) {
233
+ attributes["gen_ai.request.image.count"] = params.n;
234
+ }
235
+
236
+ // Process input image if upload callback is available
237
+ if (params.image && uploadCallback && span.spanContext().traceId && span.spanContext().spanId) {
238
+ const traceId = span.spanContext().traceId;
239
+ const spanId = span.spanContext().spanId;
240
+
241
+ const imageUrl = await processImageInRequest(params.image, traceId, spanId, uploadCallback, 0);
242
+
243
+ if (imageUrl) {
244
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify([
245
+ { type: "image_url", image_url: { url: imageUrl } },
246
+ ]);
247
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
248
+ }
249
+ }
250
+
251
+ Object.entries(attributes).forEach(([key, value]) => {
252
+ if (value !== undefined) {
253
+ span.setAttribute(key, value);
254
+ }
255
+ });
256
+ }
257
+
258
+ export async function setImageGenerationResponseAttributes(
259
+ span: Span,
260
+ response: ImagesResponse,
261
+ uploadCallback?: ImageUploadCallback,
262
+ instrumentationConfig?: { enrichTokens?: boolean },
263
+ params?: any
264
+ ): Promise<void> {
265
+ const attributes: Attributes = {};
266
+
267
+ if (response.data && response.data.length > 0) {
268
+ const completionTokens = calculateImageGenerationTokens(params, response.data.length);
269
+ attributes[ATTR_GEN_AI_USAGE_COMPLETION_TOKENS] = completionTokens;
270
+
271
+ // Calculate prompt tokens if enrichTokens is enabled
272
+ if (instrumentationConfig?.enrichTokens) {
273
+ try {
274
+ let estimatedPromptTokens = 0;
275
+
276
+ if (params?.prompt) {
277
+ estimatedPromptTokens += Math.ceil(params.prompt.length / 4);
278
+ }
279
+
280
+ if (params?.image) {
281
+ estimatedPromptTokens += 272;
282
+ }
283
+
284
+ if (estimatedPromptTokens > 0) {
285
+ attributes[ATTR_GEN_AI_USAGE_PROMPT_TOKENS] = estimatedPromptTokens;
286
+ }
287
+
288
+ attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] =
289
+ estimatedPromptTokens + completionTokens;
290
+ } catch {
291
+ attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = completionTokens;
292
+ }
293
+ } else {
294
+ attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = completionTokens;
295
+ }
296
+ }
297
+
298
+ if (response.data && response.data.length > 0) {
299
+ const firstImage = response.data[0];
300
+
301
+ if (firstImage.b64_json && uploadCallback) {
302
+ try {
303
+ const traceId = span.spanContext().traceId;
304
+ const spanId = span.spanContext().spanId;
305
+
306
+ const imageUrl = await uploadCallback(
307
+ traceId,
308
+ spanId,
309
+ "generated_image.png",
310
+ firstImage.b64_json
311
+ );
312
+
313
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
314
+ { type: "image_url", image_url: { url: imageUrl } },
315
+ ]);
316
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
317
+ } catch (error) {
318
+ console.error("Failed to upload generated image:", error);
319
+ }
320
+ } else if (firstImage.url && uploadCallback) {
321
+ try {
322
+ const traceId = span.spanContext().traceId;
323
+ const spanId = span.spanContext().spanId;
324
+
325
+ const response = await fetch(firstImage.url);
326
+ const arrayBuffer = await response.arrayBuffer();
327
+ const buffer = Buffer.from(arrayBuffer);
328
+ const base64Data = buffer.toString("base64");
329
+
330
+ const uploadedUrl = await uploadCallback(
331
+ traceId,
332
+ spanId,
333
+ "generated_image.png",
334
+ base64Data
335
+ );
336
+
337
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
338
+ { type: "image_url", image_url: { url: uploadedUrl } },
339
+ ]);
340
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
341
+ } catch (error) {
342
+ console.error("Failed to fetch and upload generated image:", error);
343
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
344
+ { type: "image_url", image_url: { url: firstImage.url } },
345
+ ]);
346
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
347
+ }
348
+ } else if (firstImage.url) {
349
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
350
+ { type: "image_url", image_url: { url: firstImage.url } },
351
+ ]);
352
+ attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
353
+ }
354
+
355
+ if (firstImage.revised_prompt) {
356
+ attributes["gen_ai.response.revised_prompt"] = firstImage.revised_prompt;
357
+ }
358
+ }
359
+
360
+ Object.entries(attributes).forEach(([key, value]) => {
361
+ if (value !== undefined) {
362
+ span.setAttribute(key, value);
363
+ }
364
+ });
365
+ }
366
+
367
+ export function wrapImageGeneration(
368
+ tracer: ReturnType<typeof trace.getTracer>,
369
+ uploadCallback?: ImageUploadCallback,
370
+ instrumentationConfig?: { enrichTokens?: boolean }
371
+ ) {
372
+ return (original: (...args: any[]) => any) =>
373
+ function (this: any, ...args: any[]) {
374
+ const params = args[0] as ImageGenerateParams;
375
+
376
+ const span = tracer.startSpan("openai.images.generate", {
377
+ kind: SpanKind.CLIENT,
378
+ attributes: {
379
+ [ATTR_GEN_AI_SYSTEM]: "OpenAI",
380
+ "gen_ai.request.type": "image_generation",
381
+ },
382
+ });
383
+
384
+ const response = original.apply(this, args);
385
+
386
+ if (response && typeof response.then === "function") {
387
+ return response
388
+ .then(async (result: any) => {
389
+ try {
390
+ setImageGenerationRequestAttributes(span, params);
391
+ await setImageGenerationResponseAttributes(
392
+ span,
393
+ result,
394
+ uploadCallback,
395
+ instrumentationConfig,
396
+ params
397
+ );
398
+ return result;
399
+ } catch (error) {
400
+ span.recordException(error as Error);
401
+ throw error;
402
+ } finally {
403
+ span.end();
404
+ }
405
+ })
406
+ .catch((error: Error) => {
407
+ span.recordException(error);
408
+ span.end();
409
+ throw error;
410
+ });
411
+ } else {
412
+ try {
413
+ setImageGenerationRequestAttributes(span, params);
414
+ return response;
415
+ } catch (error) {
416
+ span.recordException(error as Error);
417
+ throw error;
418
+ } finally {
419
+ span.end();
420
+ }
421
+ }
422
+ };
423
+ }
424
+
425
+ export function wrapImageEdit(
426
+ tracer: ReturnType<typeof trace.getTracer>,
427
+ uploadCallback?: ImageUploadCallback,
428
+ instrumentationConfig?: { enrichTokens?: boolean }
429
+ ) {
430
+ return (original: (...args: any[]) => any) =>
431
+ function (this: any, ...args: any[]) {
432
+ const params = args[0] as ImageEditParams;
433
+
434
+ const span = tracer.startSpan("openai.images.edit", {
435
+ kind: SpanKind.CLIENT,
436
+ attributes: {
437
+ [ATTR_GEN_AI_SYSTEM]: "OpenAI",
438
+ "gen_ai.request.type": "image_edit",
439
+ },
440
+ });
441
+
442
+ const setRequestAttributesPromise = setImageEditRequestAttributes(
443
+ span,
444
+ params,
445
+ uploadCallback
446
+ ).catch((error) => {
447
+ console.error("Error setting image edit request attributes:", error);
448
+ });
449
+
450
+ const response = original.apply(this, args);
451
+
452
+ if (response && typeof response.then === "function") {
453
+ return response
454
+ .then(async (result: any) => {
455
+ try {
456
+ await setRequestAttributesPromise;
457
+ await setImageGenerationResponseAttributes(
458
+ span,
459
+ result,
460
+ uploadCallback,
461
+ instrumentationConfig,
462
+ params
463
+ );
464
+ return result;
465
+ } catch (error) {
466
+ span.recordException(error as Error);
467
+ throw error;
468
+ } finally {
469
+ span.end();
470
+ }
471
+ })
472
+ .catch(async (error: Error) => {
473
+ await setRequestAttributesPromise;
474
+ span.recordException(error);
475
+ span.end();
476
+ throw error;
477
+ });
478
+ } else {
479
+ try {
480
+ return response;
481
+ } catch (error) {
482
+ span.recordException(error as Error);
483
+ throw error;
484
+ } finally {
485
+ span.end();
486
+ }
487
+ }
488
+ };
489
+ }
490
+
491
+ export function wrapImageVariation(
492
+ tracer: ReturnType<typeof trace.getTracer>,
493
+ uploadCallback?: ImageUploadCallback,
494
+ instrumentationConfig?: { enrichTokens?: boolean }
495
+ ) {
496
+ return (original: (...args: any[]) => any) =>
497
+ function (this: any, ...args: any[]) {
498
+ const params = args[0] as ImageCreateVariationParams;
499
+
500
+ const span = tracer.startSpan("openai.images.createVariation", {
501
+ kind: SpanKind.CLIENT,
502
+ attributes: {
503
+ [ATTR_GEN_AI_SYSTEM]: "OpenAI",
504
+ "gen_ai.request.type": "image_variation",
505
+ },
506
+ });
507
+
508
+ const response = original.apply(this, args);
509
+
510
+ if (response && typeof response.then === "function") {
511
+ return response
512
+ .then(async (result: any) => {
513
+ try {
514
+ await setImageVariationRequestAttributes(span, params, uploadCallback);
515
+ await setImageGenerationResponseAttributes(
516
+ span,
517
+ result,
518
+ uploadCallback,
519
+ instrumentationConfig,
520
+ params
521
+ );
522
+ return result;
523
+ } catch (error) {
524
+ span.recordException(error as Error);
525
+ throw error;
526
+ } finally {
527
+ span.end();
528
+ }
529
+ })
530
+ .catch((error: Error) => {
531
+ span.recordException(error);
532
+ span.end();
533
+ throw error;
534
+ });
535
+ } else {
536
+ try {
537
+ return response;
538
+ } catch (error) {
539
+ span.recordException(error as Error);
540
+ throw error;
541
+ } finally {
542
+ span.end();
543
+ }
544
+ }
545
+ };
546
+ }