@arizeai/openinference-instrumentation-openai 4.3.2 → 4.4.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 (45) hide show
  1. package/README.md +21 -0
  2. package/dist/esm/decisionsAttributes.d.ts +35 -0
  3. package/dist/esm/decisionsAttributes.d.ts.map +1 -0
  4. package/dist/esm/decisionsAttributes.js +55 -0
  5. package/dist/esm/decisionsAttributes.js.map +1 -0
  6. package/dist/esm/instrumentation.d.ts.map +1 -1
  7. package/dist/esm/instrumentation.js +98 -24
  8. package/dist/esm/instrumentation.js.map +1 -1
  9. package/dist/esm/responsesAttributes.d.ts.map +1 -1
  10. package/dist/esm/responsesAttributes.js +6 -3
  11. package/dist/esm/responsesAttributes.js.map +1 -1
  12. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  13. package/dist/esm/version.d.ts +1 -1
  14. package/dist/esm/version.js +1 -1
  15. package/dist/esnext/decisionsAttributes.d.ts +35 -0
  16. package/dist/esnext/decisionsAttributes.d.ts.map +1 -0
  17. package/dist/esnext/decisionsAttributes.js +50 -0
  18. package/dist/esnext/decisionsAttributes.js.map +1 -0
  19. package/dist/esnext/instrumentation.d.ts.map +1 -1
  20. package/dist/esnext/instrumentation.js +95 -24
  21. package/dist/esnext/instrumentation.js.map +1 -1
  22. package/dist/esnext/responsesAttributes.d.ts.map +1 -1
  23. package/dist/esnext/responsesAttributes.js +6 -4
  24. package/dist/esnext/responsesAttributes.js.map +1 -1
  25. package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
  26. package/dist/esnext/version.d.ts +1 -1
  27. package/dist/esnext/version.js +1 -1
  28. package/dist/src/decisionsAttributes.d.ts +35 -0
  29. package/dist/src/decisionsAttributes.d.ts.map +1 -0
  30. package/dist/src/decisionsAttributes.js +54 -0
  31. package/dist/src/decisionsAttributes.js.map +1 -0
  32. package/dist/src/instrumentation.d.ts.map +1 -1
  33. package/dist/src/instrumentation.js +95 -24
  34. package/dist/src/instrumentation.js.map +1 -1
  35. package/dist/src/responsesAttributes.d.ts.map +1 -1
  36. package/dist/src/responsesAttributes.js +6 -4
  37. package/dist/src/responsesAttributes.js.map +1 -1
  38. package/dist/src/version.d.ts +1 -1
  39. package/dist/src/version.js +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +2 -2
  42. package/src/decisionsAttributes.ts +74 -0
  43. package/src/instrumentation.ts +114 -25
  44. package/src/responsesAttributes.ts +7 -4
  45. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arizeai/openinference-instrumentation-openai",
3
- "version": "4.3.2",
3
+ "version": "4.4.1",
4
4
  "private": false,
5
5
  "description": "OpenInference instrumentation for OpenAI",
6
6
  "keywords": [],
@@ -38,7 +38,7 @@
38
38
  "@opentelemetry/sdk-trace-node": "^1.25.1",
39
39
  "@opentelemetry/semantic-conventions": "^1.25.1",
40
40
  "@types/node": "^20.14.11",
41
- "openai": "^6.7.0",
41
+ "openai": "^7.30.0",
42
42
  "typescript": "^7.0.2",
43
43
  "vitest": "^4.0.3",
44
44
  "zod": "^3.24.3"
