@arizeai/openinference-instrumentation-openai 4.3.1 → 4.4.0
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/README.md +21 -0
- package/dist/esm/decisionsAttributes.d.ts +35 -0
- package/dist/esm/decisionsAttributes.d.ts.map +1 -0
- package/dist/esm/decisionsAttributes.js +55 -0
- package/dist/esm/decisionsAttributes.js.map +1 -0
- package/dist/esm/instrumentation.d.ts.map +1 -1
- package/dist/esm/instrumentation.js +75 -0
- package/dist/esm/instrumentation.js.map +1 -1
- package/dist/esm/responsesAttributes.d.ts.map +1 -1
- package/dist/esm/responsesAttributes.js +6 -3
- package/dist/esm/responsesAttributes.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esnext/decisionsAttributes.d.ts +35 -0
- package/dist/esnext/decisionsAttributes.d.ts.map +1 -0
- package/dist/esnext/decisionsAttributes.js +50 -0
- package/dist/esnext/decisionsAttributes.js.map +1 -0
- package/dist/esnext/instrumentation.d.ts.map +1 -1
- package/dist/esnext/instrumentation.js +72 -0
- package/dist/esnext/instrumentation.js.map +1 -1
- package/dist/esnext/responsesAttributes.d.ts.map +1 -1
- package/dist/esnext/responsesAttributes.js +6 -4
- package/dist/esnext/responsesAttributes.js.map +1 -1
- package/dist/esnext/tsconfig.esnext.tsbuildinfo +1 -1
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/src/decisionsAttributes.d.ts +35 -0
- package/dist/src/decisionsAttributes.d.ts.map +1 -0
- package/dist/src/decisionsAttributes.js +54 -0
- package/dist/src/decisionsAttributes.js.map +1 -0
- package/dist/src/instrumentation.d.ts.map +1 -1
- package/dist/src/instrumentation.js +72 -0
- package/dist/src/instrumentation.js.map +1 -1
- package/dist/src/responsesAttributes.d.ts.map +1 -1
- package/dist/src/responsesAttributes.js +6 -4
- package/dist/src/responsesAttributes.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/decisionsAttributes.ts +74 -0
- package/src/instrumentation.ts +90 -0
- package/src/responsesAttributes.ts +7 -4
- 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
|
+
"version": "4.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "OpenInference instrumentation for OpenAI",
|
|
6
6
|
"keywords": [],
|
|
@@ -28,7 +28,7 @@
|
|
|
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.
|
|
31
|
+
"@arizeai/openinference-core": "2.8.0",
|
|
32
32
|
"@arizeai/openinference-semantic-conventions": "2.14.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
@@ -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": "^
|
|
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
|
+
}
|
package/src/instrumentation.ts
CHANGED
|
@@ -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,
|
|
@@ -590,6 +595,64 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
590
595
|
);
|
|
591
596
|
}
|
|
592
597
|
|
|
598
|
+
// Patch decisions (if the patched module contains the Decisions interface, openai >= 7.30)
|
|
599
|
+
if (module.OpenAI.Decisions) {
|
|
600
|
+
type DecisionsCreateType = typeof module.OpenAI.Decisions.prototype.create;
|
|
601
|
+
|
|
602
|
+
this._wrap(
|
|
603
|
+
module.OpenAI.Decisions.prototype,
|
|
604
|
+
"create",
|
|
605
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
606
|
+
(original: DecisionsCreateType): any => {
|
|
607
|
+
return function patchedCreate(this: unknown, ...args: Parameters<DecisionsCreateType>) {
|
|
608
|
+
const body = args[0];
|
|
609
|
+
// A decision model scores caller-supplied answers rather than generating
|
|
610
|
+
// text, so this is a DECISION span with no llm.* attributes.
|
|
611
|
+
const span = instrumentation.oiTracer.startSpan(`OpenAI Decisions`, {
|
|
612
|
+
kind: SpanKind.INTERNAL,
|
|
613
|
+
attributes: {
|
|
614
|
+
[SemanticConventions.OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.DECISION,
|
|
615
|
+
...getDecisionsRequestAttributes(body),
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
const execContext = getExecContext(span);
|
|
619
|
+
const recordError = (error: unknown) => {
|
|
620
|
+
span.recordException(error instanceof Error ? error : String(error));
|
|
621
|
+
span.setStatus({
|
|
622
|
+
code: SpanStatusCode.ERROR,
|
|
623
|
+
message: error instanceof Error ? error.message : String(error),
|
|
624
|
+
});
|
|
625
|
+
span.end();
|
|
626
|
+
};
|
|
627
|
+
const execPromise = safeExecuteInTheMiddle(
|
|
628
|
+
() => {
|
|
629
|
+
return context.with(trace.setSpan(execContext, span), () => {
|
|
630
|
+
return original.apply(this, args);
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
(error) => {
|
|
634
|
+
// Push the error to the span
|
|
635
|
+
if (error) {
|
|
636
|
+
recordError(error);
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
const wrappedPromiseThen = (result: Decision) => {
|
|
641
|
+
span.setAttributes(
|
|
642
|
+
getDecisionsResponseAttributes({ response: result, requestModelName: body.model }),
|
|
643
|
+
);
|
|
644
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
645
|
+
span.end();
|
|
646
|
+
return result;
|
|
647
|
+
};
|
|
648
|
+
observeRejection(execPromise, recordError);
|
|
649
|
+
const wrappedPromise = invokeMaybeAPIPromise(execPromise, wrappedPromiseThen);
|
|
650
|
+
return context.bind(execContext, wrappedPromise);
|
|
651
|
+
};
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
593
656
|
_isOpenInferencePatched = true;
|
|
594
657
|
_patchedModules.add(module.OpenAI);
|
|
595
658
|
this.patchedModuleExports.set(module.OpenAI, module);
|
|
@@ -613,6 +676,12 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
|
|
|
613
676
|
this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
|
|
614
677
|
this._unwrap(moduleExports.OpenAI.Completions.prototype, "create");
|
|
615
678
|
this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create");
|
|
679
|
+
if (moduleExports.OpenAI.Responses) {
|
|
680
|
+
this._unwrap(moduleExports.OpenAI.Responses.prototype, "create");
|
|
681
|
+
}
|
|
682
|
+
if (moduleExports.OpenAI.Decisions) {
|
|
683
|
+
this._unwrap(moduleExports.OpenAI.Decisions.prototype, "create");
|
|
684
|
+
}
|
|
616
685
|
|
|
617
686
|
// Keyed the same way patch() keys it, so a re-patch is possible after.
|
|
618
687
|
_patchedModules.delete(moduleExports.OpenAI);
|
|
@@ -1037,6 +1106,27 @@ function isAPIPromise<T>(promise: unknown): promise is APIPromise<T> {
|
|
|
1037
1106
|
return promise instanceof APIPromise;
|
|
1038
1107
|
}
|
|
1039
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Calls `onRejected` if the SDK call rejects, so the span can be ended with an
|
|
1111
|
+
* error status.
|
|
1112
|
+
*
|
|
1113
|
+
* {@link invokeMaybeAPIPromise} only sees fulfilled values, so without this a
|
|
1114
|
+
* failed request would leave its span open. For an APIPromise the rejection is
|
|
1115
|
+
* observed through `asResponse()`, which settles with the underlying HTTP
|
|
1116
|
+
* promise and never parses the body: parsing it here as well would consume the
|
|
1117
|
+
* body before the caller reads it.
|
|
1118
|
+
*
|
|
1119
|
+
* @param promise - The value returned by the original SDK method
|
|
1120
|
+
* @param onRejected - Called with the rejection reason
|
|
1121
|
+
*/
|
|
1122
|
+
function observeRejection(promise: unknown, onRejected: (error: unknown) => void): void {
|
|
1123
|
+
if (isAPIPromise<unknown>(promise)) {
|
|
1124
|
+
void promise.asResponse().then(undefined, onRejected);
|
|
1125
|
+
} else if (promise instanceof Promise) {
|
|
1126
|
+
void promise.then(undefined, onRejected);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1040
1130
|
/**
|
|
1041
1131
|
* Invokes the thennable of a promise or an APIPromise.
|
|
1042
1132
|
*
|
|
@@ -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}`] =
|
|
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)
|
|
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
|
-
|
|
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.
|
|
2
|
+
export const VERSION = "4.4.0";
|