openlit 1.6.0 → 1.7.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.
Files changed (50) hide show
  1. package/README.md +19 -1
  2. package/dist/helpers.js +8 -4
  3. package/dist/helpers.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +19 -6
  6. package/dist/index.js.map +1 -1
  7. package/dist/instrumentation/__tests__/anthropic-wrapper.test.d.ts +1 -0
  8. package/dist/instrumentation/__tests__/anthropic-wrapper.test.js +92 -0
  9. package/dist/instrumentation/__tests__/anthropic-wrapper.test.js.map +1 -0
  10. package/dist/instrumentation/__tests__/base-wrapper.test.d.ts +1 -0
  11. package/dist/instrumentation/__tests__/base-wrapper.test.js +175 -0
  12. package/dist/instrumentation/__tests__/base-wrapper.test.js.map +1 -0
  13. package/dist/instrumentation/__tests__/cohere-wrapper.test.d.ts +1 -0
  14. package/dist/instrumentation/__tests__/cohere-wrapper.test.js +131 -0
  15. package/dist/instrumentation/__tests__/cohere-wrapper.test.js.map +1 -0
  16. package/dist/instrumentation/__tests__/openai-wrapper.test.d.ts +1 -0
  17. package/dist/instrumentation/__tests__/openai-wrapper.test.js +181 -0
  18. package/dist/instrumentation/__tests__/openai-wrapper.test.js.map +1 -0
  19. package/dist/instrumentation/anthropic/wrapper.d.ts +7 -1
  20. package/dist/instrumentation/anthropic/wrapper.js +16 -1
  21. package/dist/instrumentation/anthropic/wrapper.js.map +1 -1
  22. package/dist/instrumentation/base-wrapper.d.ts +3 -2
  23. package/dist/instrumentation/base-wrapper.js +81 -1
  24. package/dist/instrumentation/base-wrapper.js.map +1 -1
  25. package/dist/instrumentation/cohere/wrapper.d.ts +7 -1
  26. package/dist/instrumentation/cohere/wrapper.js +19 -2
  27. package/dist/instrumentation/cohere/wrapper.js.map +1 -1
  28. package/dist/instrumentation/ollama/wrapper.d.ts +2 -1
  29. package/dist/instrumentation/ollama/wrapper.js +2 -2
  30. package/dist/instrumentation/ollama/wrapper.js.map +1 -1
  31. package/dist/instrumentation/openai/index.js +11 -0
  32. package/dist/instrumentation/openai/index.js.map +1 -1
  33. package/dist/instrumentation/openai/wrapper.d.ts +39 -3
  34. package/dist/instrumentation/openai/wrapper.js +537 -26
  35. package/dist/instrumentation/openai/wrapper.js.map +1 -1
  36. package/dist/otel/__tests__/metrics.test.d.ts +1 -0
  37. package/dist/otel/__tests__/metrics.test.js +51 -0
  38. package/dist/otel/__tests__/metrics.test.js.map +1 -0
  39. package/dist/otel/metrics.d.ts +22 -0
  40. package/dist/otel/metrics.js +132 -0
  41. package/dist/otel/metrics.js.map +1 -0
  42. package/dist/{tracing.d.ts → otel/tracing.d.ts} +1 -1
  43. package/dist/{tracing.js → otel/tracing.js} +17 -15
  44. package/dist/otel/tracing.js.map +1 -0
  45. package/dist/semantic-convention.d.ts +36 -0
  46. package/dist/semantic-convention.js +53 -11
  47. package/dist/semantic-convention.js.map +1 -1
  48. package/dist/types.d.ts +7 -0
  49. package/package.json +10 -11
  50. package/dist/tracing.js.map +0 -1
@@ -20,7 +20,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
20
20
  })
