openlit 1.7.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.
- package/README.md +1 -1
- package/dist/helpers.js +8 -4
- package/dist/helpers.js.map +1 -1
- package/dist/instrumentation/__tests__/openai-wrapper.test.js +81 -18
- package/dist/instrumentation/__tests__/openai-wrapper.test.js.map +1 -1
- package/dist/instrumentation/openai/index.js +11 -0
- package/dist/instrumentation/openai/index.js.map +1 -1
- package/dist/instrumentation/openai/wrapper.d.ts +30 -1
- package/dist/instrumentation/openai/wrapper.js +459 -31
- package/dist/instrumentation/openai/wrapper.js.map +1 -1
- package/dist/semantic-convention.d.ts +30 -0
- package/dist/semantic-convention.js +47 -12
- package/dist/semantic-convention.js.map +1 -1
- package/package.json +1 -1
|
@@ -61,6 +61,8 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
61
61
|
}
|
|
62
62
|
static async *_chatCompletionGenerator({ args, genAIEndpoint, response, span, }) {
|
|
63
63
|
let metricParams;
|
|
64
|
+
const timestamps = [];
|
|
65
|
+
const startTime = Date.now();
|
|
64
66
|
try {
|
|
65
67
|
const { messages } = args[0];
|
|
66
68
|
let { tools } = args[0];
|
|
@@ -68,6 +70,8 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
68
70
|
id: '0',
|
|
69
71
|
created: -1,
|
|
70
72
|
model: '',
|
|
73
|
+
system_fingerprint: '',
|
|
74
|
+
service_tier: 'auto',
|
|
71
75
|
choices: [
|
|
72
76
|
{
|
|
73
77
|
index: 0,
|
|
@@ -80,12 +84,28 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
80
84
|
prompt_tokens: 0,
|
|
81
85
|
completion_tokens: 0,
|
|
82
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
|
+
},
|
|
83
95
|
},
|
|
84
96
|
};
|
|
97
|
+
let toolCalls = [];
|
|
85
98
|
for await (const chunk of response) {
|
|
99
|
+
timestamps.push(Date.now());
|
|
86
100
|
result.id = chunk.id;
|
|
87
101
|
result.created = chunk.created;
|
|
88
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
|
+
}
|
|
89
109
|
if (chunk.choices[0]?.finish_reason) {
|
|
90
110
|
result.choices[0].finish_reason = chunk.choices[0].finish_reason;
|
|
91
111
|
}
|
|
@@ -95,11 +115,45 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
95
115
|
if (chunk.choices[0]?.delta.content) {
|
|
96
116
|
result.choices[0].message.content += chunk.choices[0].delta.content;
|
|
97
117
|
}
|
|
118
|
+
// Improved tool calls handling for streaming
|
|
98
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
|
+
}
|
|
99
147
|
tools = true;
|
|
100
148
|
}
|
|
101
149
|
yield chunk;
|
|
102
150
|
}
|
|
151
|
+
if (toolCalls.length > 0) {
|
|
152
|
+
result.choices[0].message = {
|
|
153
|
+
...result.choices[0].message,
|
|
154
|
+
tool_calls: toolCalls
|
|
155
|
+
};
|
|
156
|
+
}
|
|
103
157
|
let promptTokens = 0;
|
|
104
158
|
for (const message of messages || []) {
|
|
105
159
|
promptTokens += helpers_1.default.openaiTokens(message.content, result.model) ?? 0;
|
|
@@ -110,14 +164,25 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
110
164
|
prompt_tokens: promptTokens,
|
|
111
165
|
completion_tokens: completionTokens,
|
|
112
166
|
total_tokens: promptTokens + completionTokens,
|
|
167
|
+
completion_tokens_details: result.usage.completion_tokens_details,
|
|
168
|
+
prompt_tokens_details: result.usage.prompt_tokens_details,
|
|
113
169
|
};
|
|
114
170
|
}
|
|
115
171
|
args[0].tools = tools;
|
|
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
|
+
}
|
|
116
179
|
metricParams = await OpenAIWrapper._chatCompletionCommonSetter({
|
|
117
180
|
args,
|
|
118
181
|
genAIEndpoint,
|
|
119
182
|
result,
|
|
120
183
|
span,
|
|
184
|
+
ttft,
|
|
185
|
+
tbt,
|
|
121
186
|
});
|
|
122
187
|
return result;
|
|
123
188
|
}
|
|
@@ -132,17 +197,23 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
132
197
|
}
|
|
133
198
|
}
|
|
134
199
|
}
|
|
135
|
-
static async _chatCompletionCommonSetter({ args, genAIEndpoint, result, span, }) {
|
|
200
|
+
static async _chatCompletionCommonSetter({ args, genAIEndpoint, result, span, ttft = 0, tbt = 0, }) {
|
|
136
201
|
const traceContent = config_1.default.traceContent;
|
|
137
|
-
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];
|
|
138
203
|
// Request Params attributes : Start
|
|
139
204
|
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TOP_P, top_p || 1);
|
|
140
|
-
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);
|
|
141
206
|
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_TEMPERATURE, temperature);
|
|
142
207
|
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty);
|
|
143
208
|
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty);
|
|
144
|
-
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) : '');
|
|
145
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
|
+
}
|
|
146
217
|
if (traceContent) {
|
|
147
218
|
// Format 'messages' into a single string
|
|
148
219
|
const messagePrompt = messages || [];
|
|
@@ -174,6 +245,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
174
245
|
span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_CHAT);
|
|
175
246
|
span.setAttribute(semantic_convention_1.default.GEN_AI_RESPONSE_ID, result.id);
|
|
176
247
|
const model = result.model || 'gpt-3.5-turbo';
|
|
248
|
+
const responseModel = result.model || model;
|
|
177
249
|
const pricingInfo = await config_1.default.updatePricingJson(config_1.default.pricing_json);
|
|
178
250
|
// Calculate cost of the operation
|
|
179
251
|
const cost = helpers_1.default.getChatModelCost(model, pricingInfo, result.usage.prompt_tokens, result.usage.completion_tokens);
|
|
@@ -184,29 +256,93 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
184
256
|
cost,
|
|
185
257
|
aiSystem: OpenAIWrapper.aiSystem,
|
|
186
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
|
|
187
269
|
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, result.usage.prompt_tokens);
|
|
188
270
|
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_OUTPUT_TOKENS, result.usage.completion_tokens);
|
|
189
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
|
|
190
299
|
if (result.choices[0].finish_reason) {
|
|
191
|
-
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]);
|
|
192
301
|
}
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
326
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
340
|
}
|
|
209
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
|
+
});
|
|
210
346
|
}
|
|
211
347
|
return {
|
|
212
348
|
genAIEndpoint,
|
|
@@ -223,13 +359,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
223
359
|
return async function (...args) {
|
|
224
360
|
const span = tracer.startSpan(genAIEndpoint, { kind: api_1.SpanKind.CLIENT });
|
|
225
361
|
return api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
|
|
226
|
-
let metricParams
|
|
227
|
-
genAIEndpoint,
|
|
228
|
-
model: '',
|
|
229
|
-
user: '',
|
|
230
|
-
cost: 0,
|
|
231
|
-
aiSystem: OpenAIWrapper.aiSystem,
|
|
232
|
-
};
|
|
362
|
+
let metricParams;
|
|
233
363
|
try {
|
|
234
364
|
const response = await originalMethod.apply(this, args);
|
|
235
365
|
const model = response.model || 'text-embedding-ada-002';
|
|
@@ -237,7 +367,7 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
237
367
|
const cost = helpers_1.default.getEmbedModelCost(model, pricingInfo, response.usage.prompt_tokens);
|
|
238
368
|
span.setAttribute(semantic_convention_1.default.GEN_AI_OPERATION, semantic_convention_1.default.GEN_AI_OPERATION_TYPE_EMBEDDING);
|
|
239
369
|
const { dimensions, encoding_format = 'float', input, user } = args[0];
|
|
240
|
-
// Set base span
|
|
370
|
+
// Set base span attributes
|
|
241
371
|
OpenAIWrapper.setBaseSpanAttributes(span, {
|
|
242
372
|
genAIEndpoint,
|
|
243
373
|
model,
|
|
@@ -245,15 +375,29 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
245
375
|
cost,
|
|
246
376
|
aiSystem: OpenAIWrapper.aiSystem,
|
|
247
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');
|
|
248
385
|
// Request Params attributes : Start
|
|
249
|
-
span.setAttribute(semantic_convention_1.default.GEN_AI_REQUEST_ENCODING_FORMATS, encoding_format);
|
|
250
|
-
|
|
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
|
+
}
|
|
251
393
|
if (traceContent) {
|
|
252
|
-
|
|
394
|
+
const formattedInput = typeof input === 'string' ? input : JSON.stringify(input);
|
|
395
|
+
span.setAttribute(semantic_convention_1.default.GEN_AI_CONTENT_PROMPT, formattedInput);
|
|
253
396
|
}
|
|
254
397
|
// Request Params attributes : End
|
|
255
398
|
span.setAttribute(semantic_convention_1.default.GEN_AI_USAGE_INPUT_TOKENS, response.usage.prompt_tokens);
|
|
256
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);
|
|
257
401
|
metricParams = {
|
|
258
402
|
genAIEndpoint,
|
|
259
403
|
model,
|
|
@@ -265,10 +409,13 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
265
409
|
}
|
|
266
410
|
catch (e) {
|
|
267
411
|
helpers_1.default.handleException(span, e);
|
|
412
|
+
throw e;
|
|
268
413
|
}
|
|
269
414
|
finally {
|
|
270
415
|
span.end();
|
|
271
|
-
|
|
416
|
+
if (metricParams) {
|
|
417
|
+
base_wrapper_1.default.recordMetrics(span, metricParams);
|
|
418
|
+
}
|
|
272
419
|
}
|
|
273
420
|
});
|
|
274
421
|
};
|
|
@@ -513,6 +660,287 @@ class OpenAIWrapper extends base_wrapper_1.default {
|
|
|
513
660
|
};
|
|
514
661
|
};
|
|
515
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
|
+
}
|
|
516
944
|
}
|
|
517
945
|
OpenAIWrapper.aiSystem = semantic_convention_1.default.GEN_AI_SYSTEM_OPENAI;
|
|
518
946
|
exports.default = OpenAIWrapper;
|