@@ -0,0 +1,74 @@
1
+ import type { Attributes } from "@opentelemetry/api";
2
+ import type { Decision, DecisionCreateParams } from "openai/resources/decisions";
3
+
4
+ import {
5
+ getDecisionAttributes,
6
+ getInputAttributes,
7
+ getOutputAttributes,
8
+ safelyJSONStringify,
9
+ } from "@arizeai/openinference-core";
10
+ import {
11
+ DecisionProvider,
12
+ DecisionSystem,
13
+ MimeType,
14
+ } from "@arizeai/openinference-semantic-conventions";
15
+
16
+ /**
17
+ * Builds the OpenInference attributes for a Decisions API request.
18
+ *
19
+ * Decision spans identify the model under `decision.*` rather than `llm.*`,
20
+ * and carry the raw request as JSON on `input.value` instead of input
21
+ * messages: the interesting part of a decision request is the set of typed
22
+ * questions and candidate answers, which has no message equivalent.
23
+ *
24
+ * @param body - The `decisions.create` request body.
25
+ * @returns `decision.system`, `decision.provider`, `decision.request.model_name`,
26
+ * `decision.model_name` and the input attributes.
27
+ * @see https://github.com/Arize-ai/openinference/blob/main/spec/decision_spans.md
28
+ */
29
+ export function getDecisionsRequestAttributes(body: DecisionCreateParams): Attributes {
30
+ const input = safelyJSONStringify(body);
31
+ return {
32
+ ...getDecisionAttributes({
33
+ system: DecisionSystem.OPENAI,
34
+ provider: DecisionProvider.OPENAI,
35
+ requestModelName: body.model,
36
+ }),
37
+ ...(input != null ? getInputAttributes({ value: input, mimeType: MimeType.JSON }) : {}),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Builds the OpenInference attributes for a Decisions API response.
43
+ *
44
+ * Records the raw response as JSON on `output.value`, the model that served
45
+ * the decision as `decision.response.model_name` (and as `decision.model_name`,
46
+ * falling back to the requested model when the response omits it), and the
47
+ * reported usage as `decision.token_count.input` / `decision.token_count.output`.
48
+ *
49
+ * @param args - Attribute sources.
50
+ * @param args.response - The parsed `decisions.create` response.
51
+ * @param args.requestModelName - The model named in the request, used as the
52
+ * `decision.model_name` fallback.
53
+ * @returns The output and decision response attributes.
54
+ */
55
+ export function getDecisionsResponseAttributes({
56
+ response,
57
+ requestModelName,
58
+ }: {
59
+ response: Decision;
60
+ requestModelName?: string;
61
+ }): Attributes {
62
+ const output = safelyJSONStringify(response);
63
+ return {
64
+ ...(output != null ? getOutputAttributes({ value: output, mimeType: MimeType.JSON }) : {}),
65
+ ...getDecisionAttributes({
66
+ requestModelName,
67
+ responseModelName: response.model,
68
+ tokenCount: {
69
+ input: response.usage?.input_tokens,
70
+ output: response.usage?.output_tokens,
71
+ },
72
+ }),
73
+ };
74
+ }
@@ -21,6 +21,7 @@ import type {
21
21
  ChatCompletionMessageParam,
22
22
  } from "openai/resources/chat/completions";
23
23
  import type { CompletionCreateParamsBase } from "openai/resources/completions";
24
+ import type { Decision } from "openai/resources/decisions";
24
25
  import type {
25
26
  Response as ResponseType,
26
27
  ResponseCreateParamsBase,
@@ -38,6 +39,10 @@ import {
38
39
  SemanticConventions,
39
40
  } from "@arizeai/openinference-semantic-conventions";
40
41
 
42
+ import {
43
+ getDecisionsRequestAttributes,
44
+ getDecisionsResponseAttributes,
45
+ } from "./decisionsAttributes";
41
46
  import {
42
47
  consumeResponseStreamEvents,
43
48
  getResponsesInputMessagesAttributes,
@@ -322,6 +327,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
322
327
  },
323
328
  });
324
329
  const execContext = getExecContext(span);
330
+ const recordError = (error: unknown) => endSpanWithError(span, error);
325
331
  const execPromise = safeExecuteInTheMiddle(
326
332
  () => {
327
333
  return context.with(trace.setSpan(execContext, span), () => {
@@ -331,12 +337,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
331
337
  (error) => {
332
338
  // Push the error to the span
333
339
  if (error) {
334
- span.recordException(error);
335
- span.setStatus({
336
- code: SpanStatusCode.ERROR,
337
- message: error.message,
338
- });
339
- span.end();
340
+ recordError(error);
340
341
  }
341
342
  },
342
343
  );
@@ -370,6 +371,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
370
371
 
371
372
  return result;
372
373
  };
374
+ observeRejection(execPromise, recordError);
373
375
  const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
374
376
  return context.bind(execContext, wrappedPromise);
375
377
  };
@@ -399,7 +401,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
399
401
  },
400
402
  });
401
403
  const execContext = getExecContext(span);
402
-
404
+ const recordError = (error: unknown) => endSpanWithError(span, error);
403
405
  const execPromise = safeExecuteInTheMiddle(
404
406
  () => {
405
407
  return context.with(trace.setSpan(execContext, span), () => {
@@ -409,12 +411,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
409
411
  (error) => {
410
412
  // Push the error to the span
411
413
  if (error) {
412
- span.recordException(error);
413
- span.setStatus({
414
- code: SpanStatusCode.ERROR,
415
- message: error.message,
416
- });
417
- span.end();
414
+ recordError(error);
418
415
  }
419
416
  },
420
417
  );
@@ -434,6 +431,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
434
431
  }
435
432
  return result;
436
433
  };
434
+ observeRejection(execPromise, recordError);
437
435
  const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
438
436
  return context.bind(execContext, wrappedPromise);
439
437
  };