21
21
  .then((response) => {
22
22
  const { stream = false } = args[0];
23
- if (!!stream) {
23
+ if (stream) {
24
24
  return helpers_1.default.createStreamProxy(response, OpenAIWrapper._chatCompletionGenerator({
25
25
  args,
26
26
  genAIEndpoint,
@@ -38,8 +38,9 @@ class OpenAIWrapper extends base_wrapper_1.default {
38
38
  };
39
39
  }
40
40
  static async _chatCompletion({ args, genAIEndpoint, response, span, }) {
41
+ let metricParams;
41
42
  try {
42
- await OpenAIWrapper._chatCompletionCommonSetter({
43
+ metricParams = await OpenAIWrapper._chatCompletionCommonSetter({
43
44
  args,
44
45
  genAIEndpoint,
45
46
  result: response,
@@ -52,9 +53,16 @@ class OpenAIWrapper extends base_wrapper_1.default {
52
53
  }
53
54
  finally {
54
55
  span.end();
56
+ // Record metrics after span has ended if parameters are available
57
+ if (metricParams) {
58
+ base_wrapper_1.default.recordMetrics(span, metricParams);
59
+ }
55
60
  }
56
61
  }
57
62
  static async *_chatCompletionGenerator({ args, genAIEndpoint, response, span, }) {
63
+ let metricParams;
64
+ const timestamps = [];
65
+ const startTime = Date.now();
58
66
  try {
59
67
  const { messages } = args[0];
60
68
  let { tools } = args[0];
@@ -62,6 +70,8 @@ class OpenAIWrapper extends base_wrapper_1.default {
62
70
  id: '0',
63
71
  created: -1,
64
72
  model: '',
73
+ system_fingerprint: '',
74
+ service_tier: 'auto',
65
75
  choices: [
66
76
  {
67
77
  index: 0,
@@ -74,12 +84,28 @@ class OpenAIWrapper extends base_wrapper_1.default {
74
84
  prompt_tokens: 0,
75
85
  completion_tokens: 0,
76
86
  total_tokens: 0,
87
+ completion_tokens_details: {
88
+ reasoning_tokens: 0,
89
+ audio_tokens: 0,
90
+ },
91
+ prompt_tokens_details: {
92
+ cached_tokens: 0,
93
+ audio_tokens: 0,
94
+ },
77
95
  },
78
96
  };
97
+ let toolCalls = [];
79
98
  for await (const chunk of response) {
99
+ timestamps.push(Date.now());
80
100
  result.id = chunk.id;
81
101
  result.created = chunk.created;
82
102
  result.model = chunk.model;
103
+ if (chunk.system_fingerprint) {
104
+ result.system_fingerprint = chunk.system_fingerprint;
105
+ }
106
+ if (chunk.service_tier) {
107
+ result.service_tier = chunk.service_tier;
108
+ }
83
109
  if (chunk.choices[0]?.finish_reason) {
84
110
  result.choices[0].finish_reason = chunk.choices[0].finish_reason;
85
111
  }
@@ -89,11 +115,45 @@ class OpenAIWrapper extends base_wrapper_1.default {
89
115
  if (chunk.choices[0]?.delta.content) {
90
116
  result.choices[0].message.content += chunk.choices[0].delta.content;
91
117
  }
118
+ // Improved tool calls handling for streaming
92
119
  if (chunk.choices[0]?.delta.tool_calls) {
120
+ const deltaTools = chunk.choices[0].delta.tool_calls;
121
+ for (const tool of deltaTools) {
122
+ const idx = tool.index || 0;
123
+ // Extend array if needed
124
+ while (toolCalls.length <= idx) {
125
+ toolCalls.push({
126
+ id: '',
127
+ type: 'function',
128
+ function: { name: '', arguments: '' }
129
+ });
130
+ }
131
+ if (tool.id) {
132
+ // New tool call
133
+ toolCalls[idx].id = tool.id;
134
+ toolCalls[idx].type = tool.type || 'function';
135
+ if (tool.function?.name) {
136
+ toolCalls[idx].function.name = tool.function.name;
137
+ }
138
+ if (tool.function?.arguments) {
139
+ toolCalls[idx].function.arguments = tool.function.arguments;
140
+ }
141
+ }
142
+ else if (tool.function?.arguments) {
143
+ // Append arguments to existing tool call
144
+ toolCalls[idx].function.arguments += tool.function.arguments;
145
+ }
146
+ }
93
147
  tools = true;
94
148
  }
95
149
  yield chunk;
96
150
  }
151
+ if (toolCalls.length > 0) {
152
+ result.choices[0].message = {
153
+ ...result.choices[0].message,
154
+ tool_calls: toolCalls
155
+ };
156
+ }
97
157
  let promptTokens = 0;
98
158
  for (const message of messages || []) {
99
159
  promptTokens += helpers_1.default.openaiTokens(message.content, result.model) ?? 0;
@@ -104,14 +164,25 @@ class OpenAIWrapper extends base_wrapper_1.default {
104
164
  prompt_tokens: promptTokens,
105
165
  completion_tokens: completionTokens,
106
166
  total_tokens: promptTokens + completionTokens,
167
+ completion_tokens_details: result.usage.completion_tokens_details,
168
+ prompt_tokens_details: result.usage.prompt_tokens_details,
107
169
  };
108
170
  }
109
171
  args[0].tools = tools;
110
- await OpenAIWrapper._chatCompletionCommonSetter({
172
+ // Calculate TTFT and TBT
173
+ const ttft = timestamps.length > 0 ? (timestamps[0] - startTime) / 1000 : 0;
174
+ let tbt = 0;
175
+ if (timestamps.length > 1) {
176
+ const timeDiffs = timestamps.slice(1).map((t, i) => t - timestamps[i]);
177
+ tbt = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length / 1000;
178
+ }
179
+ metricParams = await OpenAIWrapper._chatCompletionCommonSetter({
111
180
  args,
112
181
  genAIEndpoint,
113
182
  result,
114
183
  span,
184
+ ttft,
185
+ tbt,
115
186
  });
116
187
  return result;
117
188
  }
@@ -120,19 +191,29 @@ class OpenAIWrapper extends base_wrapper_1.default {
120
191
  }
121
192
  finally {
122
193
  span.end();
194
+ // Record metrics after span has ended if parameters are available
195
+ if (metricParams) {
196
+ base_wrapper_1.default.recordMetrics(span, metricParams);
197
+ }
123
198
  }
124
199
  }
125
- static async _chatCompletionCommonSetter({ args, genAIEndpoint, result, span, }) {
200
+ static async _chatCompletionCommonSetter({ args, genAIEndpoint, result, span, ttft = 0, tbt = 0, }) {
126
201
  const traceContent = config_1.default.traceContent;
127
- const { messages, frequency_penalty = 0, max_tokens = null, n = 1, presence_penalty = 0, seed = null, temperature = 1, top_p, user, stream = false, tools, } = args[0];
202
+ const { messages, frequency_penalty = 0, max_tokens = null, n = 1, presence_penalty = 0, seed = null, stop = null, temperature = 1, top_p, user, stream = false, tools, } = args[0];
128
203
  // Request Params attributes : Start
129
204
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_P, top_p || 1);
130
- span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, max_tokens);
205
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, max_tokens || -1);
131
206
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE, temperature);
132
207
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty);
133
208
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty);
134
- span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SEED, seed);
209
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SEED, seed ? String(seed) : '');
135
210
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, stream);
211
+ if (stop) {
212
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_STOP_SEQUENCES, Array.isArray(stop) ? stop : [stop]);
213
+ }
214
+ if (user) {
215
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_USER, user);
216
+ }
136
217
  if (traceContent) {
137
218
  // Format 'messages' into a single string
138
219
  const messagePrompt = messages || [];
@@ -164,6 +245,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
164
245
  span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT);
165
246
  span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, result.id);
166
247
  const model = result.model || 'gpt-3.5-turbo';
248
+ const responseModel = result.model || model;
167
249
  const pricingInfo = await config_1.default.updatePricingJson(config_1.default.pricing_json);
168
250
  // Calculate cost of the operation
169
251
  const cost = helpers_1.default.getChatModelCost(model, pricingInfo, result.usage.prompt_tokens, result.usage.completion_tokens);
@@ -174,30 +256,101 @@ class OpenAIWrapper extends base_wrapper_1.default {
174
256
  cost,
175
257
  aiSystem: OpenAIWrapper.aiSystem,
176
258
  });
259
+ // Response model
260
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_MODEL, responseModel);
261
+ // OpenAI-specific attributes
262
+ if (result.system_fingerprint) {
263
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_SYSTEM_FINGERPRINT, result.system_fingerprint);
264
+ }
265
+ if (result.service_tier) {
266
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SERVICE_TIER, result.service_tier);
267
+ }
268
+ // Token usage
177
269
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, result.usage.prompt_tokens);
178
270
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS, result.usage.completion_tokens);
179
271
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_TOTAL_TOKENS, result.usage.total_tokens);
272
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_TOKEN_USAGE, result.usage.total_tokens);
273
+ // Enhanced token details
274
+ if (result.usage.completion_tokens_details) {
275
+ if (result.usage.completion_tokens_details.reasoning_tokens) {
276
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_REASONING_TOKENS, result.usage.completion_tokens_details.reasoning_tokens);
277
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_COMPLETION_TOKENS_DETAILS_REASONING, result.usage.completion_tokens_details.reasoning_tokens);
278
+ }
279
+ if (result.usage.completion_tokens_details.audio_tokens) {
280
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_COMPLETION_TOKENS_DETAILS_AUDIO, result.usage.completion_tokens_details.audio_tokens);
281
+ }
282
+ }
283
+ if (result.usage.prompt_tokens_details) {
284
+ if (result.usage.prompt_tokens_details.cached_tokens) {
285
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_PROMPT_TOKENS_DETAILS_CACHE_READ, result.usage.prompt_tokens_details.cached_tokens);
286
+ }
287
+ if (result.usage.prompt_tokens_details.audio_tokens) {
288
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_PROMPT_TOKENS_DETAILS_CACHE_WRITE, result.usage.prompt_tokens_details.audio_tokens);
289
+ }
290
+ }
291
+ // TTFT and TBT metrics
292
+ if (ttft > 0) {
293
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TTFT, ttft);
294
+ }
295
+ if (tbt > 0) {
296
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TBT, tbt);
297
+ }
298
+ // Finish reason
180
299
  if (result.choices[0].finish_reason) {
181
- span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON, result.choices[0].finish_reason);
300
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON, [result.choices[0].finish_reason]);
182
301
  }
