@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.
Files changed (36) hide show
  1. package/dist/esm/instrumentation.d.ts.map +1 -1
  2. package/dist/esm/instrumentation.js +438 -44
  3. package/dist/esm/instrumentation.js.map +1 -1
  4. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  5. package/dist/esm/typeUtils.d.ts +9 -0
  6. package/dist/esm/typeUtils.d.ts.map +1 -0
  7. package/dist/esm/typeUtils.js +13 -0
  8. package/dist/esm/typeUtils.js.map +1 -0
  9. package/dist/esm/version.d.ts +1 -1
  10. package/dist/esm/version.js +1 -1
  11. package/dist/esnext/instrumentation.d.ts.map +1 -1
  12. package/dist/esnext/instrumentation.js +300 -43
  13. package/dist/esnext/instrumentation.js.map +1 -1
  14. package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
  15. package/dist/esnext/typeUtils.d.ts +9 -0
  16. package/dist/esnext/typeUtils.d.ts.map +1 -0
  17. package/dist/esnext/typeUtils.js +13 -0
  18. package/dist/esnext/typeUtils.js.map +1 -0
  19. package/dist/esnext/version.d.ts +1 -1
  20. package/dist/esnext/version.js +1 -1
  21. package/dist/src/instrumentation.d.ts.map +1 -1
  22. package/dist/src/instrumentation.js +312 -43
  23. package/dist/src/instrumentation.js.map +1 -1
  24. package/dist/src/typeUtils.d.ts +9 -0
  25. package/dist/src/typeUtils.d.ts.map +1 -0
  26. package/dist/src/typeUtils.js +18 -0
  27. package/dist/src/typeUtils.js.map +1 -0
  28. package/dist/src/version.d.ts +1 -1
  29. package/dist/src/version.js +1 -1
  30. package/dist/test/openai.test.js +607 -1
  31. package/dist/test/openai.test.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +2 -2
  34. package/src/instrumentation.ts +368 -65
  35. package/src/typeUtils.ts +13 -0
  36. package/src/version.ts +1 -1
@@ -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
- type CompletionCreateType =
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: CompletionCreateType): any => {
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<CompletionCreateType>
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]: isChatCompletionResponse(
129
- result,
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<typeof module.OpenAI.Embeddings.prototype.create>
233
+ ...args: Parameters<EmbeddingsCreateType>
157
234
  ) {
158
235
  const body = args[0];
159
236
  const { input } = body;
160
- const isStringInput = typeof input == "string";
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
- * Converts the body of the request to LLM input messages
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 index_prefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}`;
244
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
245
- message.content,
246
- );
247
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = String(
248
- message.role,
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
- * Get Usage attributes
399
+ * Converts the body of a completions request to input attributes
256
400
  */
257
- function getUsageAttributes(
258
- response: Stream<ChatCompletionChunk> | ChatCompletion,
259
- ) {
260
- if (Object.prototype.hasOwnProperty.call(response, "usage")) {
261
- const completion = response as ChatCompletion;
262
- if (completion.usage) {
263
- return {
264
- [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
265
- completion.usage.completion_tokens,
266
- [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]:
267
- completion.usage.prompt_tokens,
268
- [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]:
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
- * Converts the result to LLM output attributes
424
+ * Get usage attributes
278
425
  */
279
- function getLLMOutputMessagesAttributes(
280
- response: Stream<ChatCompletionChunk> | ChatCompletion,
426
+ function getUsageAttributes(
427
+ completion: ChatCompletion | Completion,
281
428
  ): Attributes {
282
- // Handle chat completion
283
- if (Object.prototype.hasOwnProperty.call(response, "choices")) {
284
- const completion = response as ChatCompletion;
285
- // Right now support just the first choice
286
- const choice = completion.choices[0];
287
- if (!choice) {
288
- return {};
289
- }
290
- return [choice.message].reduce((acc, message, index) => {
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 == "string") {
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] == "string"
530
+ typeof request.input[0] === "string"
317
531
  ) {
318
532
  return request.input.reduce((acc, input, index) => {
319
- const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`;
320
- acc[`${index_prefix}.${SemanticConventions.EMBEDDING_TEXT}`] = input;
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 index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`;
336
- acc[`${index_prefix}.${SemanticConventions.EMBEDDING_VECTOR}`] =
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
+ }
@@ -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.0.4";
2
+ export const VERSION = "0.1.1";