@arizeai/openinference-instrumentation-openai 4.4.4 → 4.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arizeai/openinference-instrumentation-openai",
3
- "version": "4.4.4",
3
+ "version": "4.4.5",
4
4
  "private": false,
5
5
  "description": "OpenInference instrumentation for OpenAI",
6
6
  "keywords": [],
@@ -572,8 +572,17 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
572
572
  // First split the stream via tee
573
573
  const [leftStream, rightStream] = result.tee();
574
574
  // take the right stream, consuming it and then recording the final chunk
575
- // into the span
576
- void consumeResponseStreamEvents(rightStream).then(recordSpan);
575
+ // into the span. If reading it fails, the caller gets the same error from
576
+ // the left stream and the span ends with status ERROR.
577
+ void consumeResponseStreamEvents(rightStream).then((response) => {
578
+ // An aborted request ends the stream without an error or a final
579
+ // response, so leave the status unset
580
+ if (!response && rightStream.controller.signal.aborted) {
581
+ span.end();
582
+ return;
583
+ }
584
+ recordSpan(response);
585
+ }, recordError);
577
586
  // give the left stream back to the caller
578
587
  result = leftStream;
579
588
  }
@@ -1001,7 +1010,10 @@ function getEmbeddingUsageAttributes(response: CreateEmbeddingResponse): Attribu
1001
1010
  }
1002
1011
 
1003
1012
  /**
1004
- * Consumes the stream chunks and adds them to the span
1013
+ * Consumes the stream chunks and adds them to the span.
1014
+ * The span ends with status OK when the stream is read to the end, with status ERROR when
1015
+ * reading it fails, and with the status unset when the request is aborted. In every case it
1016
+ * keeps what was streamed so far.
1005
1017
  */