183
- if (tools) {
184
- span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION, 'Function called with tools');
302
+ // Output type
303
+ const outputType = typeof result.choices[0].message.content === 'string'
304
+ ? semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_TEXT
305
+ : semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_JSON;
306
+ span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_TYPE, outputType);
307
+ // Tool calls handling
308
+ if (result.choices[0].message.tool_calls) {
309
+ const toolCalls = result.choices[0].message.tool_calls;
310
+ const toolNames = toolCalls.map((t) => t.function?.name || '').filter(Boolean);
311
+ const toolIds = toolCalls.map((t) => t.id || '').filter(Boolean);
312
+ const toolArgs = toolCalls.map((t) => t.function?.arguments || '').filter(Boolean);
313
+ const toolTypes = toolCalls.map((t) => t.type || '').filter(Boolean);
314
+ if (toolNames.length > 0) {
315
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_NAME, toolNames.join(', '));
316
+ }
317
+ if (toolIds.length > 0) {
318
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ID, toolIds.join(', '));
319
+ }
320
+ if (toolArgs.length > 0) {
321
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ARGUMENTS, toolArgs);
322
+ }
323
+ if (toolTypes.length > 0) {
324
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_TYPE, toolTypes.join(', '));
325
+ }
185
326
  }
186
- else {
187
- if (traceContent) {
188
- if (n === 1) {
189
- span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION, result.choices[0].message.content);
190
- }
191
- else {
192
- let i = 0;
193
- while (i < n) {
194
- const attribute_name = `${semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION}.[i]`;
195
- span.setAttribute(attribute_name, result.choices[i].message.content);
196
- i += 1;
197
- }
327
+ // Content
328
+ if (traceContent) {
329
+ // Format completion content - use actual content or empty string if only tool calls
330
+ const completionContent = result.choices[0].message.content || '';
331
+ if (n === 1) {
332
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION, completionContent);
333
+ }
334
+ else {
335
+ let i = 0;
336
+ while (i < n) {
337
+ const attribute_name = `${semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION}.${i}`;
338
+ span.setAttribute(attribute_name, result.choices[i].message.content || '');
339
+ i += 1;
198
340
  }
199
341
  }
342
+ // Add events for backward compatibility
343
+ span.addEvent(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION_EVENT, {
344
+ [semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION]: completionContent,
345
+ });
200
346
  }
347
+ return {
348
+ genAIEndpoint,
349
+ model,
350
+ user,
351
+ cost,
352
+ aiSystem: OpenAIWrapper.aiSystem,
353
+ };
201
354
  }
