@arizeai/openinference-instrumentation-openai 0.0.2 → 0.0.4

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,16 +1,20 @@
1
1
  {
2
2
  "name": "@arizeai/openinference-instrumentation-openai",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "OpenInference instrumentation for OpenAI",
5
5
  "private": false,
6
6
  "main": "dist/src/index.js",
7
7
  "module": "dist/esm/index.js",
8
8
  "esnext": "dist/esnext/index.js",
9
9
  "types": "dist/src/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/Arize-ai/openinference.git"
13
+ },
10
14
  "dependencies": {
11
15
  "@opentelemetry/api": "^1.7.0",
12
16
  "@opentelemetry/instrumentation": "^0.46.0",
13
- "@arizeai/openinference-semantic-conventions": "0.0.11"
17
+ "@arizeai/openinference-semantic-conventions": "0.0.13"
14
18
  },
15
19
  "keywords": [],
16
20
  "files": [
@@ -30,6 +34,7 @@
30
34
  "prebuild": "rimraf dist & pnpm run version:update",
31
35
  "build": "tsc --build tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
32
36
  "version:update": "../../scripts/version-update.js",
37
+ "type:check": "tsc --noEmit",
33
38
  "test": "jest ."
34
39
  }
35
40
  }
@@ -1,209 +1,340 @@
1
1
  import * as openai from "openai";
2
2
  import {
3
- InstrumentationBase,
4
- InstrumentationConfig,
5
- InstrumentationModuleDefinition,
6
- InstrumentationNodeModuleDefinition,
7
- safeExecuteInTheMiddle,
3
+ InstrumentationBase,
4
+ InstrumentationConfig,
5
+ InstrumentationModuleDefinition,
6
+ InstrumentationNodeModuleDefinition,
7
+ safeExecuteInTheMiddle,
8
8
  } from "@opentelemetry/instrumentation";
9
9
  import {
10
- diag,
11
- context,
12
- trace,
13
- SpanKind,
14
- Attributes,
15
- SpanStatusCode,
10
+ diag,
11
+ context,
12
+ trace,
13
+ SpanKind,
14
+ Attributes,
15
+ SpanStatusCode,
16
16
  } from "@opentelemetry/api";
17
17
  import { VERSION } from "./version";
18
18
  import {
19
- SemanticConventions,
20
- OpenInferenceSpanKind,
21
- MimeType,
19
+ SemanticConventions,
20
+ OpenInferenceSpanKind,
21
+ MimeType,
22
22
  } from "@arizeai/openinference-semantic-conventions";
23
23
  import {
24
- ChatCompletion,
25
- ChatCompletionChunk,
26
- ChatCompletionCreateParamsBase,
24
+ ChatCompletion,
25
+ ChatCompletionChunk,
26
+ ChatCompletionCreateParamsBase,
27
27
  } from "openai/resources/chat/completions";
28
28
  import { Stream } from "openai/streaming";
29
+ import {
30
+ CreateEmbeddingResponse,
31
+ EmbeddingCreateParams,
32
+ } from "openai/resources";
29
33
 
30
34
  const MODULE_NAME = "openai";
31
35
 
