@arizeai/openinference-instrumentation-openai 2.2.0 → 2.3.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 (35) hide show
  1. package/README.md +11 -0
  2. package/dist/esm/instrumentation.d.ts.map +1 -1
  3. package/dist/esm/instrumentation.js +80 -1
  4. package/dist/esm/instrumentation.js.map +1 -1
  5. package/dist/esm/responsesAttributes.d.ts +8 -0
  6. package/dist/esm/responsesAttributes.d.ts.map +1 -0
  7. package/dist/esm/responsesAttributes.js +222 -0
  8. package/dist/esm/responsesAttributes.js.map +1 -0
  9. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  10. package/dist/esm/version.d.ts +1 -1
  11. package/dist/esm/version.js +1 -1
  12. package/dist/esnext/instrumentation.d.ts.map +1 -1
  13. package/dist/esnext/instrumentation.js +65 -1
  14. package/dist/esnext/instrumentation.js.map +1 -1
  15. package/dist/esnext/responsesAttributes.d.ts +8 -0
  16. package/dist/esnext/responsesAttributes.d.ts.map +1 -0
  17. package/dist/esnext/responsesAttributes.js +243 -0
  18. package/dist/esnext/responsesAttributes.js.map +1 -0
  19. package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
  20. package/dist/esnext/version.d.ts +1 -1
  21. package/dist/esnext/version.js +1 -1
  22. package/dist/src/instrumentation.d.ts.map +1 -1
  23. package/dist/src/instrumentation.js +65 -1
  24. package/dist/src/instrumentation.js.map +1 -1
  25. package/dist/src/responsesAttributes.d.ts +8 -0
  26. package/dist/src/responsesAttributes.d.ts.map +1 -0
  27. package/dist/src/responsesAttributes.js +261 -0
  28. package/dist/src/responsesAttributes.js.map +1 -0
  29. package/dist/src/version.d.ts +1 -1
  30. package/dist/src/version.js +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +4 -4
  33. package/src/instrumentation.ts +116 -2
  34. package/src/responsesAttributes.ts +286 -0
  35. package/src/version.ts +1 -1
@@ -45,6 +45,18 @@ import {
45
45
  safelyJSONStringify,
46
46
  TraceConfigOptions,
47
47
  } from "@arizeai/openinference-core";
48
+ import {
49
+ ResponseCreateParamsBase,
50
+ ResponseStreamEvent,
51
+ Response as ResponseType,
52
+ } from "openai/resources/responses/responses";
53
+
54
+ import {
55
+ consumeResponseStreamEvents,
56
+ getResponsesInputMessagesAttributes,
57
+ getResponsesOutputMessagesAttributes,
58
+ getResponsesUsageAttributes,
59
+ } from "./responsesAttributes";
48
60
 
49
61
  const MODULE_NAME = "openai";
50
62
 
@@ -141,10 +153,10 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
141
153
  // eslint-disable-next-line @typescript-eslint/no-this-alias
142
154
  const instrumentation: OpenAIInstrumentation = this;
143
155
 
156
+ // Patch create chat completions
144
157
  type ChatCompletionCreateType =
145
158
  typeof module.OpenAI.Chat.Completions.prototype.create;
146
159
 
147
- // Patch create chat completions
148
160
  this._wrap(
149
161
  module.OpenAI.Chat.Completions.prototype,
150
162
  "create",
@@ -367,6 +379,102 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
367
379
  },
368
380
  );
369
381
 