202
355
  static _patchEmbedding(tracer) {
203
356
  const genAIEndpoint = 'openai.resources.embeddings';
@@ -206,6 +359,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
206
359
  return async function (...args) {
207
360
  const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
208
361
  return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
362
+ let metricParams;
209
363
  try {
210
364
  const response = await originalMethod.apply(this, args);
211
365
  const model = response.model || 'text-embedding-ada-002';
@@ -213,7 +367,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
213
367
  const cost = helpers_1.default.getEmbedModelCost(model, pricingInfo, response.usage.prompt_tokens);
214
368
  span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_EMBEDDING);
215
369
  const { dimensions, encoding_format = 'float', input, user } = args[0];
216
- // Set base span attribues
370
+ // Set base span attributes
217
371
  OpenAIWrapper.setBaseSpanAttributes(span, {
218
372
  genAIEndpoint,
219
373
  model,
@@ -221,22 +375,47 @@ class OpenAIWrapper extends base_wrapper_1.default {
221
375
  cost,
222
376
  aiSystem: OpenAIWrapper.aiSystem,
223
377
  });
378
+ // Set missing critical attributes to match Python SDK
379
+ span.setAttribute(semantic_convention_1.default.SERVER_ADDRESS, 'api.openai.com');
380
+ span.setAttribute(semantic_convention_1.default.SERVER_PORT, 443);
381
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, false);
382
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TBT, 0);
383
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TTFT, 0);
384
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SDK_VERSION, '1.7.0');
224
385
  // Request Params attributes : Start
