Skip to content

Commit 26a0d01

Browse files
authored
feat(instrumentation-openai): Migrate OpenAI instrumentation to OTel 1.40 semantic conventions (#909)
1 parent dd89e8d commit 26a0d01

File tree

13 files changed

+1392
-1070
lines changed

13 files changed

+1392
-1070
lines changed

packages/instrumentation-openai/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@
4040
"@opentelemetry/api": "^1.9.0",
4141
"@opentelemetry/core": "^2.0.1",
4242
"@opentelemetry/instrumentation": "^0.203.0",
43-
"@opentelemetry/semantic-conventions": "^1.38.0",
43+
"@opentelemetry/semantic-conventions": "^1.40.0",
4444
"@traceloop/ai-semantic-conventions": "workspace:*",
45+
"@traceloop/instrumentation-utils": "workspace:*",
4546
"js-tiktoken": "^1.0.20",
4647
"tslib": "^2.8.1"
4748
},
@@ -57,7 +58,7 @@
5758
"@types/node-fetch": "^2.6.13",
5859
"mocha": "^11.7.1",
5960
"node-fetch": "^2.7.0",
60-
"openai": "5.12.2",
61+
"openai": "^6.32.0",
6162
"ts-mocha": "^11.1.0"
6263
},
6364
"homepage": "https://github.com/traceloop/openllmetry-js/tree/main/packages/instrumentation-openai",

