@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,757 @@
1
+ /*
2
+ * Copyright Traceloop
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { type Attributes, context, type Span, SpanKind, trace } from "@opentelemetry/api";
18
+ import {
19
+ InstrumentationBase,
20
+ type InstrumentationModuleDefinition,
21
+ InstrumentationNodeModuleDefinition,
22
+ safeExecuteInTheMiddle,
23
+ } from "@opentelemetry/instrumentation";
24
+ import {
25
+ ATTR_GEN_AI_COMPLETION,
26
+ ATTR_GEN_AI_PROMPT,
27
+ ATTR_GEN_AI_REQUEST_MAX_TOKENS,
28
+ ATTR_GEN_AI_REQUEST_MODEL,
29
+ ATTR_GEN_AI_REQUEST_TEMPERATURE,
30
+ ATTR_GEN_AI_REQUEST_TOP_P,
31
+ ATTR_GEN_AI_RESPONSE_MODEL,
32
+ ATTR_GEN_AI_SYSTEM,
33
+ ATTR_GEN_AI_USAGE_COMPLETION_TOKENS,
34
+ ATTR_GEN_AI_USAGE_PROMPT_TOKENS,
35
+ } from "@opentelemetry/semantic-conventions/incubating";
36
+ import {
37
+ CONTEXT_KEY_ALLOW_TRACE_CONTENT,
38
+ SpanAttributes,
39
+ } from "@traceloop/ai-semantic-conventions";
40
+ import { encodingForModel, type Tiktoken, type TiktokenModel } from "js-tiktoken";
41
+ import type * as openai from "openai";
42
+ import type {
43
+ ChatCompletion,
44
+ ChatCompletionChunk,
45
+ ChatCompletionCreateParamsNonStreaming,
46
+ ChatCompletionCreateParamsStreaming,
47
+ Completion,
48
+ CompletionChoice,
49
+ CompletionCreateParamsNonStreaming,
50
+ CompletionCreateParamsStreaming,
51
+ } from "openai/resources";
52
+ import type { Stream } from "openai/streaming";
53
+ import { version } from "../../package.json";
54
+ import type { OpenAIInstrumentationConfig } from "./types";
55
+
56
+ // Type definition for APIPromise - compatible with both OpenAI v4 and v5+
57
+ // The actual import is handled at runtime via require() calls in the _wrapPromise method
58
+ type APIPromiseType<T> = Promise<T> & {
59
+ _thenUnwrap: <U>(onFulfilled: (value: T) => U) => APIPromiseType<U>;
60
+ };
61
+
62
+ import { wrapImageEdit, wrapImageGeneration, wrapImageVariation } from "./image-wrappers";
63
+
64
+ export class OpenAIInstrumentation extends InstrumentationBase {
65
+ protected declare _config: OpenAIInstrumentationConfig;
66
+
67
+ constructor(config: OpenAIInstrumentationConfig = {}) {
68
+ super("overmind-js/openai-instrumentation", version, config);
69
+ }
70
+
71
+ public override setConfig(config: OpenAIInstrumentationConfig = {}) {
72
+ super.setConfig(config);
73
+ }
74
+
75
+ public manuallyInstrument(module: unknown) {
76
+ this._diag.debug(`Manually instrumenting openai`);
77
+
78
+ const openaiModule = module as any;
79
+
80
+ this._wrap(openaiModule.Chat.Completions.prototype, "create", this.patchOpenAI("chat"));
81
+ this._wrap(openaiModule.Completions.prototype, "create", this.patchOpenAI("completion"));
82
+
83
+ if (openaiModule.Images) {
84
+ this._wrap(
85
+ openaiModule.Images.prototype,
86
+ "generate",
87
+ wrapImageGeneration(this.tracer, this._config.uploadBase64Image, this._config)
88
+ );
89
+ this._wrap(
90
+ openaiModule.Images.prototype,
91
+ "edit",
92
+ wrapImageEdit(this.tracer, this._config.uploadBase64Image, this._config)
93
+ );
94
+ this._wrap(
95
+ openaiModule.Images.prototype,
96
+ "createVariation",
97
+ wrapImageVariation(this.tracer, this._config.uploadBase64Image, this._config)
98
+ );
99
+ }
100
+ }
101
+
102
+ protected init(): InstrumentationModuleDefinition {
103
+ const module = new InstrumentationNodeModuleDefinition(
104
+ "openai",
105
+ [">=4 <6"],
106
+ this.patch.bind(this),
107
+ this.unpatch.bind(this)
108
+ );
109
+ return module;
110
+ }
111
+
112
+ private patch(moduleExports: typeof openai, moduleVersion?: string) {
113
+ this._diag.debug(`Patching openai@${moduleVersion}`);
114
+
115
+ // Old version of OpenAI API (v3.1.0)
116
+ if ((moduleExports as any).OpenAIApi) {
117
+ this._wrap(
118
+ (moduleExports as any).OpenAIApi.prototype,
119
+ "createChatCompletion",
120
+ this.patchOpenAI("chat", "v3")
121
+ );
122
+ this._wrap(
123
+ (moduleExports as any).OpenAIApi.prototype,
124
+ "createCompletion",
125
+ this.patchOpenAI("completion", "v3")
126
+ );
127
+ } else {
128
+ this._wrap(
129
+ moduleExports.OpenAI.Chat.Completions.prototype,
130
+ "create",
131
+ this.patchOpenAI("chat")
132
+ );
133
+ this._wrap(
134
+ moduleExports.OpenAI.Completions.prototype,
135
+ "create",
136
+ this.patchOpenAI("completion")
137
+ );
138
+
139
+ if (moduleExports.OpenAI.Images) {
140
+ this._wrap(
141
+ moduleExports.OpenAI.Images.prototype,
142
+ "generate",
143
+ wrapImageGeneration(this.tracer, this._config.uploadBase64Image, this._config)
144
+ );
145
+ this._wrap(
146
+ moduleExports.OpenAI.Images.prototype,
147
+ "edit",
148
+ wrapImageEdit(this.tracer, this._config.uploadBase64Image, this._config)
149
+ );
150
+ this._wrap(
151
+ moduleExports.OpenAI.Images.prototype,
152
+ "createVariation",
153
+ wrapImageVariation(this.tracer, this._config.uploadBase64Image, this._config)
154
+ );
155
+ }
156
+ }
157
+ return moduleExports;
158
+ }
159
+
160
+ private unpatch(moduleExports: typeof openai, moduleVersion?: string): void {
161
+ this._diag.debug(`Unpatching openai@${moduleVersion}`);
162
+
163
+ // Old version of OpenAI API (v3.1.0)
164
+ if ((moduleExports as any).OpenAIApi) {
165
+ this._unwrap((moduleExports as any).OpenAIApi.prototype, "createChatCompletion");
166
+ this._unwrap((moduleExports as any).OpenAIApi.prototype, "createCompletion");
167
+ } else {
168
+ this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
169
+ this._unwrap(moduleExports.OpenAI.Completions.prototype, "create");
170
+
171
+ if (moduleExports.OpenAI.Images) {
172
+ this._unwrap(moduleExports.OpenAI.Images.prototype, "generate");
173
+ this._unwrap(moduleExports.OpenAI.Images.prototype, "edit");
174
+ this._unwrap(moduleExports.OpenAI.Images.prototype, "createVariation");
175
+ }
176
+ }
177
+ }
178
+
179
+ private patchOpenAI(type: "chat" | "completion", version: "v3" | "v4" = "v4") {
180
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
181
+ const plugin = this;
182
+ // eslint-disable-next-line
183
+ return (original: Function) => {
184
+ return function method(this: any, ...args: unknown[]) {
185
+ const span =
186
+ type === "chat"
187
+ ? plugin.startSpan({
188
+ type,
189
+ params: args[0] as ChatCompletionCreateParamsNonStreaming & {
190
+ extraAttributes?: Record<string, any>;
191
+ },
192
+ client: this,
193
+ })
194
+ : plugin.startSpan({
195
+ type,
196
+ params: args[0] as CompletionCreateParamsNonStreaming & {
197
+ extraAttributes?: Record<string, any>;
198
+ },
199
+ client: this,
200
+ });
201
+
202
+ const execContext = trace.setSpan(context.active(), span);
203
+ const execPromise = safeExecuteInTheMiddle(
204
+ () => {
205
+ return context.with(execContext, () => {
206
+ if ((args?.[0] as any)?.extraAttributes) {
207
+ delete (args[0] as any).extraAttributes;
208
+ }
209
+ return original.apply(this, args);
210
+ });
211
+ },
212
+ (e) => {
213
+ if (e) {
214
+ plugin._diag.error("OpenAI instrumentation: error", e);
215
+ }
216
+ }
217
+ );
218
+
219
+ if (
220
+ (args[0] as ChatCompletionCreateParamsStreaming | CompletionCreateParamsStreaming).stream
221
+ ) {
222
+ return context.bind(
223
+ execContext,
224
+ plugin._streamingWrapPromise({
225
+ span,
226
+ type,
227
+ params: args[0] as any,
228
+ promise: execPromise,
229
+ })
230
+ );
231
+ }
232
+
233
+ const wrappedPromise = plugin._wrapPromise(type, version, span, execPromise);
234
+
235
+ return context.bind(execContext, wrappedPromise as any);
236
+ };
237
+ };
238
+ }
239
+
240
+ private startSpan({
241
+ type,
242
+ params,
243
+ client,
244
+ }:
245
+ | {
246
+ type: "chat";
247
+ params: ChatCompletionCreateParamsNonStreaming & {
248
+ extraAttributes?: Record<string, any>;
249
+ };
250
+ client: any;
251
+ }
252
+ | {
253
+ type: "completion";
254
+ params: CompletionCreateParamsNonStreaming & {
255
+ extraAttributes?: Record<string, any>;
256
+ };
257
+ client: any;
258
+ }): Span {
259
+ const { provider } = this._detectVendorFromURL(client);
260
+
261
+ const attributes: Attributes = {
262
+ [ATTR_GEN_AI_SYSTEM]: provider,
263
+ [SpanAttributes.LLM_REQUEST_TYPE]: type,
264
+ };
265
+
266
+ try {
267
+ attributes[ATTR_GEN_AI_REQUEST_MODEL] = params.model;
268
+ if (params.max_tokens) {
269
+ attributes[ATTR_GEN_AI_REQUEST_MAX_TOKENS] = params.max_tokens;
270
+ }
271
+ if (params.temperature) {
272
+ attributes[ATTR_GEN_AI_REQUEST_TEMPERATURE] = params.temperature;
273
+ }
274
+ if (params.top_p) {
275
+ attributes[ATTR_GEN_AI_REQUEST_TOP_P] = params.top_p;
276
+ }
277
+ if (params.frequency_penalty) {
278
+ attributes[SpanAttributes.LLM_FREQUENCY_PENALTY] = params.frequency_penalty;
279
+ }
280
+ if (params.presence_penalty) {
281
+ attributes[SpanAttributes.LLM_PRESENCE_PENALTY] = params.presence_penalty;
282
+ }
283
+
284
+ if (params.extraAttributes !== undefined && typeof params.extraAttributes === "object") {
285
+ Object.keys(params.extraAttributes).forEach((key: string) => {
286
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287
+ attributes[key] = params.extraAttributes![key];
288
+ });
289
+ }
290
+
291
+ if (this._shouldSendPrompts()) {
292
+ if (type === "chat") {
293
+ params.messages.forEach((message, index) => {
294
+ attributes[`${ATTR_GEN_AI_PROMPT}.${index}.role`] = message.role;
295
+ if (typeof message.content === "string") {
296
+ attributes[`${ATTR_GEN_AI_PROMPT}.${index}.content`] =
297
+ (message.content as string) || "";
298
+ } else {
299
+ attributes[`${ATTR_GEN_AI_PROMPT}.${index}.content`] = JSON.stringify(
300
+ message.content
301
+ );
302
+ }
303
+ });
304
+ params.functions?.forEach((func, index) => {
305
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.name`] = func.name;
306
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.description`] =
307
+ func.description;
308
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.arguments`] =
309
+ JSON.stringify(func.parameters);
310
+ });
311
+ params.tools?.forEach((tool, index) => {
312
+ if (tool.type !== "function" || !("function" in tool) || !tool.function) {
313
+ return;
314
+ }
315
+
316
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.name`] =
317
+ tool.function.name;
318
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.description`] =
319
+ tool.function.description;
320
+ attributes[`${SpanAttributes.LLM_REQUEST_FUNCTIONS}.${index}.arguments`] =
321
+ JSON.stringify(tool.function.parameters);
322
+ });
323
+ } else {
324
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
325
+ if (typeof params.prompt === "string") {
326
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt;
327
+ } else {
328
+ attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify(params.prompt);
329
+ }
330
+ }
331
+ }
332
+ } catch (e) {
333
+ this._diag.debug(e);
334
+ this._config.exceptionLogger?.(e);
335
+ }
336
+
337
+ return this.tracer.startSpan(`openai.${type}`, {
338
+ kind: SpanKind.CLIENT,
339
+ attributes,
340
+ });
341
+ }
342
+
343
+ private async *_streamingWrapPromise({
344
+ span,
345
+ type,
346
+ params,
347
+ promise,
348
+ }:
349
+ | {
350
+ span: Span;
351
+ type: "chat";
352
+ params: ChatCompletionCreateParamsStreaming;
353
+ promise: APIPromiseType<Stream<ChatCompletionChunk>>;
354
+ }
355
+ | {
356
+ span: Span;
357
+ params: CompletionCreateParamsStreaming;
358
+ type: "completion";
359
+ promise: APIPromiseType<Stream<Completion>>;
360
+ }) {
361
+ if (type === "chat") {
362
+ const result: ChatCompletion = {
363
+ id: "0",
364
+ created: -1,
365
+ model: "",
366
+ choices: [
367
+ {
368
+ index: 0,
369
+ logprobs: null,
370
+ finish_reason: "stop",
371
+ message: {
372
+ role: "assistant",
373
+ content: "",
374
+ tool_calls: [],
375
+ } as any,
376
+ },
377
+ ],
378
+ object: "chat.completion",
379
+ };
380
+ for await (const chunk of await promise) {
381
+ yield chunk;
382
+
383
+ result.id = chunk.id;
384
+ result.created = chunk.created;
385
+ result.model = chunk.model;
386
+
387
+ if (chunk.choices[0]?.finish_reason) {
388
+ result.choices[0].finish_reason = chunk.choices[0].finish_reason;
389
+ }
390
+ if (chunk.choices[0]?.logprobs) {
391
+ result.choices[0].logprobs = chunk.choices[0].logprobs;
392
+ }
393
+ if (chunk.choices[0]?.delta.content) {
394
+ result.choices[0].message.content += chunk.choices[0].delta.content;
395
+ }
396
+ if (
397
+ chunk.choices[0]?.delta.function_call &&
398
+ chunk.choices[0]?.delta.function_call.arguments &&
399
+ chunk.choices[0]?.delta.function_call.name
400
+ ) {
401
+ // I needed to re-build the object so that Typescript will understand that `name` and `argument` are not null.
402
+ result.choices[0].message.function_call = {
403
+ name: chunk.choices[0].delta.function_call.name,
404
+ arguments: chunk.choices[0].delta.function_call.arguments,
405
+ };
406
+ }
407
+ for (const toolCall of chunk.choices[0]?.delta?.tool_calls ?? []) {
408
+ if ((result.choices[0].message.tool_calls?.length ?? 0) < toolCall.index + 1) {
409
+ result.choices[0].message.tool_calls?.push({
410
+ function: {
411
+ name: "",
412
+ arguments: "",
413
+ },
414
+ id: "",
415
+ type: "function",
416
+ });
417
+ }
418
+
419
+ if (result.choices[0].message.tool_calls) {
420
+ if (toolCall.id) {
421
+ result.choices[0].message.tool_calls[toolCall.index].id += toolCall.id;
422
+ }
423
+ if (toolCall.type) {
424
+ result.choices[0].message.tool_calls[toolCall.index].type = toolCall.type;
425
+ }
426
+ if (toolCall.function?.name) {
427
+ (result.choices[0].message.tool_calls[toolCall.index] as any).function.name +=
428
+ toolCall.function.name;
429
+ }
430
+ if (toolCall.function?.arguments) {
431
+ (result.choices[0].message.tool_calls[toolCall.index] as any).function.arguments +=
432
+ toolCall.function.arguments;
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ if (result.choices[0].logprobs?.content) {
439
+ this._addLogProbsEvent(span, result.choices[0].logprobs);
440
+ }
441
+
442
+ if (this._config.enrichTokens) {
443
+ let promptTokens = 0;
444
+ for (const message of params.messages) {
445
+ promptTokens += this.tokenCountFromString(message.content as string, result.model) ?? 0;
446
+ }
447
+
448
+ const completionTokens = this.tokenCountFromString(
449
+ result.choices[0].message.content ?? "",
450
+ result.model
451
+ );
452
+ if (completionTokens) {
453
+ result.usage = {
454
+ prompt_tokens: promptTokens,
455
+ completion_tokens: completionTokens,
456
+ total_tokens: promptTokens + completionTokens,
457
+ };
458
+ }
459
+ }
460
+
461
+ this._endSpan({ span, type, result });
462
+ } else {
463
+ const result: Completion = {
464
+ id: "0",
465
+ created: -1,
466
+ model: "",
467
+ choices: [
468
+ {
469
+ index: 0,
470
+ logprobs: null,
471
+ finish_reason: "stop",
472
+ text: "",
473
+ },
474
+ ],
475
+ object: "text_completion",
476
+ };
477
+ for await (const chunk of await promise) {
478
+ yield chunk;
479
+
480
+ try {
481
+ result.id = chunk.id;
482
+ result.created = chunk.created;
483
+ result.model = chunk.model;
484
+
485
+ if (chunk.choices[0]?.finish_reason) {
486
+ result.choices[0].finish_reason = chunk.choices[0].finish_reason;
487
+ }
488
+ if (chunk.choices[0]?.logprobs) {
489
+ result.choices[0].logprobs = chunk.choices[0].logprobs;
490
+ }
491
+ if (chunk.choices[0]?.text) {
492
+ result.choices[0].text += chunk.choices[0].text;
493
+ }
494
+ } catch (e) {
495
+ this._diag.debug(e);
496
+ this._config.exceptionLogger?.(e);
497
+ }
498
+ }
499
+
500
+ try {
501
+ if (result.choices[0].logprobs) {
502
+ this._addLogProbsEvent(span, result.choices[0].logprobs);
503
+ }
504
+
505
+ if (this._config.enrichTokens) {
506
+ const promptTokens =
507
+ this.tokenCountFromString(params.prompt as string, result.model) ?? 0;
508
+
509
+ const completionTokens = this.tokenCountFromString(
510
+ result.choices[0].text ?? "",
511
+ result.model
512
+ );
513
+ if (completionTokens) {
514
+ result.usage = {
515
+ prompt_tokens: promptTokens,
516
+ completion_tokens: completionTokens,
517
+ total_tokens: promptTokens + completionTokens,
518
+ };
519
+ }
520
+ }
521
+ } catch (e) {
522
+ this._diag.debug(e);
523
+ this._config.exceptionLogger?.(e);
524
+ }
525
+
526
+ this._endSpan({ span, type, result });
527
+ }
528
+ }
529
+
530
+ private _wrapPromise<T>(
531
+ type: "chat" | "completion",
532
+ version: "v3" | "v4",
533
+ span: Span,
534
+ promise: APIPromiseType<T>
535
+ ): APIPromiseType<T> {
536
+ return promise._thenUnwrap((result) => {
537
+ if (version === "v3") {
538
+ if (type === "chat") {
539
+ this._addLogProbsEvent(
540
+ span,
541
+ ((result as any).data as ChatCompletion).choices[0].logprobs
542
+ );
543
+ this._endSpan({
544
+ type,
545
+ span,
546
+ result: (result as any).data as ChatCompletion,
547
+ });
548
+ } else {
549
+ this._addLogProbsEvent(span, ((result as any).data as Completion).choices[0].logprobs);
550
+ this._endSpan({
551
+ type,
552
+ span,
553
+ result: (result as any).data as Completion,
554
+ });
555
+ }
556
+ } else {
557
+ if (type === "chat") {
558
+ this._addLogProbsEvent(span, (result as ChatCompletion).choices[0].logprobs);
559
+ this._endSpan({ type, span, result: result as ChatCompletion });
560
+ } else {
561
+ this._addLogProbsEvent(span, (result as Completion).choices[0].logprobs);
562
+ this._endSpan({ type, span, result: result as Completion });
563
+ }
564
+ }
565
+
566
+ return result;
567
+ });
568
+ }
569
+
570
+ private _endSpan({
571
+ span,
572
+ type,
573
+ result,
574
+ }:
575
+ | { span: Span; type: "chat"; result: ChatCompletion }
576
+ | { span: Span; type: "completion"; result: Completion }) {
577
+ try {
578
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, result.model);
579
+ if (result.usage) {
580
+ span.setAttribute(SpanAttributes.LLM_USAGE_TOTAL_TOKENS, result.usage?.total_tokens);
581
+ span.setAttribute(ATTR_GEN_AI_USAGE_COMPLETION_TOKENS, result.usage?.completion_tokens);
582
+ span.setAttribute(ATTR_GEN_AI_USAGE_PROMPT_TOKENS, result.usage?.prompt_tokens);
583
+ }
584
+
585
+ if (this._shouldSendPrompts()) {
586
+ if (type === "chat") {
587
+ result.choices.forEach((choice, index) => {
588
+ span.setAttribute(
589
+ `${ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`,
590
+ choice.finish_reason
591
+ );
592
+ span.setAttribute(`${ATTR_GEN_AI_COMPLETION}.${index}.role`, choice.message.role);
593
+ span.setAttribute(
594
+ `${ATTR_GEN_AI_COMPLETION}.${index}.content`,
595
+ choice.message.content ?? ""
596
+ );
597
+
598
+ if (choice.message.function_call) {
599
+ span.setAttribute(
600
+ `${ATTR_GEN_AI_COMPLETION}.${index}.function_call.name`,
601
+ choice.message.function_call.name
602
+ );
603
+ span.setAttribute(
604
+ `${ATTR_GEN_AI_COMPLETION}.${index}.function_call.arguments`,
605
+ choice.message.function_call.arguments
606
+ );
607
+ }
608
+ for (const [toolIndex, toolCall] of choice?.message?.tool_calls?.entries() || []) {
609
+ if (toolCall.type === "function" && "function" in toolCall) {
610
+ span.setAttribute(
611
+ `${ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.name`,
612
+ toolCall.function.name
613
+ );
614
+ span.setAttribute(
615
+ `${ATTR_GEN_AI_COMPLETION}.${index}.tool_calls.${toolIndex}.arguments`,
616
+ toolCall.function.arguments
617
+ );
618
+ }
619
+ }
620
+ });
621
+ } else {
622
+ result.choices.forEach((choice, index) => {
623
+ span.setAttribute(
624
+ `${ATTR_GEN_AI_COMPLETION}.${index}.finish_reason`,
625
+ choice.finish_reason
626
+ );
627
+ span.setAttribute(`${ATTR_GEN_AI_COMPLETION}.${index}.role`, "assistant");
628
+ span.setAttribute(`${ATTR_GEN_AI_COMPLETION}.${index}.content`, choice.text);
629
+ });
630
+ }
631
+ }
632
+ } catch (e) {
633
+ this._diag.debug(e);
634
+ this._config.exceptionLogger?.(e);
635
+ }
636
+
637
+ span.end();
638
+ }
639
+
640
+ private _shouldSendPrompts() {
641
+ const contextShouldSendPrompts = context.active().getValue(CONTEXT_KEY_ALLOW_TRACE_CONTENT);
642
+
643
+ if (contextShouldSendPrompts !== undefined) {
644
+ return contextShouldSendPrompts;
645
+ }
646
+
647
+ return this._config.traceContent !== undefined ? this._config.traceContent : true;
648
+ }
649
+
650
+ private _addLogProbsEvent(
651
+ span: Span,
652
+ logprobs:
653
+ | ChatCompletion.Choice.Logprobs
654
+ | ChatCompletionChunk.Choice.Logprobs
655
+ | CompletionChoice.Logprobs
656
+ | null
657
+ ) {
658
+ try {
659
+ let result: { token: string; logprob: number }[] = [];
660
+
661
+ if (!logprobs) {
662
+ return;
663
+ }
664
+
665
+ const chatLogprobs = logprobs as
666
+ | ChatCompletion.Choice.Logprobs
667
+ | ChatCompletionChunk.Choice.Logprobs;
668
+ const completionLogprobs = logprobs as CompletionChoice.Logprobs;
669
+ if (chatLogprobs.content) {
670
+ result = chatLogprobs.content.map((logprob) => {
671
+ return {
672
+ token: logprob.token,
673
+ logprob: logprob.logprob,
674
+ };
675
+ });
676
+ } else if (completionLogprobs?.tokens && completionLogprobs?.token_logprobs) {
677
+ completionLogprobs.tokens.forEach((token, index) => {
678
+ const logprob = completionLogprobs.token_logprobs?.[index];
679
+ if (logprob) {
680
+ result.push({
681
+ token,
682
+ logprob,
683
+ });
684
+ }
685
+ });
686
+ }
687
+
688
+ span.addEvent("logprobs", { logprobs: JSON.stringify(result) });
689
+ } catch (e) {
690
+ this._diag.debug(e);
691
+ this._config.exceptionLogger?.(e);
692
+ }
693
+ }
694
+
695
+ private _encodingCache = new Map<string, Tiktoken>();
696
+
697
+ private tokenCountFromString(text: string, model: string) {
698
+ if (!text) {
699
+ return 0;
700
+ }
701
+
702
+ let encoding = this._encodingCache.get(model);
703
+
704
+ if (!encoding) {
705
+ try {
706
+ encoding = encodingForModel(model as TiktokenModel);
707
+ this._encodingCache.set(model, encoding);
708
+ } catch (e) {
709
+ this._diag.debug(e);
710
+ this._config.exceptionLogger?.(e);
711
+ return 0;
712
+ }
713
+ }
714
+
715
+ return encoding.encode(text).length;
716
+ }
717
+
718
+ private _detectVendorFromURL(client: any): {
719
+ provider: string;
720
+ modelVendor: string;
721
+ } {
722
+ const modelVendor = "OpenAI";
723
+
724
+ try {
725
+ if (!client?.baseURL) {
726
+ return { provider: "OpenAI", modelVendor };
727
+ }
728
+
729
+ const baseURL = client.baseURL.toLowerCase();
730
+
731
+ if (baseURL.includes("azure") || baseURL.includes("openai.azure.com")) {
732
+ return { provider: "Azure", modelVendor };
733
+ }
734
+
735
+ if (baseURL.includes("openai.com") || baseURL.includes("api.openai.com")) {
736
+ return { provider: "OpenAI", modelVendor };
737
+ }
738
+
739
+ if (baseURL.includes("amazonaws.com") || baseURL.includes("bedrock")) {
740
+ return { provider: "AWS", modelVendor };
741
+ }
742
+
743
+ if (baseURL.includes("googleapis.com")) {
744
+ return { provider: "Google", modelVendor };
745
+ }
746
+
747
+ if (baseURL.includes("openrouter")) {
748
+ return { provider: "OpenRouter", modelVendor };
749
+ }
750
+
751
+ return { provider: "OpenAI", modelVendor };
752
+ } catch (e) {
753
+ this._diag.debug(`Failed to detect vendor from URL: ${e}`);
754
+ return { provider: "OpenAI", modelVendor };
755
+ }
756
+ }
757
+ }