382
+ // Patch responses (if the patched module contains the Responses interface)
383
+ if (module.OpenAI.Responses) {
384
+ type ResponsesCreateType =
385
+ typeof module.OpenAI.Responses.prototype.create;
386
+
387
+ this._wrap(
388
+ module.OpenAI.Responses.prototype,
389
+ "create",
390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
+ (original: ResponsesCreateType): any => {
392
+ return function patchedCreate(
393
+ this: unknown,
394
+ ...args: Parameters<ResponsesCreateType>
395
+ ) {
396
+ const body = args[0];
397
+ const { input: _messages, ...invocationParameters } = body;
398
+ const span = instrumentation.oiTracer.startSpan(
399
+ `OpenAI Responses`,
400
+ {
401
+ kind: SpanKind.INTERNAL,
402
+ attributes: {
403
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]:
404
+ OpenInferenceSpanKind.LLM,
405
+ [SemanticConventions.LLM_MODEL_NAME]: body.model,
406
+ [SemanticConventions.INPUT_VALUE]: JSON.stringify(body),
407
+ [SemanticConventions.INPUT_MIME_TYPE]: MimeType.JSON,
408
+ [SemanticConventions.LLM_INVOCATION_PARAMETERS]:
409
+ JSON.stringify(invocationParameters),
410
+ [SemanticConventions.LLM_SYSTEM]: LLMSystem.OPENAI,
411
+ [SemanticConventions.LLM_PROVIDER]: LLMProvider.OPENAI,
412
+ ...getResponsesInputMessagesAttributes(body),
413
+ ...getLLMToolsJSONSchema(body),
414
+ },
415
+ },
416
+ );
417
+ const execContext = getExecContext(span);
418
+ const execPromise = safeExecuteInTheMiddle<
419
+ ReturnType<ResponsesCreateType>
420
+ >(
421
+ () => {
422
+ return context.with(trace.setSpan(execContext, span), () => {
423
+ return original.apply(this, args);
424
+ });
425
+ },
426
+ (error) => {
427
+ // Push the error to the span
428
+ if (error) {
429
+ span.recordException(error);
430
+ span.setStatus({
431
+ code: SpanStatusCode.ERROR,
432
+ message: error.message,
433
+ });
434
+ span.end();
435
+ }
436
+ },
437
+ );
438
+ const wrappedPromise = execPromise.then((result) => {
439
+ const recordSpan = (result?: ResponseType) => {
440
+ if (!result) {
441
+ span.setStatus({ code: SpanStatusCode.ERROR });
442
+ span.end();
443
+ return;
444
+ }
445
+ span.setAttributes({
446
+ [SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
447
+ [SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
448
+ // Override the model from the value sent by the server
449
+ [SemanticConventions.LLM_MODEL_NAME]: result.model,
450
+ ...getResponsesOutputMessagesAttributes(result),
451
+ ...getResponsesUsageAttributes(result),
452
+ });
453
+ span.setStatus({ code: SpanStatusCode.OK });
454
+ span.end();
455
+ };
456
+ if (isResponseCreateResponse(result)) {
457
+ // Record the results, as we have the final result
458
+ recordSpan(result);
459
+ } else {
460
+ // This is a streaming response
461
+ // First split the stream via tee
462
+ const [leftStream, rightStream] = result.tee();
463
+ // take the right stream, consuming it and then recording the final chunk
464
+ // into the span
465
+ consumeResponseStreamEvents(rightStream).then(recordSpan);
466
+ // give the left stream back to the caller
467
+ result = leftStream;
468
+ }
469
+
470
+ return result;
471
+ });
472
+ return context.bind(execContext, wrappedPromise);
473
+ };
474
+ },
475
+ );
476
+ }
477
+
370
478
  _isOpenInferencePatched = true;
371
479
  try {
372
480
  // This can fail if the module is made immutable via the runtime or bundler
@@ -399,6 +507,12 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
399
507
  }
400
508
  }
401
509
 
510
+ function isResponseCreateResponse(
511
+ response: Stream<ResponseStreamEvent> | ResponseType,
512
+ ): response is ResponseType {
513
+ return "object" in response && response.object === "response";
514
+ }
515
+
402
516
  /**
403
517
  * type-guard that checks if the response is a chat completion response
404
518
  */