225
- span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_ENCODING_FORMATS, encoding_format);
226
- span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_EMBEDDING_DIMENSION, dimensions);
386
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_ENCODING_FORMATS, [encoding_format]);
387
+ if (dimensions) {
388
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_EMBEDDING_DIMENSION, dimensions);
389
+ }
390
+ if (user) {
391
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_USER, user);
392
+ }
227
393
  if (traceContent) {
228
- span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_PROMPT, input);
394
+ const formattedInput = typeof input === 'string' ? input : JSON.stringify(input);
395
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_PROMPT, formattedInput);
229
396
  }
230
397
  // Request Params attributes : End
231
398
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, response.usage.prompt_tokens);
232
399
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_TOTAL_TOKENS, response.usage.total_tokens);
400
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_TOKEN_USAGE, response.usage.prompt_tokens);
401
+ metricParams = {
402
+ genAIEndpoint,
403
+ model,
404
+ user,
405
+ cost,
406
+ aiSystem: OpenAIWrapper.aiSystem,
407
+ };
233
408
  return response;
234
409
  }
235
410
  catch (e) {
236
411
  helpers_1.default.handleException(span, e);
412
+ throw e;
237
413
  }
238
414
  finally {
239
415
  span.end();
416
+ if (metricParams) {
417
+ base_wrapper_1.default.recordMetrics(span, metricParams);
418
+ }
240
419
  }
241
420
  });
242
421
  };
@@ -248,6 +427,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
248
427
  return async function (...args) {
249
428
  const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
250
429
  return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
430
+ let metricParams;
251
431
  try {
252
432
  const response = await originalMethod.apply(this, args);
253
433
  const model = response.model || 'gpt-3.5-turbo';
@@ -271,6 +451,13 @@ class OpenAIWrapper extends base_wrapper_1.default {
271
451
  span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, response.id);
272
452
  span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, response.usage.prompt_tokens);
273
453
  span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_FINETUNE_STATUS, response.status);
454
+ // Store metric parameters for use after span ends
455
+ metricParams = {
456
+ genAIEndpoint,
457
+ model,
458
+ user,
459
+ aiSystem: OpenAIWrapper.aiSystem,
460
+ };
274
461
  return response;
275
462
  }
276
463
  catch (e) {
@@ -278,6 +465,10 @@ class OpenAIWrapper extends base_wrapper_1.default {
278
465
  }
279
466
  finally {
280
467
  span.end();
468
+ // Record metrics after span has ended if parameters are available
469
+ if (metricParams) {
470
+ base_wrapper_1.default.recordMetrics(span, metricParams);
471
+ }
281
472
  }
282
473
  });
283
474
  };
@@ -290,6 +481,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
290
481
  return async function (...args) {
291
482
  const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
292
483
  return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
484
+ let metricParams;
293
485
  try {
294
486
  const response = await originalMethod.apply(this, args);
295
487
  const { prompt, quality = 'standard', response_format = 'url', size = '1024x1024', style = 'vivid', user, } = args[0];
@@ -323,6 +515,14 @@ class OpenAIWrapper extends base_wrapper_1.default {
323
515
  imagesCount++;
324
516
  }
325
517
  }
518
+ // Store metric parameters for use after span ends
519
+ metricParams = {
520
+ genAIEndpoint,
521
+ model,
522
+ user,
523
+ cost,
524
+ aiSystem: OpenAIWrapper.aiSystem,
525
+ };
326
526
  return response;
327
527
  }
328
528
  catch (e) {
@@ -330,6 +530,10 @@ class OpenAIWrapper extends base_wrapper_1.default {
330
530
  }
331
531
  finally {
332
532
  span.end();
533
+ // Record metrics after span has ended if parameters are available
534
+ if (metricParams) {
535
+ base_wrapper_1.default.recordMetrics(span, metricParams);
536
+ }
333
537
  }
334
538
  });
335
539
  };
