@arizeai/openinference-instrumentation-openai 4.4.3 → 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.3",
3
+ "version": "4.4.5",
4
4
  "private": false,
5
5
  "description": "OpenInference instrumentation for OpenAI",
6
6
  "keywords": [],
@@ -28,8 +28,8 @@
28
28
  "@opentelemetry/api": "^1.9.0",
29
29
  "@opentelemetry/core": "^2.8.0",
30
30
  "@opentelemetry/instrumentation": "^0.46.0",
31
- "@arizeai/openinference-core": "2.8.1",
32
- "@arizeai/openinference-semantic-conventions": "2.15.0"
31
+ "@arizeai/openinference-core": "2.8.2",
32
+ "@arizeai/openinference-semantic-conventions": "2.16.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@opentelemetry/exporter-trace-otlp-proto": "^0.50.0",
@@ -87,6 +87,7 @@ export const HOST_SUFFIX_TO_PROVIDER: Record<string, LLMProvider> = {
87
87
  "api.minimaxi.com": LLMProvider.MINIMAX,
88
88
  "api.minimax.chat": LLMProvider.MINIMAX,
89
89
  "oci.oraclecloud.com": LLMProvider.ORACLE,
90
+ "ml.cloud.ibm.com": LLMProvider.IBM,
90
91
  };
91
92
 
92
93
  /**
@@ -571,8 +572,17 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
571
572
  // First split the stream via tee
572
573
  const [leftStream, rightStream] = result.tee();
573
574
  // take the right stream, consuming it and then recording the final chunk
574
- // into the span
575
- 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);
576
586
  // give the left stream back to the caller
577
587
  result = leftStream;
578
588
  }
@@ -1000,7 +1010,10 @@ function getEmbeddingUsageAttributes(response: CreateEmbeddingResponse): Attribu
1000
1010
  }
1001
1011
 
1002
1012
  /**
1003
- * 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.
1004
1017
  */
1005
1018
  async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionChunk>, span: Span) {
1006
1019
  let streamResponse = "";
@@ -1009,34 +1022,44 @@ async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionCh
1009
1022
  // So the final tool and function calls need to be aggregated
1010
1023
  // across chunks
1011
1024
  const toolAndFunctionCallAttributes: Attributes = {};
1025
+ const getToolCallIndex = createStreamToolCallIndexResolver();
1012
1026
  let finishReason: string | undefined;
1013
- // The first message is for the assistant response so we start at 1
1014
- for await (const chunk of stream) {
1015
- // With `stream_options.include_usage`, the final chunk carries the request's
1016
- // usage and an empty `choices` array, so read it before the guard below.
1017
- if (chunk.usage) {
1018
- span.setAttributes(getUsageAttributes(chunk));
1019
- }
1020
- if (chunk.choices.length <= 0) {
1021
- continue;
1022
- }
1023
- const choice = chunk.choices[0];
1024
- if (choice.delta.content) {
1025
- streamResponse += choice.delta.content;
1026
- }
1027
- if (choice.finish_reason) {
1028
- finishReason = choice.finish_reason;
1029
- }
1030
- // Accumulate the tool and function call attributes
1031
- const toolAndFunctionCallAttributesDiff =
1032
- getToolAndFunctionCallAttributesFromStreamChunk(chunk);
1033
- for (const [key, value] of Object.entries(toolAndFunctionCallAttributesDiff)) {
1034
- if (isString(toolAndFunctionCallAttributes[key]) && isString(value)) {
1035
- toolAndFunctionCallAttributes[key] += value;
1036
- } else if (isString(value)) {
1037
- 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
+ }
1038
1057
  }
1039
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;
1040
1063
  }
1041
1064
  const messageIndexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.0.`;
1042
1065
 
@@ -1055,20 +1078,59 @@ async function consumeChatCompletionStreamChunks(stream: Stream<ChatCompletionCh
1055
1078
  attributes[`${messageIndexPrefix}${key}`] = value;
1056
1079
  }
1057
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
+ }
1058
1089
  span.end();
1059
1090
  }
1060
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
+
1061
1119
  /**
1062
1120
  * Extracts the semantic attributes from the stream chunk for tool_calls and function_calls
1063
1121
  */
1064
- function getToolAndFunctionCallAttributesFromStreamChunk(chunk: ChatCompletionChunk): Attributes {
1122
+ function getToolAndFunctionCallAttributesFromStreamChunk(
1123
+ chunk: ChatCompletionChunk,
1124
+ getToolCallIndex: (toolCall: ChatCompletionChunk.Choice.Delta.ToolCall) => number,
1125
+ ): Attributes {
1065
1126
  if (chunk.choices.length <= 0) {
1066
1127
  return {};
1067
1128
  }
1068
1129
  const choice = chunk.choices[0];
1069
1130
  const attributes: Attributes = {};
1070
1131
  if (choice.delta.tool_calls) {
1071
- choice.delta.tool_calls.forEach((toolCall, index) => {
1132
+ choice.delta.tool_calls.forEach((toolCall) => {
1133
+ const index = getToolCallIndex(toolCall);
1072
1134
  const toolCallIndexPrefix = `${SemanticConventions.MESSAGE_TOOL_CALLS}.${index}.`;
1073
1135
  // Add the tool call id if it exists
1074
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.3";
2
+ export const VERSION = "4.4.5";