32
36
  export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> {
33
- constructor(config?: InstrumentationConfig) {
34
- super(
35
- "@arizeai/openinference-instrumentation-openai",
36
- VERSION,
37
- Object.assign({}, config)
38
- );
39
- }
37
+ constructor(config?: InstrumentationConfig) {
38
+ super(
39
+ "@arizeai/openinference-instrumentation-openai",
40
+ VERSION,
41
+ Object.assign({}, config),
42
+ );
43
+ }
40
44
 
41
- protected init(): InstrumentationModuleDefinition<typeof openai> {
42
- const module = new InstrumentationNodeModuleDefinition<typeof openai>(
43
- "openai",
44
- ["^4.0.0"],
45
- this.patch.bind(this),
46
- this.unpatch.bind(this)
47
- );
48
- return module;
45
+ protected init(): InstrumentationModuleDefinition<typeof openai> {
46
+ const module = new InstrumentationNodeModuleDefinition<typeof openai>(
47
+ "openai",
48
+ ["^4.0.0"],
49
+ this.patch.bind(this),
50
+ this.unpatch.bind(this),
51
+ );
52
+ return module;
53
+ }
54
+ /**
55
+ * Patches the OpenAI module
56
+ */
57
+ private patch(
58
+ module: typeof openai & { openInferencePatched?: boolean },
59
+ moduleVersion?: string,
60
+ ) {
61
+ diag.debug(`Applying patch for ${MODULE_NAME}@${moduleVersion}`);
62
+ if (module?.openInferencePatched) {
63
+ return module;
49
64
  }
50
- /**
51
- * Patches the OpenAI module
52
- */
53
- private patch(
54
- module: typeof openai & { openInferencePatched?: boolean },
55
- moduleVersion?: string
56
- ) {
57
- diag.debug(`Applying patch for ${MODULE_NAME}@${moduleVersion}`);
58
- if (module?.openInferencePatched) {
59
- return module;
60
- }
61
- const instrumentation: OpenAIInstrumentation = this;
62
- type CompletionCreateType =
63
- typeof module.OpenAI.Chat.Completions.prototype.create;
65
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
66
+ const instrumentation: OpenAIInstrumentation = this;
67
+ type CompletionCreateType =
68
+ typeof module.OpenAI.Chat.Completions.prototype.create;
64
69
 
65
- // Patch create chat completions
66
- this._wrap(
67
- module.OpenAI.Chat.Completions.prototype,
68
- "create",
69
- (original: CompletionCreateType): any => {
70
- return function patchedCreate(
71
- this: unknown,
72
- ...args: Parameters<
73
- typeof module.OpenAI.Chat.Completions.prototype.create
74
- >
75
- ) {
76
- const body = args[0];
77
- const { messages, ...invocationParameters } = body;
78
- const span = instrumentation.tracer.startSpan(
79
- `OpenAI Chat Completions`,
80
- {
81
- kind: SpanKind.CLIENT,
82
- attributes: {
83
- [SemanticConventions.OPENINFERENCE_SPAN_KIND]:
84
- OpenInferenceSpanKind.LLM,
85
- [SemanticConventions.LLM_MODEL_NAME]:
86
- body.model,
87
- [SemanticConventions.INPUT_VALUE]:
88
- JSON.stringify(body),
89
- [SemanticConventions.INPUT_MIME_TYPE]:
90
- MimeType.JSON,
91
- [SemanticConventions.LLM_INVOCATION_PARAMETERS]:
92
- JSON.stringify(invocationParameters),
93
- ...getLLMInputMessagesAttributes(body),
94
- },
95
- }
96
- );
97
- const execContext = trace.setSpan(context.active(), span);
98
- const execPromise = safeExecuteInTheMiddle<
99
- ReturnType<CompletionCreateType>
100
- >(
101
- () => {
102
- return context.with(execContext, () => {
103
- return original.apply(this, args);
104
- });
105
- },
106
- (error) => {
107
- // Push the error to the span
108
- if (error) {
109
- span.recordException(error);
110
- }
111
- }
112
- );
113
- const wrappedPromise = execPromise.then((result) => {
114
- if (result) {
115
- // Record the results
116
- span.setAttributes({
117
- [SemanticConventions.OUTPUT_VALUE]:
118
- JSON.stringify(result),
119
- [SemanticConventions.OUTPUT_MIME_TYPE]:
120
- MimeType.JSON,
121
- ...getLLMOutputMessagesAttributes(result),
122
- ...getUsageAttributes(result),
123
- });
124
- }
125
- span.setStatus({ code: SpanStatusCode.OK });
126
- span.end();
127
- return result;
128
- });
129
- return context.bind(execContext, wrappedPromise);
130
- };
70
+ // Patch create chat completions
71
+ this._wrap(
72
+ module.OpenAI.Chat.Completions.prototype,
73
+ "create",
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ (original: CompletionCreateType): any => {
76
+ return function patchedCreate(
77
+ this: unknown,
78
+ ...args: Parameters<
79
+ typeof module.OpenAI.Chat.Completions.prototype.create
80
+ >
81
+ ) {
82
+ const body = args[0];
83
+ const { messages: _messages, ...invocationParameters } = body;
84
+ const span = instrumentation.tracer.startSpan(
85
+ `OpenAI Chat Completions`,
86
+ {
87
+ kind: SpanKind.INTERNAL,
88
+ attributes: {
89
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]:
90
+ OpenInferenceSpanKind.LLM,
91
+ [SemanticConventions.LLM_MODEL_NAME]: body.model,
92
+ [SemanticConventions.INPUT_VALUE]: JSON.stringify(body),
93
+ [SemanticConventions.INPUT_MIME_TYPE]: MimeType.JSON,
94
+ [SemanticConventions.LLM_INVOCATION_PARAMETERS]:
95
+ JSON.stringify(invocationParameters),
96
+ ...getLLMInputMessagesAttributes(body),
97
+ },
98
+ },
99
+ );
100
+ const execContext = trace.setSpan(context.active(), span);
101
+ const execPromise = safeExecuteInTheMiddle<
102
+ ReturnType<CompletionCreateType>
103
+ >(
104
+ () => {
105
+ return context.with(execContext, () => {
106
+ return original.apply(this, args);
107
+ });
108
+ },
109
+ (error) => {
110
+ // Push the error to the span
111
+ if (error) {
112
+ span.recordException(error);
113
+ span.setStatus({
114
+ code: SpanStatusCode.ERROR,
115
+ message: error.message,
116
+ });
117
+ span.end();
118
+ }
119
+ },
120
+ );
121
+ const wrappedPromise = execPromise.then((result) => {
122
+ if (result) {
123
+ // Record the results
124
+ span.setAttributes({
125
+ [SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result),
126
+ [SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON,
127
+ // Override the model from the value sent by the server
128
+ [SemanticConventions.LLM_MODEL_NAME]: isChatCompletionResponse(
129
+ result,
130
+ )
131
+ ? result.model
132
+ : body.model,
133
+ ...getLLMOutputMessagesAttributes(result),
134
+ ...getUsageAttributes(result),
135
+ });
131
136
  }
132
- );
133
- module.openInferencePatched = true;
134
- return module;
135
- }
136
- /**
137
- * Un-patches the OpenAI module's chat completions API
138
- */
139
- private unpatch(moduleExports: typeof openai, moduleVersion?: string) {
140
- diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`);
141
- this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
142
- }
137
+ span.setStatus({ code: SpanStatusCode.OK });
138
+ span.end();
139
+ return result;
140
+ });
141
+ return context.bind(execContext, wrappedPromise);
142
+ };
143
+ },
144
+ );
145
+
146
+ // Patch embeddings
147
+ type EmbeddingsCreateType =
148
+ typeof module.OpenAI.Embeddings.prototype.create;
149
+ this._wrap(
150
+ module.OpenAI.Embeddings.prototype,
151
+ "create",
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ (original: EmbeddingsCreateType): any => {
154
+ return function patchedEmbeddingCreate(
155
+ this: unknown,
156
+ ...args: Parameters<typeof module.OpenAI.Embeddings.prototype.create>
157
+ ) {
158
+ const body = args[0];
159
+ const { input } = body;
160
+ const isStringInput = typeof input == "string";
161
+ const span = instrumentation.tracer.startSpan(`OpenAI Embeddings`, {
162
+ kind: SpanKind.INTERNAL,
163
+ attributes: {
164
+ [SemanticConventions.OPENINFERENCE_SPAN_KIND]:
165
+ OpenInferenceSpanKind.EMBEDDING,
166
+ [SemanticConventions.EMBEDDING_MODEL_NAME]: body.model,
167
+ [SemanticConventions.INPUT_VALUE]: isStringInput
168
+ ? input
169
+ : JSON.stringify(input),
170
+ [SemanticConventions.INPUT_MIME_TYPE]: isStringInput
171
+ ? MimeType.TEXT
172
+ : MimeType.JSON,
173
+ ...getEmbeddingTextAttributes(body),
174
+ },
175
+ });
176
+ const execContext = trace.setSpan(context.active(), span);
177
+ const execPromise = safeExecuteInTheMiddle<
178
+ ReturnType<EmbeddingsCreateType>
179
+ >(
180
+ () => {
181
+ return context.with(execContext, () => {
182
+ return original.apply(this, args);
183
+ });
184
+ },
185
+ (error) => {
186
+ // Push the error to the span
187
+ if (error) {
188
+ span.recordException(error);
189
+ span.setStatus({
190
+ code: SpanStatusCode.ERROR,
191
+ message: error.message,
192
+ });
193
+ span.end();
194
+ }
195
+ },
196
+ );
197
+ const wrappedPromise = execPromise.then((result) => {
198
+ if (result) {
199
+ // Record the results
200
+ span.setAttributes({
201
+ // Do not record the output data as it can be large
202
+ ...getEmbeddingEmbeddingsAttributes(result),
203
+ });
204
+ }
205
+ span.setStatus({ code: SpanStatusCode.OK });
206
+ span.end();
207
+ return result;
208
+ });
209
+ return context.bind(execContext, wrappedPromise);
210
+ };
211
+ },
212
+ );
213
+
214
+ module.openInferencePatched = true;
215
+ return module;
216
+ }
217
+ /**
218
+ * Un-patches the OpenAI module's chat completions API
219
+ */
220
+ private unpatch(moduleExports: typeof openai, moduleVersion?: string) {
221
+ diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`);
222
+ this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create");
223
+ this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create");
224
+ }
225
+ }
226
+
227
+ /**
228
+ * type-guard that checks if the response is a chat completion response
229
+ */
230
+ function isChatCompletionResponse(
231
+ response: Stream<ChatCompletionChunk> | ChatCompletion,
232
+ ): response is ChatCompletion {
233
+ return "choices" in response;
143
234
  }
144
235
 
145
236
  /**
146
237
  * Converts the body of the request to LLM input messages
147
238
  */
148
239
  function getLLMInputMessagesAttributes(
149
- body: ChatCompletionCreateParamsBase
240
+ body: ChatCompletionCreateParamsBase,
150
241
  ): Attributes {
151
- return body.messages.reduce((acc, message, index) => {
152
- const index_prefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}`;
153
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
154
- message.content
155
- );
156
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = String(
157
- message.role
158
- );
159
- return acc;
160
- }, {} as Attributes);
242
+ return body.messages.reduce((acc, message, index) => {
243
+ const index_prefix = `${SemanticConventions.LLM_INPUT_MESSAGES}.${index}`;
244
+ acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
245
+ message.content,
246
+ );
247
+ acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = String(
248
+ message.role,
249
+ );
250
+ return acc;
251
+ }, {} as Attributes);
161
252
  }
162
253
 
163
254
  /**
164
255
  * Get Usage attributes
165
256
  */
166
257
  function getUsageAttributes(
167
- response: Stream<ChatCompletionChunk> | ChatCompletion
258
+ response: Stream<ChatCompletionChunk> | ChatCompletion,
168
259
  ) {
169
- if (response.hasOwnProperty("usage")) {
170
- const completion = response as ChatCompletion;
171
- if (completion.usage) {
172
- return {
173
- [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
174
- completion.usage.completion_tokens,
175
- [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]:
176
- completion.usage.prompt_tokens,
177
- [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]:
178
- completion.usage.total_tokens,
179
- };
180
- }
260
+ if (Object.prototype.hasOwnProperty.call(response, "usage")) {
261
+ const completion = response as ChatCompletion;
262
+ if (completion.usage) {
263
+ return {
264
+ [SemanticConventions.LLM_TOKEN_COUNT_COMPLETION]:
265
+ completion.usage.completion_tokens,
266
+ [SemanticConventions.LLM_TOKEN_COUNT_PROMPT]:
267
+ completion.usage.prompt_tokens,
268
+ [SemanticConventions.LLM_TOKEN_COUNT_TOTAL]:
269
+ completion.usage.total_tokens,
270
+ };
181
271
  }
182
- return {};
272
+ }
273
+ return {};
183
274
  }
184
275
 
185
276
  /**
186
277
  * Converts the result to LLM output attributes
187
278
  */
188
279
  function getLLMOutputMessagesAttributes(
189
- response: Stream<ChatCompletionChunk> | ChatCompletion
280
+ response: Stream<ChatCompletionChunk> | ChatCompletion,
190
281
  ): Attributes {
191
- // Handle chat completion
192
- if (response.hasOwnProperty("choices")) {
193
- const completion = response as ChatCompletion;
194
- // Right now support just the first choice
195
- const choice = completion.choices[0];
196
- if (!choice) {
197
- return {};
198
- }
199
- return [choice.message].reduce((acc, message, index) => {
200
- const index_prefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
201
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] =
202
- String(message.content);
203
- acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] =
204
- message.role;
205
- return acc;
206
- }, {} as Attributes);
282
+ // Handle chat completion
283
+ if (Object.prototype.hasOwnProperty.call(response, "choices")) {
284
+ const completion = response as ChatCompletion;
285
+ // Right now support just the first choice
286
+ const choice = completion.choices[0];
287
+ if (!choice) {
288
+ return {};
207
289
  }
208
- return {};
290
+ return [choice.message].reduce((acc, message, index) => {
291
+ const index_prefix = `${SemanticConventions.LLM_OUTPUT_MESSAGES}.${index}`;
292
+ acc[`${index_prefix}.${SemanticConventions.MESSAGE_CONTENT}`] = String(
293
+ message.content,
294
+ );
295
+ acc[`${index_prefix}.${SemanticConventions.MESSAGE_ROLE}`] = message.role;
296
+ return acc;
297
+ }, {} as Attributes);
298
+ }
299
+ return {};
300
+ }
301
+
302
+ /**
303
+ * Converts the embedding result payload to embedding attributes
304
+ */
305
+ function getEmbeddingTextAttributes(
306
+ request: EmbeddingCreateParams,
307
+ ): Attributes {
308
+ if (typeof request.input == "string") {
309
+ return {
310
+ [`${SemanticConventions.EMBEDDING_EMBEDDINGS}.0.${SemanticConventions.EMBEDDING_TEXT}`]:
311
+ request.input,
312
+ };
313
+ } else if (
314
+ Array.isArray(request.input) &&
315
+ request.input.length > 0 &&
316
+ typeof request.input[0] == "string"
317
+ ) {
318
+ return request.input.reduce((acc, input, index) => {
319
+ const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`;
320
+ acc[`${index_prefix}.${SemanticConventions.EMBEDDING_TEXT}`] = input;
321
+ return acc;
322
+ }, {} as Attributes);
323
+ }
324
+ // Ignore other cases where input is a number or an array of numbers
325
+ return {};
326
+ }
327
+
328
+ /**
329
+ * Converts the embedding result payload to embedding attributes
330
+ */
331
+ function getEmbeddingEmbeddingsAttributes(
332
+ response: CreateEmbeddingResponse,
333
+ ): Attributes {
334
+ return response.data.reduce((acc, embedding, index) => {
335
+ const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`;
336
+ acc[`${index_prefix}.${SemanticConventions.EMBEDDING_VECTOR}`] =
337
+ embedding.embedding;
338
+ return acc;
339
+ }, {} as Attributes);
209
340
  }
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 = "0.0.2";
2
+ export const VERSION = "0.0.4";