@@ -342,6 +546,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
342
546
  return async function (...args) {
343
547
  const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
344
548
  return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
549
+ let metricParams;
345
550
  try {
346
551
  const response = await originalMethod.apply(this, args);
347
552
  const { prompt, quality = 'standard', response_format = 'url', size = '1024x1024', style = 'vivid', user, } = args[0];
@@ -376,6 +581,14 @@ class OpenAIWrapper extends base_wrapper_1.default {
376
581
  imagesCount++;
377
582
  }
378
583
  }
584
+ // Store metric parameters for use after span ends
585
+ metricParams = {
586
+ genAIEndpoint,
587
+ model,
588
+ user,
589
+ cost,
590
+ aiSystem: OpenAIWrapper.aiSystem,
591
+ };
379
592
  return response;
380
593
  }
381
594
  catch (e) {
@@ -383,6 +596,10 @@ class OpenAIWrapper extends base_wrapper_1.default {
383
596
  }
384
597
  finally {
385
598
  span.end();
599
+ // Record metrics after span has ended if parameters are available
600
+ if (metricParams) {
601
+ base_wrapper_1.default.recordMetrics(span, metricParams);
602
+ }
386
603
  }
387
604
  });
388
605
  };
@@ -395,6 +612,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
395
612
  return async function (...args) {
396
613
  const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
397
614
  return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
615
+ let metricParams;
398
616
  try {
399
617
  const response = await originalMethod.apply(this, args);
400
618
  const { input, user, voice, response_format = 'mp3', speed = 1 } = args[0];
@@ -418,6 +636,14 @@ class OpenAIWrapper extends base_wrapper_1.default {
418
636
  span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_PROMPT, input);
419
637
  }
420
638
  // Request Params attributes : End
639
+ // Store metric parameters for use after span ends
640
+ metricParams = {
641
+ genAIEndpoint,
642
+ model,
643
+ user,
644
+ cost,
645
+ aiSystem: OpenAIWrapper.aiSystem,
646
+ };
421
647
  return response;
422
648
  }
423
649
  catch (e) {
@@ -425,11 +651,296 @@ class OpenAIWrapper extends base_wrapper_1.default {
425
651
  }
426
652
  finally {
427
653
  span.end();
654
+ // Record metrics after span has ended if parameters are available
655
+ if (metricParams) {
656
+ base_wrapper_1.default.recordMetrics(span, metricParams);
657
+ }
428
658
  }
429
659
  });
430
660
  };
431
661
  };
432
662
  }
