@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.
Files changed (42) hide show
  1. package/dist/esm/index.d.ts +1 -1
  2. package/dist/esm/index.js +1 -1
  3. package/dist/esm/instrumentation.d.ts +13 -19
  4. package/dist/esm/instrumentation.d.ts.map +1 -1
  5. package/dist/esm/instrumentation.js +451 -30
  6. package/dist/esm/instrumentation.js.map +1 -1
  7. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  8. package/dist/esm/typeUtils.d.ts +8 -0
  9. package/dist/esm/typeUtils.d.ts.map +1 -0
  10. package/dist/esm/typeUtils.js +10 -0
  11. package/dist/esm/typeUtils.js.map +1 -0
  12. package/dist/esm/version.d.ts +2 -2
  13. package/dist/esm/version.js +2 -2
  14. package/dist/esnext/index.d.ts +1 -1
  15. package/dist/esnext/index.js +1 -1
  16. package/dist/esnext/instrumentation.d.ts +13 -19
  17. package/dist/esnext/instrumentation.d.ts.map +1 -1
  18. package/dist/esnext/instrumentation.js +321 -29
  19. package/dist/esnext/instrumentation.js.map +1 -1
  20. package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
  21. package/dist/esnext/typeUtils.d.ts +8 -0
  22. package/dist/esnext/typeUtils.d.ts.map +1 -0
  23. package/dist/esnext/typeUtils.js +10 -0
  24. package/dist/esnext/typeUtils.js.map +1 -0
  25. package/dist/esnext/version.d.ts +2 -2
  26. package/dist/esnext/version.js +2 -2
  27. package/dist/src/instrumentation.d.ts.map +1 -1
  28. package/dist/src/instrumentation.js +333 -29
  29. package/dist/src/instrumentation.js.map +1 -1
  30. package/dist/src/typeUtils.d.ts +8 -0
  31. package/dist/src/typeUtils.d.ts.map +1 -0
  32. package/dist/src/typeUtils.js +14 -0
  33. package/dist/src/typeUtils.js.map +1 -0
  34. package/dist/src/version.d.ts +1 -1
  35. package/dist/src/version.js +1 -1
  36. package/dist/test/openai.test.js +387 -6
  37. package/dist/test/openai.test.js.map +1 -1
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +6 -2
  40. package/src/instrumentation.ts +420 -48
  41. package/src/typeUtils.ts +9 -0
  42. 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",
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.12"
17
+ "@arizeai/openinference-semantic-conventions": "0.1.0"
14
18
  },
15
19
  "keywords": [],
16
20
  "files": [
@@ -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
- type CompletionCreateType =
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: CompletionCreateType): any => {
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.CLIENT,
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<CompletionCreateType>
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
- ...getLLMOutputMessagesAttributes(result),
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
- * Converts the body of the request to LLM input messages
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 index_prefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}`;
150
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
151
- message.content,
152
- );
153
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = String(
154
- message.role,
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
- * Get Usage attributes
399
+ * Converts the body of a completions request to input attributes
162
400
  */
163
- function getUsageAttributes(
164
- response: Stream<ChatCompletionChunk> | ChatCompletion,
165
- ) {
166
- if (Object.prototype.hasOwnProperty.call(response, "usage")) {
167
- const completion = response as ChatCompletion;
168
- if (completion.usage) {
169
- return {
170
- [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
171
- completion.usage.completion_tokens,
172
- [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]:
173
- completion.usage.prompt_tokens,
174
- [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]:
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
- * Converts the result to LLM output attributes
424
+ * Get usage attributes
184
425
  */
185
- function getLLMOutputMessagesAttributes(
186
- response: Stream<ChatCompletionChunk> | ChatCompletion,
426
+ function getUsageAttributes(
427
+ completion: ChatCompletion | Completion,
187
428
  ): Attributes {
188
- // Handle chat completion
189
- if (Object.prototype.hasOwnProperty.call(response, "choices")) {
190
- const completion = response as ChatCompletion;
191
- // Right now support just the first choice
192
- const choice = completion.choices[0];
193
- if (!choice) {
194
- return {};
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 [choice.message].reduce((acc, message, index) => {
197
- const index_prefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
198
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
199
- message.content,
200
- );
201
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = message.role;
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
+ }
@@ -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";
2
+ export const VERSION = "0.1.0";