@@ -449,7 +563,7 @@ function getLLMInputMessagesAttributes(
449
563
  * Converts each tool definition into a json schema
450
564
  */
451
565
  function getLLMToolsJSONSchema(
452
- body: ChatCompletionCreateParamsBase,
566
+ body: ChatCompletionCreateParamsBase | ResponseCreateParamsBase,
453
567
  ): Attributes {
454
568
  if (!body.tools) {
455
569
  // If tools is undefined, return an empty object
@@ -0,0 +1,286 @@
1
+ import { SemanticConventions } from "@arizeai/openinference-semantic-conventions";
2
+ import { Attributes } from "@opentelemetry/api";
3
+ import {
4
+ ResponseCreateParamsBase,
5
+ ResponseInputItem,
6
+ ResponseOutputItem,
7
+ ResponseStreamEvent,
8
+ Response as ResponseType,
9
+ } from "openai/resources/responses/responses";
10
+ import { Stream } from "openai/streaming";
11
+
12
+ /**
13
+ * Get attributes for responses api Items that are not typical messages with role
14
+ * @param item - The item to get attributes for
15
+ * @param prefix - The prefix to use for the attributes
16
+ * @returns The attributes for the item
17
+ */
18
+ function getResponseItemAttributes(
19
+ item: Exclude<ResponseInputItem | ResponseOutputItem, { role: string }>,
20
+ prefix = "",
21
+ ): Attributes {
22
+ const attributes: Attributes = {};
23
+ // all items that are not typical messages with role
24
+ // things like images, files, etc.
25
+ const toolCallPrefix = `${prefix}${SemanticConventions.MESSAGE_TOOL_CALLS}.0.`;
26
+
27
+ switch (item.type) {
28
+ case "function_call": {
29
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant";
30
+ // now using tool call prefix to simulate multiple tool calls per message
31
+ attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] =
32
+ item.call_id;
33
+ attributes[
34
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`
35
+ ] = item.name;
36
+ attributes[
37
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`
38
+ ] = item.arguments;
39
+ break;
40
+ }
41
+ case "function_call_output": {
42
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool";
43
+ attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] =
44
+ item.call_id;
45
+ attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] =
46
+ item.output;
47
+ break;
48
+ }
49
+ case "reasoning": {
50
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant";
51
+ item.summary.forEach((summaryItem, index) => {
52
+ const summaryItemPrefix = `${prefix}${SemanticConventions.MESSAGE_CONTENTS}.${index}.`;
53
+ if (summaryItem.type === "summary_text") {
54
+ attributes[
55
+ `${summaryItemPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`
56
+ ] = "summary_text";
57
+ attributes[
58
+ `${summaryItemPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`
59
+ ] = summaryItem.text;
60
+ }
61
+ });
62
+ break;
63
+ }
64
+ case "item_reference": {
65
+ break;
66
+ }
67
+ case "file_search_call": {
68
+ if (!item.results) {
69
+ // its a tool call
70
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] =
71
+ "assistant";
72
+ // now using tool call prefix to simulate multiple tool calls per message
73
+ attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] =
74
+ item.id;
75
+ attributes[
76
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`
77
+ ] = item.type;
78
+ attributes[
79
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`
80
+ ] = JSON.stringify(item.queries);
81
+ } else {
82
+ // its a tool call output
83
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool";
84
+ attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] =
85
+ item.id;
86
+ attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] =
87
+ JSON.stringify(item.results);
88
+ }
89
+ break;
90
+ }
91
+ case "computer_call": {
92
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant";
93
+ // now using tool call prefix to simulate multiple tool calls per message
94
+ attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] =
95
+ item.id;
96
+ attributes[
97
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`
98
+ ] = item.type;
99
+ attributes[
100
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_ARGUMENTS_JSON}`
101
+ ] = JSON.stringify(item.action);
102
+ break;
103
+ }
104
+ case "computer_call_output": {
105
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool";
106
+ attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] =
107
+ item.call_id;
108
+ attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] =
109
+ JSON.stringify(item.output);
110
+ break;
111
+ }
112
+ case "web_search_call": {
113
+ attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "assistant";
114
+ // now using tool call prefix to simulate multiple tool calls per message
115
+ attributes[`${toolCallPrefix}${SemanticConventions.TOOL_CALL_ID}`] =
116
+ item.id;
117
+ attributes[
118
+ `${toolCallPrefix}${SemanticConventions.TOOL_CALL_FUNCTION_NAME}`
119
+ ] = item.type;
120
+ // web search call does not share its arguments with the caller
121
+ // it will show "undefined" in the arguments when traced
122
+ break;
123
+ }
124
+ }
125
+ return attributes;
126
+ }
127
+
128
+ /**
129
+ * Get attributes for responses api Item, input or output
130
+ * Non message items are detected and handled by {@link getResponseItemAttributes}
131
+ * @param itemMessage - The message item to get attributes for
132
+ * @param prefix - The prefix to use for the attributes
133
+ * @returns The attributes for the message item
134
+ */
135
+ function getResponseItemMessageAttributes(
136
+ itemMessage: ResponseInputItem | ResponseOutputItem,
137
+ prefix = "",
138
+ ): Attributes {
139
+ const message =
140
+ typeof itemMessage === "string"
141
+ ? ({ content: itemMessage, role: "user" } satisfies ResponseInputItem)
142
+ : itemMessage;
143
+ if (!("role" in message)) {
144
+ return getResponseItemAttributes(message, prefix);
145
+ }
146
+ const role = message.role;
147
+ const attributes: Attributes = {
148
+ [`${prefix}${SemanticConventions.MESSAGE_ROLE}`]: role,
149
+ };
150
+ // add contents from message
151
+ if (typeof message.content === "string") {
152
+ attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] =
153
+ message.content;
154
+ } else if (Array.isArray(message.content)) {
155
+ message.content.forEach((part, index) => {
156
+ const contentsIndexPrefix = `${prefix}${SemanticConventions.MESSAGE_CONTENTS}.${index}.`;
157
+ if (part.type === "input_text") {
158
+ attributes[
159
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`
160
+ ] = "input_text";
161
+ attributes[
162
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`
163
+ ] = part.text;
164
+ } else if (part.type === "output_text") {
165
+ attributes[
166
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`
167
+ ] = "output_text";
168
+ attributes[
169
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`
170
+ ] = part.text;
171
+ } else if (part.type === "input_image") {
172
+ attributes[
173
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`
174
+ ] = "input_image";
175
+ if (part.image_url) {
176
+ attributes[
177
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_IMAGE}.${SemanticConventions.IMAGE_URL}`
178
+ ] = part.image_url;
179
+ }
180
+ } else if (part.type === "input_file") {
181
+ // TODO: Handle input file
182
+ } else if (part.type === "refusal") {
183
+ attributes[
184
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TYPE}`
185
+ ] = "refusal";
186
+ attributes[
187
+ `${contentsIndexPrefix}${SemanticConventions.MESSAGE_CONTENT_TEXT}`
188
+ ] = part.refusal;
189
+ }
190
+ });
191
+ }
192
+ // add role based fields to attributes
193
+ // as of now, there are no role based fields as they are handled by alternatively typed
194
+ // items
195
+ switch (message.role) {
196
+ case "user": {
197
+ break;
198
+ }
199
+ case "assistant": {
200
+ break;
201
+ }
202
+ case "system": {
203
+ break;
204
+ }
205
+ case "developer": {
206
+ break;
207
+ }
208
+ }
209
+ return attributes;
210
+ }
211
+
212
+ export function getResponsesInputMessagesAttributes(
213
+ body: ResponseCreateParamsBase,
214
+ ): Attributes {
215
+ const attributes: Attributes = {};
216
+ const items: ResponseInputItem[] = [];
217
+ if (body.instructions) {
218
+ items.push({ content: body.instructions, role: "system" });
219
+ }
220
+ if (typeof body.input === "string") {
221
+ items.push({ content: body.input, role: "user" });
222
+ } else {
223
+ items.push(...body.input);
224
+ }
225
+ items.forEach((item, index) => {
226
+ const indexPrefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}.`;
227
+ const itemAttributes = getResponseItemMessageAttributes(item, indexPrefix);
228
+ Object.entries(itemAttributes).forEach(([key, value]) => {
229
+ attributes[key] = value;
230
+ });
231
+ });
232
+
233
+ return attributes;
234
+ }
235
+
236
+ export function getResponsesUsageAttributes(
237
+ response: ResponseType,
238
+ ): Attributes {
239
+ if (response.usage) {
240
+ return {
241
+ [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
242
+ response.usage.output_tokens,
243
+ [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]: response.usage.input_tokens,
244
+ [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]: response.usage.total_tokens,
245
+ [SemanticConventions.LLM_TOKEN_COUNT_PROMPT_DETAILS_CACHE_READ]:
246
+ response.usage.input_tokens_details?.cached_tokens,
247
+ [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION_DETAILS_REASONING]:
248
+ response.usage.output_tokens_details?.reasoning_tokens,
249
+ // no audio tokens for response inputs or outputs
250
+ };
251
+ }
252
+ return {};
253
+ }
254
+
255
+ export function getResponsesOutputMessagesAttributes(
256
+ response: ResponseType,
257
+ ): Attributes {
258
+ const attributes: Attributes = {};
259
+ const items = response.output;
260
+ items.forEach((item, index) => {
261
+ const indexPrefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}.`;
262
+ const itemAttributes = getResponseItemMessageAttributes(item, indexPrefix);
263
+ Object.entries(itemAttributes).forEach(([key, value]) => {
264
+ attributes[key] = value;
265
+ });
266
+ });
267
+
268
+ return attributes;
269
+ }
270
+
271
+ export async function consumeResponseStreamEvents(
272
+ stream: Stream<ResponseStreamEvent>,
273
+ ): Promise<ResponseType | undefined> {
274
+ let response: ResponseType | undefined;
275
+
276
+ for await (const event of stream) {
277
+ switch (event.type) {
278
+ case "response.completed": {
279
+ response = event.response;
280
+ break;
281
+ }
282
+ }
283
+ }
284
+
285
+ return response;
286
+ }
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 = "2.2.0";
2
+ export const VERSION = "2.3.1";