663
+ static _patchResponsesCreate(tracer) {
664
+ const genAIEndpoint = 'openai.resources.responses';
665
+ return (originalMethod) => {
666
+ return async function (...args) {
667
+ const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
668
+ return api_1.context
669
+ .with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
670
+ return originalMethod.apply(this, args);
671
+ })
672
+ .then((response) => {
673
+ const { stream = false } = args[0];
674
+ if (stream) {
675
+ return helpers_1.default.createStreamProxy(response, OpenAIWrapper._responsesGenerator({
676
+ args,
677
+ genAIEndpoint,
678
+ response,
679
+ span,
680
+ }));
681
+ }
682
+ return OpenAIWrapper._responsesComplete({ args, genAIEndpoint, response, span });
683
+ })
684
+ .catch((e) => {
685
+ helpers_1.default.handleException(span, e);
686
+ span.end();
687
+ });
688
+ };
689
+ };
690
+ }
691
+ static async _responsesComplete({ args, genAIEndpoint, response, span, }) {
692
+ let metricParams;
693
+ try {
694
+ metricParams = await OpenAIWrapper._responsesCommonSetter({
695
+ args,
696
+ genAIEndpoint,
697
+ result: response,
698
+ span,
699
+ });
700
+ return response;
701
+ }
702
+ catch (e) {
703
+ helpers_1.default.handleException(span, e);
704
+ }
705
+ finally {
706
+ span.end();
707
+ if (metricParams) {
708
+ base_wrapper_1.default.recordMetrics(span, metricParams);
709
+ }
710
+ }
711
+ }
712
+ static async *_responsesGenerator({ args, genAIEndpoint, response, span, }) {
713
+ let metricParams;
714
+ const timestamps = [];
715
+ const startTime = Date.now();
716
+ try {
717
+ const { input } = args[0];
718
+ const result = {
719
+ id: '',
720
+ model: '',
721
+ service_tier: 'default',
722
+ status: 'completed',
723
+ output: [],
724
+ usage: {
725
+ input_tokens: 0,
726
+ output_tokens: 0,
727
+ output_tokens_details: {
728
+ reasoning_tokens: 0,
729
+ },
730
+ },
731
+ };
732
+ let llmResponse = '';
733
+ let responseTools = [];
734
+ for await (const chunk of response) {
735
+ timestamps.push(Date.now());
736
+ if (chunk.type === 'response.output_text.delta') {
737
+ llmResponse += chunk.delta || '';
738
+ }
739
+ else if (chunk.type === 'response.output_item.added') {
740
+ const item = chunk.item;
741
+ if (item?.type === 'function_call') {
742
+ responseTools.push({
743
+ id: item.id,
744
+ call_id: item.call_id,
745
+ name: item.name,
746
+ type: item.type,
747
+ arguments: item.arguments || '',
748
+ status: item.status,
749
+ });
750
+ }
751
+ }
752
+ else if (chunk.type === 'response.function_call_arguments.delta') {
753
+ const itemId = chunk.item_id;
754
+ const delta = chunk.delta || '';
755
+ const tool = responseTools.find(t => t.id === itemId);
756
+ if (tool) {
757
+ tool.arguments += delta;
758
+ }
759
+ }
760
+ else if (chunk.type === 'response.completed') {
761
+ const responseData = chunk.response;
762
+ result.id = responseData.id;
763
+ result.model = responseData.model;
764
+ result.status = responseData.status;
765
+ const usage = responseData.usage || {};
766
+ result.usage.input_tokens = usage.input_tokens || 0;
767
+ result.usage.output_tokens = usage.output_tokens || 0;
768
+ result.usage.output_tokens_details.reasoning_tokens =
769
+ usage.output_tokens_details?.reasoning_tokens || 0;
770
+ }
771
+ yield chunk;
772
+ }
773
+ // Construct output array
774
+ if (llmResponse) {
775
+ result.output.push({
776
+ type: 'message',
777
+ content: [{ type: 'text', text: llmResponse }],
778
+ });
779
+ }
780
+ if (responseTools.length > 0) {
781
+ result.output.push(...responseTools);
782
+ }
783
+ // Calculate TTFT and TBT
784
+ const ttft = timestamps.length > 0 ? (timestamps[0] - startTime) / 1000 : 0;
785
+ let tbt = 0;
786
+ if (timestamps.length > 1) {
787
+ const timeDiffs = timestamps.slice(1).map((t, i) => t - timestamps[i]);
788
+ tbt = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length / 1000;
789
+ }
790
+ metricParams = await OpenAIWrapper._responsesCommonSetter({
791
+ args,
792
+ genAIEndpoint,
793
+ result,
794
+ span,
795
+ ttft,
796
+ tbt,
797
+ });
798
+ return result;
799
+ }
800
+ catch (e) {
801
+ helpers_1.default.handleException(span, e);
802
+ }
803
+ finally {
804
+ span.end();
805
+ if (metricParams) {
806
+ base_wrapper_1.default.recordMetrics(span, metricParams);
807
+ }
808
+ }
809
+ }
810
+ static async _responsesCommonSetter({ args, genAIEndpoint, result, span, ttft = 0, tbt = 0, }) {
811
+ const traceContent = config_1.default.traceContent;
812
+ const { input, temperature = 1.0, top_p = 1.0, max_output_tokens, reasoning, stream = false, } = args[0];
813
+ // Format input for prompt
814
+ let prompt = '';
815
+ if (typeof input === 'string') {
816
+ prompt = input;
817
+ }
818
+ else if (Array.isArray(input)) {
819
+ const formattedMessages = [];
820
+ for (const item of input) {
821
+ const role = item.role || 'user';
822
+ const content = item.content;
823
+ if (typeof content === 'string') {
824
+ formattedMessages.push(`${role}: ${content}`);
825
+ }
826
+ else if (Array.isArray(content)) {
827
+ const contentParts = content
828
+ .map((part) => {
829
+ if (part.type === 'input_text') {
830
+ return `text: ${part.text || ''}`;
831
+ }
832
+ else if (part.type === 'input_image' && part.image_url && !part.image_url.startsWith('data:')) {
833
+ return `image_url: ${part.image_url}`;
834
+ }
835
+ return '';
836
+ })
837
+ .filter(Boolean)
838
+ .join(', ');
839
+ formattedMessages.push(`${role}: ${contentParts}`);
840
+ }
841
+ }
842
+ prompt = formattedMessages.join('\n');
843
+ }
844
+ // Request Params attributes
845
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE, temperature);
846
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_P, top_p);
847
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_MAX_TOKENS, max_output_tokens || -1);
848
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_IS_STREAM, stream);
849
+ if (reasoning?.effort) {
850
+ span.setAttribute('gen_ai.request.reasoning_effort', reasoning.effort);
851
+ }
852
+ if (traceContent) {
853
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_PROMPT, prompt);
854
+ }
855
+ span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT);
856
+ const model = result.model || 'gpt-4o';
857
+ const responseModel = result.model || model;
858
+ const pricingInfo = await config_1.default.updatePricingJson(config_1.default.pricing_json);
859
+ // Calculate cost
860
+ const inputTokens = result.usage?.input_tokens || 0;
861
+ const outputTokens = result.usage?.output_tokens || 0;
862
+ const cost = helpers_1.default.getChatModelCost(model, pricingInfo, inputTokens, outputTokens);
863
+ OpenAIWrapper.setBaseSpanAttributes(span, {
864
+ genAIEndpoint,
865
+ model,
866
+ user: '',
867
+ cost,
868
+ aiSystem: OpenAIWrapper.aiSystem,
869
+ });
870
+ // Response attributes
871
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, result.id);
872
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_MODEL, responseModel);
873
+ span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_FINISH_REASON, [result.status || 'completed']);
874
+ span.setAttribute(semantic_convention_1.default.GEN_AI_OUTPUT_TYPE, semantic_convention_1.default.GEN_AI_OUTPUT_TYPE_TEXT);
875
+ if (result.service_tier) {
876
+ span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_SERVICE_TIER, result.service_tier);
877
+ }
878
+ // Token usage
879
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, inputTokens);
880
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS, outputTokens);
881
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_TOTAL_TOKENS, inputTokens + outputTokens);
882
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CLIENT_TOKEN_USAGE, inputTokens + outputTokens);
883
+ // Reasoning tokens
884
+ if (result.usage?.output_tokens_details?.reasoning_tokens) {
885
+ span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_REASONING_TOKENS, result.usage.output_tokens_details.reasoning_tokens);
886
+ }
887
+ // TTFT and TBT metrics
888
+ if (ttft > 0) {
889
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TTFT, ttft);
890
+ }
891
+ if (tbt > 0) {
892
+ span.setAttribute(semantic_convention_1.default.GEN_AI_SERVER_TBT, tbt);
893
+ }
894
+ // Extract completion text from output
895
+ let completionText = '';
896
+ if (result.output && Array.isArray(result.output)) {
897
+ for (const item of result.output) {
898
+ if (item.type === 'message' && item.content) {
899
+ for (const content of item.content) {
900
+ if (content.type === 'text' || content.type === 'output_text') {
901
+ completionText += content.text || '';
902
+ }
903
+ }
904
+ }
905
+ }
906
+ }
907
+ // Tool calls handling for Responses API
908
+ const toolCalls = result.tools || [];
909
+ if (toolCalls.length > 0) {
910
+ const toolNames = toolCalls.map((t) => t.name || '').filter(Boolean);
911
+ const toolIds = toolCalls.map((t) => t.call_id || '').filter(Boolean);
912
+ const toolArgs = toolCalls.map((t) => t.arguments || '').filter(Boolean);
913
+ const toolTypes = toolCalls.map((t) => t.type || '').filter(Boolean);
914
+ if (toolNames.length > 0) {
915
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_NAME, toolNames.join(', '));
916
+ }
917
+ if (toolIds.length > 0) {
918
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ID, toolIds.join(', '));
919
+ }
920
+ if (toolArgs.length > 0) {
921
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_CALL_ARGUMENTS, toolArgs.join(', '));
922
+ }
923
+ if (toolTypes.length > 0) {
924
+ span.setAttribute(semantic_convention_1.default.GEN_AI_TOOL_TYPE, toolTypes.join(', '));
925
+ }
926
+ }
927
+ // Content
928
+ if (traceContent) {
929
+ // Set completion content - use actual text or empty string if only tool calls
930
+ span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION, completionText);
931
+ // Add events for backward compatibility
932
+ span.addEvent(semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION_EVENT, {
933
+ [semantic_convention_1.default.GEN_AI_CONTENT_COMPLETION]: completionText,
934
+ });
935
+ }
936
+ return {
937
+ genAIEndpoint,
938
+ model,
939
+ user: '',
940
+ cost,
941
+ aiSystem: OpenAIWrapper.aiSystem,
942
+ };
943
+ }
433
944
  }
434
945
  OpenAIWrapper.aiSystem = semantic_convention_1.default.GEN_AI_SYSTEM_OPENAI;
435
946
  exports.default = OpenAIWrapper;