1006
1018
  async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionChunk>, span: Span) {
1007
1019
  let streamResponse = "";
@@ -1010,34 +1022,44 @@ async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionCh
1010
1022
  // So the final tool and function calls need to be aggregated
1011
1023
  // across chunks
1012
1024
  const toolAndFunctionCallAttributes: Attributes = {};
1025
+ const getToolCallIndex = createStreamToolCallIndexResolver();
1013
1026
  let finishReason: string | undefined;
1014
- // The first message is for the assistant response so we start at 1
1015
- for await (const chunk of stream) {
1016
- // With `stream_options.include_usage`, the final chunk carries the request's
1017
- // usage and an empty `choices` array, so read it before the guard below.
1018
- if (chunk.usage) {
1019
- span.setAttributes(getUsageAttributes(chunk));
1020
- }
1021
- if (chunk.choices.length <= 0) {
1022
- continue;
1023
- }
1024
- const choice = chunk.choices[0];
1025
- if (choice.delta.content) {
1026
- streamResponse += choice.delta.content;
1027
- }
1028
- if (choice.finish_reason) {
1029
- finishReason = choice.finish_reason;
1030
- }
1031
- // Accumulate the tool and function call attributes
1032
- const toolAndFunctionCallAttributesDiff =
1033
- getToolAndFunctionCallAttributesFromStreamChunk(chunk);
1034
- for (const [key, value] of Object.entries(toolAndFunctionCallAttributesDiff)) {
1035
- if (isString(toolAndFunctionCallAttributes[key]) && isString(value)) {
1036
- toolAndFunctionCallAttributes[key] += value;
1037
- } else if (isString(value)) {
1038
- toolAndFunctionCallAttributes[key] = value;
1027
+ let streamFailed = false;
1028
+ let streamError: unknown;
1029
+ try {
1030
+ for await (const chunk of stream) {
1031
+ // With `stream_options.include_usage`, the final chunk carries the request's
1032
+ // usage and an empty `choices` array, so read it before the guard below.
1033
+ if (chunk.usage) {
1034
+ span.setAttributes(getUsageAttributes(chunk));
1035
+ }
1036
+ if (chunk.choices.length <= 0) {
1037
+ continue;
1038
+ }
1039
+ const choice = chunk.choices[0];
1040
+ if (choice.delta.content) {
1041
+ streamResponse += choice.delta.content;
1042
+ }
1043
+ if (choice.finish_reason) {
1044
+ finishReason = choice.finish_reason;
1045
+ }
1046
+ // Accumulate the tool and function call attributes
1047
+ const toolAndFunctionCallAttributesDiff = getToolAndFunctionCallAttributesFromStreamChunk(
1048
+ chunk,
1049
+ getToolCallIndex,
1050
+ );
1051
+ for (const [key, value] of Object.entries(toolAndFunctionCallAttributesDiff)) {
1052
+ if (isString(toolAndFunctionCallAttributes[key]) && isString(value)) {
1053
+ toolAndFunctionCallAttributes[key] += value;
1054
+ } else if (isString(value)) {
1055
+ toolAndFunctionCallAttributes[key] = value;
1056
+ }
1039
1057
  }
1040
1058
  }
1059
+ } catch (error) {
1060
+ // The caller reads the other branch of the tee and gets the same error
1061
+ streamFailed = true;
1062
+ streamError = error;
1041
1063
  }
1042
1064
  const messageIndexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.0.`;
1043
1065
 
@@ -1056,20 +1078,59 @@ async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionCh
1056
1078
  attributes[`${messageIndexPrefix}${key}`] = value;
1057
1079
  }
1058
1080
  span.setAttributes(attributes);
1081
+ if (streamFailed) {
1082
+ endSpanWithError(span, streamError);
1083
+ return;
1084
+ }
1085
+ // An aborted request ends the stream without an error, but did not run to completion
1086
+ if (!stream.controller.signal.aborted) {
1087
+ span.setStatus({ code: SpanStatusCode.OK });
1088
+ }
1059
1089
  span.end();
1060
1090
  }
1061
1091
 
1092
+ /**
1093
+ * Returns a function that gives the position of a streamed tool call delta within the message.
1094
+ * OpenAI sends an index with every delta. Some OpenAI-compatible providers (such as Gemini)
1095
+ * leave it out, so a delta with a new id, or a function name without an id, starts the next
1096
+ * tool call and any other delta continues the latest one.
1097
+ */
1098
+ function createStreamToolCallIndexResolver() {
1099
+ let latestIndex = -1;
1100
+ let latestId: string | undefined;
1101
+ let nextIndex = 0;
1102
+ return (toolCall: ChatCompletionChunk.Choice.Delta.ToolCall): number => {
1103
+ // The SDK types the index as required, but some providers omit it
1104
+ if (typeof toolCall.index === "number") {
1105
+ latestIndex = toolCall.index;
1106
+ nextIndex = Math.max(nextIndex, toolCall.index + 1);
1107
+ return toolCall.index;
1108
+ }
1109
+ const startsNewToolCall =
1110
+ latestIndex < 0 || (toolCall.id ? toolCall.id !== latestId : toolCall.function?.name != null);
1111
+ if (startsNewToolCall) {
1112
+ latestIndex = nextIndex++;
1113
+ latestId = toolCall.id;
1114
+ }
1115
+ return latestIndex;
1116
+ };
1117
+ }
1118
+
1062
1119
  /**
1063
1120
  * Extracts the semantic attributes from the stream chunk for tool_calls and function_calls
1064
1121
  */
1065
- function getToolAndFunctionCallAttributesFromStreamChunk(chunk: ChatCompletionChunk): Attributes {
1122
+ function getToolAndFunctionCallAttributesFromStreamChunk(
1123
+ chunk: ChatCompletionChunk,
1124
+ getToolCallIndex: (toolCall: ChatCompletionChunk.Choice.Delta.ToolCall) => number,
1125
+ ): Attributes {
1066
1126
  if (chunk.choices.length <= 0) {
1067
1127
  return {};
1068
1128
  }
1069
1129
  const choice = chunk.choices[0];
1070
1130
  const attributes: Attributes = {};
1071
1131
  if (choice.delta.tool_calls) {
1072
- choice.delta.tool_calls.forEach((toolCall, index) => {
1132
+ choice.delta.tool_calls.forEach((toolCall) => {
1133
+ const index = getToolCallIndex(toolCall);
1073
1134
  const toolCallIndexPrefix = `${SemanticConventions.MESSAGE_TOOL_CALLS}.${index}.`;
1074
1135
  // Add the tool call id if it exists
1075
1136
  if (toolCall.id) {
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 = "4.4.4";
2
+ export const VERSION = "4.4.5";