packages/instrumentation-openai/src/image-wrappers.ts

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import { trace, Span, SpanKind, Attributes } from "@opentelemetry/api";
33
import { SpanAttributes } from "@traceloop/ai-semantic-conventions";
44
import {
5-
ATTR_GEN_AI_COMPLETION,
6-
ATTR_GEN_AI_PROMPT,
75
ATTR_GEN_AI_REQUEST_MODEL,
8-
ATTR_GEN_AI_SYSTEM,
9-
ATTR_GEN_AI_USAGE_COMPLETION_TOKENS,
10-
ATTR_GEN_AI_USAGE_PROMPT_TOKENS,
6+
ATTR_GEN_AI_PROVIDER_NAME,
7+
ATTR_GEN_AI_INPUT_MESSAGES,
8+
ATTR_GEN_AI_OUTPUT_MESSAGES,
9+
ATTR_GEN_AI_USAGE_INPUT_TOKENS,
10+
ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
11+
ATTR_GEN_AI_OPERATION_NAME,
12+
GEN_AI_PROVIDER_NAME_VALUE_OPENAI,
1113
} from "@opentelemetry/semantic-conventions/incubating";
1214
import type { ImageUploadCallback } from "./types";
1315
import type {
@@ -169,8 +171,9 @@ export function setImageGenerationRequestAttributes(
169171
}
170172

171173
if (params.prompt) {
172-
attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt;
173-
attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
174+
attributes[ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify([
175+
{ role: "user", parts: [{ type: "text", content: params.prompt }] },
176+
]);
174177
}
175178

176179
Object.entries(attributes).forEach(([key, value]) => {
@@ -200,8 +203,9 @@ export async function setImageEditRequestAttributes(
200203
}
201204

202205
if (params.prompt) {
203-
attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = params.prompt;
204-
attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
206+
attributes[ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify([
207+
{ role: "user", parts: [{ type: "text", content: params.prompt }] },
208+
]);
205209
}
206210

207211
// Process input image if upload callback is available
@@ -223,10 +227,24 @@ export async function setImageEditRequestAttributes(
223227
);
224228

225229
if (imageUrl) {
226-
attributes[`${ATTR_GEN_AI_PROMPT}.1.content`] = JSON.stringify([
227-
{ type: "image_url", image_url: { url: imageUrl } },
228-
]);
229-
attributes[`${ATTR_GEN_AI_PROMPT}.1.role`] = "user";
230+
// Add the image as a part of the existing user message
231+
const existingMessages = attributes[ATTR_GEN_AI_INPUT_MESSAGES];
232+
if (existingMessages) {
233+
const parsed = JSON.parse(existingMessages as string);
234+
parsed[0].parts.push({
235+
type: "uri",
236+
modality: "image",
237+
uri: imageUrl,
238+
});
239+
attributes[ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify(parsed);
240+
} else {
241+
attributes[ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify([
242+
{
243+
role: "user",
244+
parts: [{ type: "uri", modality: "image", uri: imageUrl }],
245+
},
246+
]);
247+
}
230248
}
231249
}
232250

@@ -275,10 +293,12 @@ export async function setImageVariationRequestAttributes(
275293
);
276294

277295
if (imageUrl) {
278-
attributes[`${ATTR_GEN_AI_PROMPT}.0.content`] = JSON.stringify([
279-
{ type: "image_url", image_url: { url: imageUrl } },
296+
attributes[ATTR_GEN_AI_INPUT_MESSAGES] = JSON.stringify([
297+
{
298+
role: "user",
299+
parts: [{ type: "uri", modality: "image", uri: imageUrl }],
300+
},
280301
]);
281-
attributes[`${ATTR_GEN_AI_PROMPT}.0.role`] = "user";
282302
}
283303
}
284304

@@ -303,7 +323,7 @@ export async function setImageGenerationResponseAttributes(
303323
params,
304324
response.data.length,
305325
);
306-
attributes[ATTR_GEN_AI_USAGE_COMPLETION_TOKENS] = completionTokens;
326+
attributes[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = completionTokens;
307327

308328
// Calculate prompt tokens if enrichTokens is enabled
309329
if (instrumentationConfig?.enrichTokens) {
@@ -319,38 +339,34 @@ export async function setImageGenerationResponseAttributes(
319339
}
320340

321341
if (estimatedPromptTokens > 0) {
322-
attributes[ATTR_GEN_AI_USAGE_PROMPT_TOKENS] = estimatedPromptTokens;
342+
attributes[ATTR_GEN_AI_USAGE_INPUT_TOKENS] = estimatedPromptTokens;
323343
}
324344

325-
attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] =
345+
attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] =
326346
estimatedPromptTokens + completionTokens;
327347
} catch {
328-
attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = completionTokens;
348+
attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] = completionTokens;
329349
}
330350
} else {
331-
attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = completionTokens;
351+
attributes[SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS] = completionTokens;
332352
}
333353
}
334354

335355
if (response.data && response.data.length > 0) {
336356
const firstImage = response.data[0];
357+
let imageOutputUrl: string | undefined;
337358

338359
if (firstImage.b64_json && uploadCallback) {
339360
try {
340361
const traceId = span.spanContext().traceId;
341362
const spanId = span.spanContext().spanId;
342363

343-
const imageUrl = await uploadCallback(
364+
imageOutputUrl = await uploadCallback(
344365
traceId,
345366
spanId,
346367
"generated_image.png",
347368
firstImage.b64_json,
348369
);
349-
350-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
351-
{ type: "image_url", image_url: { url: imageUrl } },
352-
]);
353-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
354370
} catch (error) {
355371
console.error("Failed to upload generated image:", error);
356372
}
@@ -364,29 +380,28 @@ export async function setImageGenerationResponseAttributes(
364380
const buffer = Buffer.from(arrayBuffer);
365381
const base64Data = buffer.toString("base64");
366382

367-
const uploadedUrl = await uploadCallback(
383+
imageOutputUrl = await uploadCallback(
368384
traceId,
369385
spanId,
370386
"generated_image.png",
371387
base64Data,
372388
);
373-
374-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
375-
{ type: "image_url", image_url: { url: uploadedUrl } },
376-
]);
377-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
378389
} catch (error) {
379390
console.error("Failed to fetch and upload generated image:", error);
380-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
381-
{ type: "image_url", image_url: { url: firstImage.url } },
382-
]);
383-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
391+
imageOutputUrl = firstImage.url;
384392
}
385393
} else if (firstImage.url) {
386-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.content`] = JSON.stringify([
387-
{ type: "image_url", image_url: { url: firstImage.url } },
394+
imageOutputUrl = firstImage.url;
395+
}
396+
397+
if (imageOutputUrl) {
398+
attributes[ATTR_GEN_AI_OUTPUT_MESSAGES] = JSON.stringify([
399+
{
400+
role: "assistant",
401+
finish_reason: "stop",
402+
parts: [{ type: "uri", modality: "image", uri: imageOutputUrl }],
403+
},
388404
]);
389-
attributes[`${ATTR_GEN_AI_COMPLETION}.0.role`] = "assistant";
390405
}
391406

392407
if (firstImage.revised_prompt) {
@@ -410,10 +425,11 @@ export function wrapImageGeneration(
410425
return function (this: any, ...args: any[]) {
411426
const params = args[0] as ImageGenerateParams;
412427

413-
const span = tracer.startSpan("openai.images.generate", {
428+
const span = tracer.startSpan(`image_generation ${params.model}`, {
414429
kind: SpanKind.CLIENT,
415430
attributes: {
416-
[ATTR_GEN_AI_SYSTEM]: "OpenAI",
431+
[ATTR_GEN_AI_PROVIDER_NAME]: GEN_AI_PROVIDER_NAME_VALUE_OPENAI,
432+
[ATTR_GEN_AI_OPERATION_NAME]: "image_generation",
417433
"gen_ai.request.type": "image_generation",
418434
},
419435
});
@@ -469,10 +485,11 @@ export function wrapImageEdit(
469485
return function (this: any, ...args: any[]) {
470486
const params = args[0] as ImageEditParams;
471487

472-
const span = tracer.startSpan("openai.images.edit", {
488+
const span = tracer.startSpan(`image_edit ${params.model}`, {
473489
kind: SpanKind.CLIENT,
474490
attributes: {
475-
[ATTR_GEN_AI_SYSTEM]: "OpenAI",
491+
[ATTR_GEN_AI_PROVIDER_NAME]: GEN_AI_PROVIDER_NAME_VALUE_OPENAI,
492+
[ATTR_GEN_AI_OPERATION_NAME]: "image_edit",
476493
"gen_ai.request.type": "image_edit",
477494
},
478495
});
@@ -536,10 +553,11 @@ export function wrapImageVariation(
536553
return function (this: any, ...args: any[]) {
537554
const params = args[0] as ImageCreateVariationParams;
538555

539-
const span = tracer.startSpan("openai.images.createVariation", {
556+
const span = tracer.startSpan(`image_variation ${params.model}`, {
540557
kind: SpanKind.CLIENT,
541558
attributes: {
542-
[ATTR_GEN_AI_SYSTEM]: "OpenAI",
559+
[ATTR_GEN_AI_PROVIDER_NAME]: GEN_AI_PROVIDER_NAME_VALUE_OPENAI,
560+
[ATTR_GEN_AI_OPERATION_NAME]: "image_variation",
543561
"gen_ai.request.type": "image_variation",
544562
},
545563
});

0 commit comments

Comments
 (0)