@@ -467,6 +465,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
467
465
  },
468
466
  });
469
467
  const execContext = getExecContext(span);
468
+ const recordError = (error: unknown) => endSpanWithError(span, error);
470
469
  const execPromise = safeExecuteInTheMiddle(
471
470
  () => {
472
471
  return context.with(trace.setSpan(execContext, span), () => {
@@ -476,12 +475,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
476
475
  (error) => {
477
476
  // Push the error to the span
478
477
  if (error) {
479
- span.recordException(error);
480
- span.setStatus({
481
- code: SpanStatusCode.ERROR,
482
- message: error.message,
483
- });
484
- span.end();
478
+ recordError(error);
485
479
  }
486
480
  },
487
481
  );
@@ -497,6 +491,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
497
491
  span.end();
498
492
  return result;
499
493
  };
494
+ observeRejection(execPromise, recordError);
500
495
  const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
501
496
  return context.bind(execContext, wrappedPromise);
502
497
  };
@@ -531,6 +526,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
531
526
  },
532
527
  });
533
528
  const execContext = getExecContext(span);
529
+ const recordError = (error: unknown) => endSpanWithError(span, error);
534
530
  const execPromise = safeExecuteInTheMiddle(
535
531
  () => {
536
532
  return context.with(trace.setSpan(execContext, span), () => {
@@ -540,12 +536,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
540
536
  (error) => {
541
537
  // Push the error to the span
542
538
  if (error) {
543
- span.recordException(error);
544
- span.setStatus({
545
- code: SpanStatusCode.ERROR,
546
- message: error.message,
547
- });
548
- span.end();
539
+ recordError(error);
549
540
  }
550
541
  },
551
542
  );
@@ -583,6 +574,58 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
583
574
 
584
575
  return result;
585
576
  };
577
+ observeRejection(execPromise, recordError);
578
+ const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
579
+ return context.bind(execContext, wrappedPromise);
580
+ };
581
+ },
582
+ );
583
+ }
584
+
585
+ // Patch decisions (if the patched module contains the Decisions interface, openai >= 7.30)
586
+ if (module.OpenAI.Decisions) {
587
+ type DecisionsCreateType = typeof module.OpenAI.Decisions.prototype.create;
588
+
589
+ this._wrap(
590
+ module.OpenAI.Decisions.prototype,
591
+ "create",
592
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
593
+ (original: DecisionsCreateType): any => {
594
+ return function patchedCreate(this: unknown, ...args: Parameters<DecisionsCreateType>) {
595
+ const body = args[0];
596
+ // A decision model scores caller-supplied answers rather than generating
597
+ // text, so this is a DECISION span with no llm.* attributes.
598
+ const span = instrumentation.oiTracer.startSpan(`OpenAI Decisions`, {
599
+ kind: SpanKind.INTERNAL,
600
+ attributes: {
601
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.DECISION,
602
+ ...getDecisionsRequestAttributes(body),
603
+ },
604
+ });
605
+ const execContext = getExecContext(span);
606
+ const recordError = (error: unknown) => endSpanWithError(span, error);
607
+ const execPromise = safeExecuteInTheMiddle(
608
+ () => {
609
+ return context.with(trace.setSpan(execContext, span), () => {
610
+ return original.apply(this, args);
611
+ });
612
+ },
613
+ (error) => {
614
+ // Push the error to the span
615
+ if (error) {
616
+ recordError(error);
617
+ }
618
+ },
619
+ );
620
+ const wrappedPromiseThen = (result: Decision) => {
621
+ span.setAttributes(
622
+ getDecisionsResponseAttributes({ response: result, requestModelName: body.model }),
623
+ );
624
+ span.setStatus({ code: SpanStatusCode.OK });
625
+ span.end();
626
+ return result;
627
+ };
628
+ observeRejection(execPromise, recordError);
586
629
  const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
587
630
  return context.bind(execContext, wrappedPromise);
588
631
  };
@@ -613,6 +656,12 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
613
656
  this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
614
657
  this._unwrap(moduleExports.OpenAI.Completions.prototype, "create");
615
658
  this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create");
659
+ if (moduleExports.OpenAI.Responses) {
660
+ this._unwrap(moduleExports.OpenAI.Responses.prototype, "create");
661
+ }
662
+ if (moduleExports.OpenAI.Decisions) {
663
+ this._unwrap(moduleExports.OpenAI.Decisions.prototype, "create");
664
+ }
616
665
 
617
666
  // Keyed the same way patch() keys it, so a re-patch is possible after.
618
667
  _patchedModules.delete(moduleExports.OpenAI);
@@ -1037,6 +1086,46 @@ function isAPIPromise<T>(promise: unknown): promise is APIPromise<T> {
1037
1086
  return promise instanceof APIPromise;
1038
1087
  }
1039
1088
 
1089
+ /**
1090
+ * Calls `onRejected` if the SDK call rejects, so the span can be ended with an
1091
+ * error status.
1092
+ *
1093
+ * {@link invokeMaybeAPIPromise} only sees fulfilled values, so without this a
1094
+ * failed request would leave its span open. For an APIPromise the rejection is
1095
+ * observed through `asResponse()`, which settles with the underlying HTTP
1096
+ * promise and never parses the body: parsing it here as well would consume the
1097
+ * body before the caller reads it. So only a failed request is observed, not a
1098
+ * response body that fails to parse.
1099
+ *
1100
+ * @param promise - The value returned by the original SDK method
1101
+ * @param onRejected - Called with the rejection reason
1102
+ */
1103
+ function observeRejection(promise: unknown, onRejected: (error: unknown) => void): void {
1104
+ if (isAPIPromise<unknown>(promise)) {
1105
+ void promise.asResponse().then(undefined, onRejected);
1106
+ } else if (promise instanceof Promise) {
1107
+ void promise.then(undefined, onRejected);
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Records the error on the span, sets its status to ERROR, and ends it.
1113
+ *
1114
+ * Used both when the SDK method throws synchronously and, through
1115
+ * {@link observeRejection}, when its promise rejects (#3845).
1116
+ *
1117
+ * @param span - The span to end
1118
+ * @param error - The thrown or rejected value, which need not be an Error
1119
+ */
1120
+ function endSpanWithError(span: Span, error: unknown): void {
1121
+ span.recordException(error instanceof Error ? error : String(error));
1122
+ span.setStatus({
1123
+ code: SpanStatusCode.ERROR,
1124
+ message: error instanceof Error ? error.message : String(error),
1125
+ });
1126
+ span.end();
1127
+ }
1128
+
1040
1129
  /**
1041
1130
  * Invokes the thennable of a promise or an APIPromise.
1042
1131
  *
@@ -20,7 +20,7 @@ import { getNumberProperty } from "./typeUtils";
20
20
  * @returns The attributes for the item
21
21
  */
22
22
  function getResponseItemAttributes(
23
- item: Exclude<ResponseInputItem | ResponseOutputItem, { role: string }>,
23
+ item: Exclude<ResponseInputItem | ResponseOutputItem, { role: string; content: unknown }>,
24
24
  prefix = "",
25
25
  ): Attributes {
26
26
  const attributes: Attributes = {};
@@ -40,13 +40,14 @@ function getResponseItemAttributes(
40
40
  }
41
41
  case "function_call_output": {
42
42
  attributes[`${prefix}${SemanticConventions.MESSAGE_ROLE}`] = "tool";
43
- attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] = item.call_id;
43
+ attributes[`${prefix}${SemanticConventions.MESSAGE_TOOL_CALL_ID}`] =
44
+ item.call_id ?? undefined;
44
45
  if (typeof item.output === "string") {
45
46
  attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] = item.output;
46
47
  } else {
47
48
  // TODO(2410): figure out how to serialize the list of tools
48
49
  attributes[`${prefix}${SemanticConventions.MESSAGE_CONTENT}`] =
49
- safelyJSONStringify(item.output) || undefined;
50
+ safelyJSONStringify(item.output) ?? undefined;
50
51
  }
51
52
 
52
53
  break;
@@ -131,7 +132,9 @@ function getResponseItemMessageAttributes(
131
132
  typeof itemMessage === "string"
132
133
  ? ({ content: itemMessage, role: "user" } satisfies ResponseInputItem)
133
134
  : itemMessage;
134
- if (!("role" in message)) {
135
+ // Items such as `additional_tools` carry a role but no content, and are
136
+ // handled with the other non-message items.
137
+ if (!("role" in message) || !("content" in message)) {
135
138
  return getResponseItemAttributes(message, prefix);
136
139
  }
137
140
  const role = message.role;
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.3.2";
2
+ export const VERSION = "4.4.1";