@arizeai/openinference-instrumentation-openai 0.0.3 → 0.1.0
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/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/instrumentation.d.ts +13 -19
- package/dist/esm/instrumentation.d.ts.map +1 -1
- package/dist/esm/instrumentation.js +451 -30
- package/dist/esm/instrumentation.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/typeUtils.d.ts +8 -0
- package/dist/esm/typeUtils.d.ts.map +1 -0
- package/dist/esm/typeUtils.js +10 -0
- package/dist/esm/typeUtils.js.map +1 -0
- package/dist/esm/version.d.ts +2 -2
- package/dist/esm/version.js +2 -2
- package/dist/esnext/index.d.ts +1 -1
- package/dist/esnext/index.js +1 -1
- package/dist/esnext/instrumentation.d.ts +13 -19
- package/dist/esnext/instrumentation.d.ts.map +1 -1
- package/dist/esnext/instrumentation.js +321 -29
- package/dist/esnext/instrumentation.js.map +1 -1
- package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
- package/dist/esnext/typeUtils.d.ts +8 -0
- package/dist/esnext/typeUtils.d.ts.map +1 -0
- package/dist/esnext/typeUtils.js +10 -0
- package/dist/esnext/typeUtils.js.map +1 -0
- package/dist/esnext/version.d.ts +2 -2
- package/dist/esnext/version.js +2 -2
- package/dist/src/instrumentation.d.ts.map +1 -1
- package/dist/src/instrumentation.js +333 -29
- package/dist/src/instrumentation.js.map +1 -1
- package/dist/src/typeUtils.d.ts +8 -0
- package/dist/src/typeUtils.d.ts.map +1 -0
- package/dist/src/typeUtils.js +14 -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 +387 -6
- package/dist/test/openai.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -2
- package/src/instrumentation.ts +420 -48
- package/src/typeUtils.ts +9 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arizeai/openinference-instrumentation-openai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "OpenInference instrumentation for OpenAI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/src/index.js",
|
|
7
7
|
"module": "dist/esm/index.js",
|
|
8
8
|
"esnext": "dist/esnext/index.js",
|
|
9
9
|
"types": "dist/src/index.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/Arize-ai/openinference.git"
|
|
13
|
+
},
|
|
10
14
|
"dependencies": {
|
|
11
15
|
"@opentelemetry/api": "^1.7.0",
|
|
12
16
|
"@opentelemetry/instrumentation": "^0.46.0",
|
|
13
|
-
"@arizeai/openinference-semantic-conventions": "0.0
|
|
17
|
+
"@arizeai/openinference-semantic-conventions": "0.1.0"
|
|
14
18
|
},
|
|
15
19
|
"keywords": [],
|
|
16
20
|
"files": [
|
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,8 +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";
|
|
33
|
+
import {
|
|
34
|
+
Completion,
|
|
35
|
+
CreateEmbeddingResponse,
|
|
36
|
+
EmbeddingCreateParams,
|
|
37
|
+
} from "openai/resources";
|
|
38
|
+
import { assertUnreachable } from "./typeUtils";
|
|
29
39
|
|
|
30
40
|
const MODULE_NAME = "openai";
|
|
31
41
|
|
|
@@ -60,7 +70,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
60
70
|
}
|
|
61
71
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
62
72
|
const instrumentation: OpenAIInstrumentation = this;
|
|
63
|
-
|
|
73
|
+
|
|
74
|
+
type ChatCompletionCreateType =
|
|
64
75
|
typeof module.OpenAI.Chat.Completions.prototype.create;
|
|
65
76
|
|
|
66
77
|
// Patch create chat completions
|
|
@@ -68,19 +79,17 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
68
79
|
module.OpenAI.Chat.Completions.prototype,
|
|
69
80
|
"create",
|
|
70
81
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
-
(original:
|
|
82
|
+
(original: ChatCompletionCreateType): any => {
|
|
72
83
|
return function patchedCreate(
|
|
73
84
|
this: unknown,
|
|
74
|
-
...args: Parameters<
|
|
75
|
-
typeof module.OpenAI.Chat.Completions.prototype.create
|
|
76
|
-
>
|
|
85
|
+
...args: Parameters<ChatCompletionCreateType>
|
|
77
86
|
) {
|
|
78
87
|
const body = args[0];
|
|
79
88
|
const { messages: _messages, ...invocationParameters } = body;
|
|
80
89
|
const span = instrumentation.tracer.startSpan(
|
|
81
90
|
`OpenAI Chat Completions`,
|
|
82
91
|
{
|
|
83
|
-
kind: SpanKind.
|
|
92
|
+
kind: SpanKind.INTERNAL,
|
|
84
93
|
attributes: {
|
|
85
94
|
[SemanticConventions.OPENINFERENCE_SPAN_KIND]:
|
|
86
95
|
OpenInferenceSpanKind.LLM,
|
|
@@ -95,7 +104,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
95
104
|
);
|
|
96
105
|
const execContext = trace.setSpan(context.active(), span);
|
|
97
106
|
const execPromise = safeExecuteInTheMiddle<
|
|
98
|
-
ReturnType<
|
|
107
|
+
ReturnType<ChatCompletionCreateType>
|
|
99
108
|
>(
|
|
100
109
|
() => {
|
|
101
110
|
return context.with(execContext, () => {
|
|
@@ -106,18 +115,169 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
106
115
|
// Push the error to the span
|
|
107
116
|
if (error) {
|
|
108
117
|
span.recordException(error);
|
|
118
|
+
span.setStatus({
|
|
119
|
+
code: SpanStatusCode.ERROR,
|
|
120
|
+
message: error.message,
|
|
121
|
+
});
|
|
122
|
+
span.end();
|
|
109
123
|
}
|
|
110
124
|
},
|
|
111
125
|
);
|
|
112
126
|
const wrappedPromise = execPromise.then((result) => {
|
|
113
|
-
if (result) {
|
|
127
|
+
if (isChatCompletionResponse(result)) {
|
|
128
|
+
// Record the results
|
|
129
|
+
span.setAttributes({
|
|
130
|
+
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
|
|
131
|
+
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
|
|
132
|
+
// Override the model from the value sent by the server
|
|
133
|
+
[SemanticConventions.LLM_MODEL_NAME]: result.model,
|
|
134
|
+
...getChatCompletionLLMOutputMessagesAttributes(result),
|
|
135
|
+
...getUsageAttributes(result),
|
|
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)) {
|
|
114
204
|
// Record the results
|
|
115
205
|
span.setAttributes({
|
|
116
206
|
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
|
|
117
207
|
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
|
|
118
|
-
|
|
208
|
+
// Override the model from the value sent by the server
|
|
209
|
+
[SemanticConventions.LLM_MODEL_NAME]: result.model,
|
|
210
|
+
...getCompletionOutputValueAndMimeType(result),
|
|
119
211
|
...getUsageAttributes(result),
|
|
120
212
|
});
|
|
213
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
214
|
+
span.end();
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
});
|
|
218
|
+
return context.bind(execContext, wrappedPromise);
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Patch embeddings
|
|
224
|
+
type EmbeddingsCreateType =
|
|
225
|
+
typeof module.OpenAI.Embeddings.prototype.create;
|
|
226
|
+
this._wrap(
|
|
227
|
+
module.OpenAI.Embeddings.prototype,
|
|
228
|
+
"create",
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
(original: EmbeddingsCreateType): any => {
|
|
231
|
+
return function patchedEmbeddingCreate(
|
|
232
|
+
this: unknown,
|
|
233
|
+
...args: Parameters<EmbeddingsCreateType>
|
|
234
|
+
) {
|
|
235
|
+
const body = args[0];
|
|
236
|
+
const { input } = body;
|
|
237
|
+
const isStringInput = typeof input === "string";
|
|
238
|
+
const span = instrumentation.tracer.startSpan(`OpenAI Embeddings`, {
|
|
239
|
+
kind: SpanKind.INTERNAL,
|
|
240
|
+
attributes: {
|
|
241
|
+
[SemanticConventions.OPENINFERENCE_SPAN_KIND]:
|
|
242
|
+
OpenInferenceSpanKind.EMBEDDING,
|
|
243
|
+
[SemanticConventions.EMBEDDING_MODEL_NAME]: body.model,
|
|
244
|
+
[SemanticConventions.INPUT_VALUE]: isStringInput
|
|
245
|
+
? input
|
|
246
|
+
: JSON.stringify(input),
|
|
247
|
+
[SemanticConventions.INPUT_MIME_TYPE]: isStringInput
|
|
248
|
+
? MimeType.TEXT
|
|
249
|
+
: MimeType.JSON,
|
|
250
|
+
...getEmbeddingTextAttributes(body),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const execContext = trace.setSpan(context.active(), span);
|
|
254
|
+
const execPromise = safeExecuteInTheMiddle<
|
|
255
|
+
ReturnType<EmbeddingsCreateType>
|
|
256
|
+
>(
|
|
257
|
+
() => {
|
|
258
|
+
return context.with(execContext, () => {
|
|
259
|
+
return original.apply(this, args);
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
(error) => {
|
|
263
|
+
// Push the error to the span
|
|
264
|
+
if (error) {
|
|
265
|
+
span.recordException(error);
|
|
266
|
+
span.setStatus({
|
|
267
|
+
code: SpanStatusCode.ERROR,
|
|
268
|
+
message: error.message,
|
|
269
|
+
});
|
|
270
|
+
span.end();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
const wrappedPromise = execPromise.then((result) => {
|
|
275
|
+
if (result) {
|
|
276
|
+
// Record the results
|
|
277
|
+
span.setAttributes({
|
|
278
|
+
// Do not record the output data as it can be large
|
|
279
|
+
...getEmbeddingEmbeddingsAttributes(result),
|
|
280
|
+
});
|
|
121
281
|
}
|
|
122
282
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
123
283
|
span.end();
|
|
@@ -127,6 +287,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
127
287
|
};
|
|
128
288
|
},
|
|
129
289
|
);
|
|
290
|
+
|
|
130
291
|
module.openInferencePatched = true;
|
|
131
292
|
return module;
|
|
132
293
|
}
|
|
@@ -136,71 +297,282 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
136
297
|
private unpatch(moduleExports: typeof openai, moduleVersion?: string) {
|
|
137
298
|
diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`);
|
|
138
299
|
this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
|
|
300
|
+
this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create");
|
|
139
301
|
}
|
|
140
302
|
}
|
|
141
303
|
|
|
142
304
|
/**
|
|
143
|
-
*
|
|
305
|
+
* type-guard that checks if the response is a chat completion response
|
|
306
|
+
*/
|
|
307
|
+
function isChatCompletionResponse(
|
|
308
|
+
response: Stream<ChatCompletionChunk> | ChatCompletion,
|
|
309
|
+
): response is ChatCompletion {
|
|
310
|
+
return "choices" in response;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
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
|
|
144
335
|
*/
|
|
145
336
|
function getLLMInputMessagesAttributes(
|
|
146
337
|
body: ChatCompletionCreateParamsBase,
|
|
147
338
|
): Attributes {
|
|
148
339
|
return body.messages.reduce((acc, message, index) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
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
|
+
}
|
|
156
346
|
return acc;
|
|
157
347
|
}, {} as Attributes);
|
|
158
348
|
}
|
|
159
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
|
+
|
|
160
398
|
/**
|
|
161
|
-
*
|
|
399
|
+
* Converts the body of a completions request to input attributes
|
|
162
400
|
*/
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
) {
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
completion.usage.total_tokens,
|
|
176
|
-
};
|
|
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 {};
|
|
177
413
|
}
|
|
414
|
+
return {
|
|
415
|
+
[SemanticConventions.INPUT_VALUE]: prompt,
|
|
416
|
+
[SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
|
|
417
|
+
};
|
|
178
418
|
}
|
|
419
|
+
// Other cases in which the prompt is a token or array of tokens are currently unsupported
|
|
179
420
|
return {};
|
|
180
421
|
}
|
|
181
422
|
|
|
182
423
|
/**
|
|
183
|
-
*
|
|
424
|
+
* Get usage attributes
|
|
184
425
|
*/
|
|
185
|
-
function
|
|
186
|
-
|
|
426
|
+
function getUsageAttributes(
|
|
427
|
+
completion: ChatCompletion | Completion,
|
|
187
428
|
): Attributes {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
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;
|
|
195
459
|
}
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Converts the embedding result payload to embedding attributes
|
|
518
|
+
*/
|
|
519
|
+
function getEmbeddingTextAttributes(
|
|
520
|
+
request: EmbeddingCreateParams,
|
|
521
|
+
): Attributes {
|
|
522
|
+
if (typeof request.input === "string") {
|
|
523
|
+
return {
|
|
524
|
+
[`${SemanticConventions.EMBEDDING_EMBEDDINGS}.0.${SemanticConventions.EMBEDDING_TEXT}`]:
|
|
525
|
+
request.input,
|
|
526
|
+
};
|
|
527
|
+
} else if (
|
|
528
|
+
Array.isArray(request.input) &&
|
|
529
|
+
request.input.length > 0 &&
|
|
530
|
+
typeof request.input[0] === "string"
|
|
531
|
+
) {
|
|
532
|
+
return request.input.reduce((acc, input, index) => {
|
|
533
|
+
const indexPrefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}.`;
|
|
534
|
+
acc[`${indexPrefix}${SemanticConventions.EMBEDDING_TEXT}`] = input;
|
|
202
535
|
return acc;
|
|
203
536
|
}, {} as Attributes);
|
|
204
537
|
}
|
|
538
|
+
// Ignore other cases where input is a number or an array of numbers
|
|
205
539
|
return {};
|
|
206
540
|
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Converts the embedding result payload to embedding attributes
|
|
544
|
+
*/
|
|
545
|
+
function getEmbeddingEmbeddingsAttributes(
|
|
546
|
+
response: CreateEmbeddingResponse,
|
|
547
|
+
): Attributes {
|
|
548
|
+
return response.data.reduce((acc, embedding, index) => {
|
|
549
|
+
const indexPrefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}.`;
|
|
550
|
+
acc[`${indexPrefix}${SemanticConventions.EMBEDDING_VECTOR}`] =
|
|
551
|
+
embedding.embedding;
|
|
552
|
+
return acc;
|
|
553
|
+
}, {} as Attributes);
|
|
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
|
+
for await (const chunk of stream) {
|
|
565
|
+
if (chunk.choices.length > 0 && chunk.choices[0].delta.content) {
|
|
566
|
+
streamResponse += chunk.choices[0].delta.content;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
span.setAttributes({
|
|
570
|
+
[SemanticConventions.OUTPUT_VALUE]: streamResponse,
|
|
571
|
+
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.TEXT,
|
|
572
|
+
[`${SemanticConventions.LLM_OUTPUT_MESSAGES}.0.${SemanticConventions.MESSAGE_CONTENT}`]:
|
|
573
|
+
streamResponse,
|
|
574
|
+
[`${SemanticConventions.LLM_OUTPUT_MESSAGES}.0.${SemanticConventions.MESSAGE_ROLE}`]:
|
|
575
|
+
"assistant",
|
|
576
|
+
});
|
|
577
|
+
span.end();
|
|
578
|
+
}
|
package/src/typeUtils.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
}
|
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.0
|
|
2
|
+
export const VERSION = "0.1.0";
|