@arizeai/openinference-instrumentation-openai 0.0.4 → 0.1.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/dist/esm/instrumentation.d.ts.map +1 -1
- package/dist/esm/instrumentation.js +438 -44
- package/dist/esm/instrumentation.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/typeUtils.d.ts +9 -0
- package/dist/esm/typeUtils.d.ts.map +1 -0
- package/dist/esm/typeUtils.js +13 -0
- package/dist/esm/typeUtils.js.map +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esnext/instrumentation.d.ts.map +1 -1
- package/dist/esnext/instrumentation.js +300 -43
- package/dist/esnext/instrumentation.js.map +1 -1
- package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
- package/dist/esnext/typeUtils.d.ts +9 -0
- package/dist/esnext/typeUtils.d.ts.map +1 -0
- package/dist/esnext/typeUtils.js +13 -0
- package/dist/esnext/typeUtils.js.map +1 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/src/instrumentation.d.ts.map +1 -1
- package/dist/src/instrumentation.js +312 -43
- package/dist/src/instrumentation.js.map +1 -1
- package/dist/src/typeUtils.d.ts +9 -0
- package/dist/src/typeUtils.d.ts.map +1 -0
- package/dist/src/typeUtils.js +18 -0
- package/dist/src/typeUtils.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/test/openai.test.js +607 -1
- package/dist/test/openai.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/instrumentation.ts +368 -65
- package/src/typeUtils.ts +13 -0
- package/src/version.ts +1 -1
package/src/instrumentation.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
SpanKind,
|
|
14
14
|
Attributes,
|
|
15
15
|
SpanStatusCode,
|
|
16
|
+
Span,
|
|
16
17
|
} from "@opentelemetry/api";
|
|
17
18
|
import { VERSION } from "./version";
|
|
18
19
|
import {
|
|
@@ -24,12 +25,17 @@ import {
|
|
|
24
25
|
ChatCompletion,
|
|
25
26
|
ChatCompletionChunk,
|
|
26
27
|
ChatCompletionCreateParamsBase,
|
|
28
|
+
ChatCompletionMessage,
|
|
29
|
+
ChatCompletionMessageParam,
|
|
27
30
|
} from "openai/resources/chat/completions";
|
|
31
|
+
import { CompletionCreateParamsBase } from "openai/resources/completions";
|
|
28
32
|
import { Stream } from "openai/streaming";
|
|
29
33
|
import {
|
|
34
|
+
Completion,
|
|
30
35
|
CreateEmbeddingResponse,
|
|
31
36
|
EmbeddingCreateParams,
|
|
32
37
|
} from "openai/resources";
|
|
38
|
+
import { assertUnreachable, isString } from "./typeUtils";
|
|
33
39
|
|
|
34
40
|
const MODULE_NAME = "openai";
|
|
35
41
|
|
|
@@ -64,7 +70,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
64
70
|
}
|
|
65
71
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
66
72
|
const instrumentation: OpenAIInstrumentation = this;
|
|
67
|
-
|
|
73
|
+
|
|
74
|
+
type ChatCompletionCreateType =
|
|
68
75
|
typeof module.OpenAI.Chat.Completions.prototype.create;
|
|
69
76
|
|
|
70
77
|
// Patch create chat completions
|
|
@@ -72,12 +79,10 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
72
79
|
module.OpenAI.Chat.Completions.prototype,
|
|
73
80
|
"create",
|
|
74
81
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
-
(original:
|
|
82
|
+
(original: ChatCompletionCreateType): any => {
|
|
76
83
|
return function patchedCreate(
|
|
77
84
|
this: unknown,
|
|
78
|
-
...args: Parameters<
|
|
79
|
-
typeof module.OpenAI.Chat.Completions.prototype.create
|
|
80
|
-
>
|
|
85
|
+
...args: Parameters<ChatCompletionCreateType>
|
|
81
86
|
) {
|
|
82
87
|
const body = args[0];
|
|
83
88
|
const { messages: _messages, ...invocationParameters } = body;
|
|
@@ -99,7 +104,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
99
104
|
);
|
|
100
105
|
const execContext = trace.setSpan(context.active(), span);
|
|
101
106
|
const execPromise = safeExecuteInTheMiddle<
|
|
102
|
-
ReturnType<
|
|
107
|
+
ReturnType<ChatCompletionCreateType>
|
|
103
108
|
>(
|
|
104
109
|
() => {
|
|
105
110
|
return context.with(execContext, () => {
|
|
@@ -119,23 +124,95 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
119
124
|
},
|
|
120
125
|
);
|
|
121
126
|
const wrappedPromise = execPromise.then((result) => {
|
|
122
|
-
if (result) {
|
|
127
|
+
if (isChatCompletionResponse(result)) {
|
|
123
128
|
// Record the results
|
|
124
129
|
span.setAttributes({
|
|
125
130
|
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
|
|
126
131
|
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
|
|
127
132
|
// Override the model from the value sent by the server
|
|
128
|
-
[SemanticConventions.LLM_MODEL_NAME]:
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
? result.model
|
|
132
|
-
: body.model,
|
|
133
|
-
...getLLMOutputMessagesAttributes(result),
|
|
133
|
+
[SemanticConventions.LLM_MODEL_NAME]: result.model,
|
|
134
|
+
...getChatCompletionLLMOutputMessagesAttributes(result),
|
|
134
135
|
...getUsageAttributes(result),
|
|
135
136
|
});
|
|
137
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
138
|
+
span.end();
|
|
139
|
+
} else {
|
|
140
|
+
// This is a streaming response
|
|
141
|
+
// handle the chunks and add them to the span
|
|
142
|
+
// First split the stream via tee
|
|
143
|
+
const [leftStream, rightStream] = result.tee();
|
|
144
|
+
consumeChatCompletionStreamChunks(rightStream, span);
|
|
145
|
+
result = leftStream;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
});
|
|
150
|
+
return context.bind(execContext, wrappedPromise);
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Patch create completions
|
|
156
|
+
type CompletionsCreateType =
|
|
157
|
+
typeof module.OpenAI.Completions.prototype.create;
|
|
158
|
+
|
|
159
|
+
this._wrap(
|
|
160
|
+
module.OpenAI.Completions.prototype,
|
|
161
|
+
"create",
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
(original: CompletionsCreateType): any => {
|
|
164
|
+
return function patchedCreate(
|
|
165
|
+
this: unknown,
|
|
166
|
+
...args: Parameters<CompletionsCreateType>
|
|
167
|
+
) {
|
|
168
|
+
const body = args[0];
|
|
169
|
+
const { prompt: _prompt, ...invocationParameters } = body;
|
|
170
|
+
const span = instrumentation.tracer.startSpan(`OpenAI Completions`, {
|
|
171
|
+
kind: SpanKind.INTERNAL,
|
|
172
|
+
attributes: {
|
|
173
|
+
[SemanticConventions.OPENINFERENCE_SPAN_KIND]:
|
|
174
|
+
OpenInferenceSpanKind.LLM,
|
|
175
|
+
[SemanticConventions.LLM_MODEL_NAME]: body.model,
|
|
176
|
+
[SemanticConventions.LLM_INVOCATION_PARAMETERS]:
|
|
177
|
+
JSON.stringify(invocationParameters),
|
|
178
|
+
...getCompletionInputValueAndMimeType(body),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const execContext = trace.setSpan(context.active(), span);
|
|
182
|
+
const execPromise = safeExecuteInTheMiddle<
|
|
183
|
+
ReturnType<CompletionsCreateType>
|
|
184
|
+
>(
|
|
185
|
+
() => {
|
|
186
|
+
return context.with(execContext, () => {
|
|
187
|
+
return original.apply(this, args);
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
(error) => {
|
|
191
|
+
// Push the error to the span
|
|
192
|
+
if (error) {
|
|
193
|
+
span.recordException(error);
|
|
194
|
+
span.setStatus({
|
|
195
|
+
code: SpanStatusCode.ERROR,
|
|
196
|
+
message: error.message,
|
|
197
|
+
});
|
|
198
|
+
span.end();
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
const wrappedPromise = execPromise.then((result) => {
|
|
203
|
+
if (isCompletionResponse(result)) {
|
|
204
|
+
// Record the results
|
|
205
|
+
span.setAttributes({
|
|
206
|
+
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
|
|
207
|
+
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
|
|
208
|
+
// Override the model from the value sent by the server
|
|
209
|
+
[SemanticConventions.LLM_MODEL_NAME]: result.model,
|
|
210
|
+
...getCompletionOutputValueAndMimeType(result),
|
|
211
|
+
...getUsageAttributes(result),
|
|
212
|
+
});
|
|
213
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
214
|
+
span.end();
|
|
136
215
|
}
|
|
137
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
138
|
-
span.end();
|
|
139
216
|
return result;
|
|
140
217
|
});
|
|
141
218
|
return context.bind(execContext, wrappedPromise);
|
|
@@ -153,11 +230,11 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
153
230
|
(original: EmbeddingsCreateType): any => {
|
|
154
231
|
return function patchedEmbeddingCreate(
|
|
155
232
|
this: unknown,
|
|
156
|
-
...args: Parameters<
|
|
233
|
+
...args: Parameters<EmbeddingsCreateType>
|
|
157
234
|
) {
|
|
158
235
|
const body = args[0];
|
|
159
236
|
const { input } = body;
|
|
160
|
-
const isStringInput = typeof input
|
|
237
|
+
const isStringInput = typeof input === "string";
|
|
161
238
|
const span = instrumentation.tracer.startSpan(`OpenAI Embeddings`, {
|
|
162
239
|
kind: SpanKind.INTERNAL,
|
|
163
240
|
attributes: {
|
|
@@ -234,78 +311,215 @@ function isChatCompletionResponse(
|
|
|
234
311
|
}
|
|
235
312
|
|
|
236
313
|
/**
|
|
237
|
-
*
|
|
314
|
+
* type-guard that checks if the response is a completion response
|
|
315
|
+
*/
|
|
316
|
+
function isCompletionResponse(
|
|
317
|
+
response: Stream<Completion> | Completion,
|
|
318
|
+
): response is Completion {
|
|
319
|
+
return "choices" in response;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* type-guard that checks if completion prompt attribute is an array of strings
|
|
324
|
+
*/
|
|
325
|
+
function isPromptStringArray(
|
|
326
|
+
prompt: CompletionCreateParamsBase["prompt"],
|
|
327
|
+
): prompt is Array<string> {
|
|
328
|
+
return (
|
|
329
|
+
Array.isArray(prompt) && prompt.every((item) => typeof item === "string")
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Converts the body of a chat completions request to LLM input messages
|
|
238
335
|
*/
|
|
239
336
|
function getLLMInputMessagesAttributes(
|
|
240
337
|
body: ChatCompletionCreateParamsBase,
|
|
241
338
|
): Attributes {
|
|
242
339
|
return body.messages.reduce((acc, message, index) => {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
340
|
+
const messageAttributes = getChatCompletionInputMessageAttributes(message);
|
|
341
|
+
const indexPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}.`;
|
|
342
|
+
// Flatten the attributes on the index prefix
|
|
343
|
+
for (const [key, value] of Object.entries(messageAttributes)) {
|
|
344
|
+
acc[`${indexPrefix}${key}`] = value;
|
|
345
|
+
}
|
|
250
346
|
return acc;
|
|
251
347
|
}, {} as Attributes);
|
|
252
348
|
}
|
|
253
349
|
|
|
350
|
+
function getChatCompletionInputMessageAttributes(
|
|
351
|
+
message: ChatCompletionMessageParam,
|
|
352
|
+
): Attributes {
|
|
353
|
+
const role = message.role;
|
|
354
|
+
const attributes: Attributes = {
|
|
355
|
+
[SemanticConventions.MESSAGE_ROLE]: role,
|
|
356
|
+
};
|
|
357
|
+
// Add the content only if it is a string
|
|
358
|
+
if (typeof message.content === "string")
|
|
359
|
+
attributes[SemanticConventions.MESSAGE_CONTENT] = message.content;
|
|
360
|
+
switch (role) {
|
|
361
|
+
case "user":
|
|
362
|
+
// There's nothing to add for the user
|
|
363
|
+
break;
|
|
364
|
+
case "assistant":
|
|
365
|
+
if (message.tool_calls) {
|
|
366
|
+
message.tool_calls.forEach((toolCall, index) => {
|
|
367
|
+
// Make sure the tool call has a function
|
|
368
|
+
if (toolCall.function) {
|
|
369
|
+
const toolCallIndexPrefix = `${SemanticConventions.MESSAGE_TOOL_CALLS}.${index}.`;
|
|
370
|
+
attributes[
|
|
371
|
+
toolCallIndexPrefix + SemanticConventions.TOOL_CALL_FUNCTION_NAME
|
|
372
|
+
] = toolCall.function.name;
|
|
373
|
+
attributes[
|
|
374
|
+
toolCallIndexPrefix +
|
|
375
|
+
SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON
|
|
376
|
+
] = toolCall.function.arguments;
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
case "function":
|
|
382
|
+
attributes[SemanticConventions.MESSAGE_FUNCTION_CALL_NAME] = message.name;
|
|
383
|
+
break;
|
|
384
|
+
case "tool":
|
|
385
|
+
// There's nothing to add for the tool. There is a tool_id, but there are no
|
|
386
|
+
// semantic conventions for it
|
|
387
|
+
break;
|
|
388
|
+
case "system":
|
|
389
|
+
// There's nothing to add for the system. Content is captured above
|
|
390
|
+
break;
|
|
391
|
+
default:
|
|
392
|
+
assertUnreachable(role);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
return attributes;
|
|
396
|
+
}
|
|
397
|
+
|
|
254
398
|
/**
|
|
255
|
-
*
|
|
399
|
+
* Converts the body of a completions request to input attributes
|
|
256
400
|
*/
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
) {
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
completion.usage.total_tokens,
|
|
270
|
-
};
|
|
401
|
+
function getCompletionInputValueAndMimeType(
|
|
402
|
+
body: CompletionCreateParamsBase,
|
|
403
|
+
): Attributes {
|
|
404
|
+
if (typeof body.prompt === "string") {
|
|
405
|
+
return {
|
|
406
|
+
[SemanticConventions.INPUT_VALUE]: body.prompt,
|
|
407
|
+
[SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
|
|
408
|
+
};
|
|
409
|
+
} else if (isPromptStringArray(body.prompt)) {
|
|
410
|
+
const prompt = body.prompt[0]; // Only single prompts are currently supported
|
|
411
|
+
if (prompt === undefined) {
|
|
412
|
+
return {};
|
|
271
413
|
}
|
|
414
|
+
return {
|
|
415
|
+
[SemanticConventions.INPUT_VALUE]: prompt,
|
|
416
|
+
[SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
|
|
417
|
+
};
|
|
272
418
|
}
|
|
419
|
+
// Other cases in which the prompt is a token or array of tokens are currently unsupported
|
|
273
420
|
return {};
|
|
274
421
|
}
|
|
275
422
|
|
|
276
423
|
/**
|
|
277
|
-
*
|
|
424
|
+
* Get usage attributes
|
|
278
425
|
*/
|
|
279
|
-
function
|
|
280
|
-
|
|
426
|
+
function getUsageAttributes(
|
|
427
|
+
completion: ChatCompletion | Completion,
|
|
281
428
|
): Attributes {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const index_prefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
|
|
292
|
-
acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
|
|
293
|
-
message.content,
|
|
294
|
-
);
|
|
295
|
-
acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = message.role;
|
|
296
|
-
return acc;
|
|
297
|
-
}, {} as Attributes);
|
|
429
|
+
if (completion.usage) {
|
|
430
|
+
return {
|
|
431
|
+
[SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
|
|
432
|
+
completion.usage.completion_tokens,
|
|
433
|
+
[SemanticConventions.LLM_TOKEN_COUNT_PROMPT]:
|
|
434
|
+
completion.usage.prompt_tokens,
|
|
435
|
+
[SemanticConventions.LLM_TOKEN_COUNT_TOTAL]:
|
|
436
|
+
completion.usage.total_tokens,
|
|
437
|
+
};
|
|
298
438
|
}
|
|
299
439
|
return {};
|
|
300
440
|
}
|
|
301
441
|
|
|
442
|
+
/**
|
|
443
|
+
* Converts the chat completion result to LLM output attributes
|
|
444
|
+
*/
|
|
445
|
+
function getChatCompletionLLMOutputMessagesAttributes(
|
|
446
|
+
chatCompletion: ChatCompletion,
|
|
447
|
+
): Attributes {
|
|
448
|
+
// Right now support just the first choice
|
|
449
|
+
const choice = chatCompletion.choices[0];
|
|
450
|
+
if (!choice) {
|
|
451
|
+
return {};
|
|
452
|
+
}
|
|
453
|
+
return [choice.message].reduce((acc, message, index) => {
|
|
454
|
+
const indexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}.`;
|
|
455
|
+
const messageAttributes = getChatCompletionOutputMessageAttributes(message);
|
|
456
|
+
// Flatten the attributes on the index prefix
|
|
457
|
+
for (const [key, value] of Object.entries(messageAttributes)) {
|
|
458
|
+
acc[`${indexPrefix}${key}`] = value;
|
|
459
|
+
}
|
|
460
|
+
return acc;
|
|
461
|
+
}, {} as Attributes);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function getChatCompletionOutputMessageAttributes(
|
|
465
|
+
message: ChatCompletionMessage,
|
|
466
|
+
): Attributes {
|
|
467
|
+
const role = message.role;
|
|
468
|
+
const attributes: Attributes = {
|
|
469
|
+
[SemanticConventions.MESSAGE_ROLE]: role,
|
|
470
|
+
};
|
|
471
|
+
if (message.content) {
|
|
472
|
+
attributes[SemanticConventions.MESSAGE_CONTENT] = message.content;
|
|
473
|
+
}
|
|
474
|
+
if (message.tool_calls) {
|
|
475
|
+
message.tool_calls.forEach((toolCall, index) => {
|
|
476
|
+
const toolCallIndexPrefix = `${SemanticConventions.MESSAGE_TOOL_CALLS}.${index}.`;
|
|
477
|
+
// Double check that the tool call has a function
|
|
478
|
+
// NB: OpenAI only supports tool calls with functions right now but this may change
|
|
479
|
+
if (toolCall.function) {
|
|
480
|
+
attributes[
|
|
481
|
+
toolCallIndexPrefix + SemanticConventions.TOOL_CALL_FUNCTION_NAME
|
|
482
|
+
] = toolCall.function.name;
|
|
483
|
+
attributes[
|
|
484
|
+
toolCallIndexPrefix +
|
|
485
|
+
SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON
|
|
486
|
+
] = toolCall.function.arguments;
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (message.function_call) {
|
|
491
|
+
attributes[SemanticConventions.MESSAGE_FUNCTION_CALL_NAME] =
|
|
492
|
+
message.function_call.name;
|
|
493
|
+
attributes[SemanticConventions.MESSAGE_FUNCTION_CALL_ARGUMENTS_JSON] =
|
|
494
|
+
message.function_call.arguments;
|
|
495
|
+
}
|
|
496
|
+
return attributes;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Converts the completion result to output attributes
|
|
501
|
+
*/
|
|
502
|
+
function getCompletionOutputValueAndMimeType(
|
|
503
|
+
completion: Completion,
|
|
504
|
+
): Attributes {
|
|
505
|
+
// Right now support just the first choice
|
|
506
|
+
const choice = completion.choices[0];
|
|
507
|
+
if (!choice) {
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
[SemanticConventions.OUTPUT_VALUE]: String(choice.text),
|
|
512
|
+
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.TEXT,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
302
516
|
/**
|
|
303
517
|
* Converts the embedding result payload to embedding attributes
|
|
304
518
|
*/
|
|
305
519
|
function getEmbeddingTextAttributes(
|
|
306
520
|
request: EmbeddingCreateParams,
|
|
307
521
|
): Attributes {
|
|
308
|
-
if (typeof request.input
|
|
522
|
+
if (typeof request.input === "string") {
|
|
309
523
|
return {
|
|
310
524
|
[`${SemanticConventions.EMBEDDING_EMBEDDINGS}.0.${SemanticConventions.EMBEDDING_TEXT}`]:
|
|
311
525
|
request.input,
|
|
@@ -313,11 +527,11 @@ function getEmbeddingTextAttributes(
|
|
|
313
527
|
} else if (
|
|
314
528
|
Array.isArray(request.input) &&
|
|
315
529
|
request.input.length > 0 &&
|
|
316
|
-
typeof request.input[0]
|
|
530
|
+
typeof request.input[0] === "string"
|
|
317
531
|
) {
|
|
318
532
|
return request.input.reduce((acc, input, index) => {
|
|
319
|
-
const
|
|
320
|
-
acc[`${
|
|
533
|
+
const indexPrefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}.`;
|
|
534
|
+
acc[`${indexPrefix}${SemanticConventions.EMBEDDING_TEXT}`] = input;
|
|
321
535
|
return acc;
|
|
322
536
|
}, {} as Attributes);
|
|
323
537
|
}
|
|
@@ -332,9 +546,98 @@ function getEmbeddingEmbeddingsAttributes(
|
|
|
332
546
|
response: CreateEmbeddingResponse,
|
|
333
547
|
): Attributes {
|
|
334
548
|
return response.data.reduce((acc, embedding, index) => {
|
|
335
|
-
const
|
|
336
|
-
acc[`${
|
|
549
|
+
const indexPrefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}.`;
|
|
550
|
+
acc[`${indexPrefix}${SemanticConventions.EMBEDDING_VECTOR}`] =
|
|
337
551
|
embedding.embedding;
|
|
338
552
|
return acc;
|
|
339
553
|
}, {} as Attributes);
|
|
340
554
|
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Consumes the stream chunks and adds them to the span
|
|
558
|
+
*/
|
|
559
|
+
async function consumeChatCompletionStreamChunks(
|
|
560
|
+
stream: Stream<ChatCompletionChunk>,
|
|
561
|
+
span: Span,
|
|
562
|
+
) {
|
|
563
|
+
let streamResponse = "";
|
|
564
|
+
// Tool and function call attributes can also arrive in the stream
|
|
565
|
+
// NB: the tools and function calls arrive in partial diffs
|
|
566
|
+
// So the final tool and function calls need to be aggregated
|
|
567
|
+
// across chunks
|
|
568
|
+
const toolAndFunctionCallAttributes: Attributes = {};
|
|
569
|
+
// The first message is for the assistant response so we start at 1
|
|
570
|
+
for await (const chunk of stream) {
|
|
571
|
+
if (chunk.choices.length <= 0) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const choice = chunk.choices[0];
|
|
575
|
+
if (choice.delta.content) {
|
|
576
|
+
streamResponse += choice.delta.content;
|
|
577
|
+
}
|
|
578
|
+
// Accumulate the tool and function call attributes
|
|
579
|
+
const toolAndFunctionCallAttributesDiff =
|
|
580
|
+
getToolAndFunctionCallAttributesFromStreamChunk(chunk);
|
|
581
|
+
for (const [key, value] of Object.entries(
|
|
582
|
+
toolAndFunctionCallAttributesDiff,
|
|
583
|
+
)) {
|
|
584
|
+
if (isString(toolAndFunctionCallAttributes[key]) && isString(value)) {
|
|
585
|
+
toolAndFunctionCallAttributes[key] += value;
|
|
586
|
+
} else if (isString(value)) {
|
|
587
|
+
toolAndFunctionCallAttributes[key] = value;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const messageIndexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.0.`;
|
|
592
|
+
|
|
593
|
+
// Append the attributes to the span as a message
|
|
594
|
+
const attributes: Attributes = {
|
|
595
|
+
[SemanticConventions.OUTPUT_VALUE]: streamResponse,
|
|
596
|
+
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.TEXT,
|
|
597
|
+
[`${messageIndexPrefix}${SemanticConventions.MESSAGE_CONTENT}`]:
|
|
598
|
+
streamResponse,
|
|
599
|
+
[`${messageIndexPrefix}${SemanticConventions.MESSAGE_ROLE}`]: "assistant",
|
|
600
|
+
};
|
|
601
|
+
// Add the tool and function call attributes
|
|
602
|
+
for (const [key, value] of Object.entries(toolAndFunctionCallAttributes)) {
|
|
603
|
+
attributes[`${messageIndexPrefix}${key}`] = value;
|
|
604
|
+
}
|
|
605
|
+
span.setAttributes(attributes);
|
|
606
|
+
span.end();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Extracts the semantic attributes from the stream chunk for tool_calls and function_calls
|
|
611
|
+
*/
|
|
612
|
+
function getToolAndFunctionCallAttributesFromStreamChunk(
|
|
613
|
+
chunk: ChatCompletionChunk,
|
|
614
|
+
): Attributes {
|
|
615
|
+
if (chunk.choices.length <= 0) {
|
|
616
|
+
return {};
|
|
617
|
+
}
|
|
618
|
+
const choice = chunk.choices[0];
|
|
619
|
+
const attributes: Attributes = {};
|
|
620
|
+
if (choice.delta.tool_calls) {
|
|
621
|
+
choice.delta.tool_calls.forEach((toolCall, index) => {
|
|
622
|
+
const toolCallIndexPrefix = `${SemanticConventions.MESSAGE_TOOL_CALLS}.${index}.`;
|
|
623
|
+
// Double check that the tool call has a function
|
|
624
|
+
// NB: OpenAI only supports tool calls with functions right now but this may change
|
|
625
|
+
if (toolCall.function) {
|
|
626
|
+
attributes[
|
|
627
|
+
toolCallIndexPrefix + SemanticConventions.TOOL_CALL_FUNCTION_NAME
|
|
628
|
+
] = toolCall.function.name;
|
|
629
|
+
attributes[
|
|
630
|
+
toolCallIndexPrefix +
|
|
631
|
+
SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON
|
|
632
|
+
] = toolCall.function.arguments;
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (choice.delta.function_call) {
|
|
637
|
+
attributes[SemanticConventions.MESSAGE_FUNCTION_CALL_NAME] =
|
|
638
|
+
choice.delta.function_call.name;
|
|
639
|
+
attributes[SemanticConventions.MESSAGE_FUNCTION_CALL_ARGUMENTS_JSON] =
|
|
640
|
+
choice.delta.function_call.arguments;
|
|
641
|
+
}
|
|
642
|
+
return attributes;
|
|
643
|
+
}
|
package/src/typeUtils.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function that uses the type system to check if a switch statement is exhaustive.
|
|
3
|
+
* If the switch statement is not exhaustive, there will be a type error caught in typescript
|
|
4
|
+
*
|
|
5
|
+
* See https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript for more details.
|
|
6
|
+
*/
|
|
7
|
+
export function assertUnreachable(_: never): never {
|
|
8
|
+
throw new Error("Unreachable");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isString(value: unknown): value is string {
|
|
12
|
+
return typeof value === "string";
|
|
13
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// this is an auto-generated file. see js/scripts/version-update.js
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.1.1";
|