opentelemetry-instrumentation-openai 0.34.1__py3-none-any.whl → 0.49.3__py3-none-any.whl

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.

Potentially problematic release.


This version of opentelemetry-instrumentation-openai might be problematic. Click here for more details.

Files changed (22) hide show
  1. opentelemetry/instrumentation/openai/__init__.py +11 -6
  2. opentelemetry/instrumentation/openai/shared/__init__.py +167 -68
  3. opentelemetry/instrumentation/openai/shared/chat_wrappers.py +544 -231
  4. opentelemetry/instrumentation/openai/shared/completion_wrappers.py +143 -81
  5. opentelemetry/instrumentation/openai/shared/config.py +8 -3
  6. opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +91 -30
  7. opentelemetry/instrumentation/openai/shared/event_emitter.py +108 -0
  8. opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  9. opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +1 -1
  10. opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
  11. opentelemetry/instrumentation/openai/utils.py +42 -9
  12. opentelemetry/instrumentation/openai/v0/__init__.py +32 -11
  13. opentelemetry/instrumentation/openai/v1/__init__.py +177 -69
  14. opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +208 -109
  15. opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +41 -19
  16. opentelemetry/instrumentation/openai/v1/responses_wrappers.py +1073 -0
  17. opentelemetry/instrumentation/openai/version.py +1 -1
  18. {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/METADATA +7 -8
  19. opentelemetry_instrumentation_openai-0.49.3.dist-info/RECORD +21 -0
  20. {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/WHEEL +1 -1
  21. opentelemetry_instrumentation_openai-0.34.1.dist-info/RECORD +0 -17
  22. {opentelemetry_instrumentation_openai-0.34.1.dist-info → opentelemetry_instrumentation_openai-0.49.3.dist-info}/entry_points.txt +0 -0
@@ -1,12 +1,9 @@
1
1
  from typing import Callable, Collection, Optional
2
- from typing_extensions import Coroutine
3
2
 
4
3
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5
-
6
4
  from opentelemetry.instrumentation.openai.shared.config import Config
7
5
  from opentelemetry.instrumentation.openai.utils import is_openai_v1
8
- from opentelemetry.instrumentation.openai.v0 import OpenAIV0Instrumentor
9
- from opentelemetry.instrumentation.openai.v1 import OpenAIV1Instrumentor
6
+ from typing_extensions import Coroutine
10
7
 
11
8
  _instruments = ("openai >= 0.27.0",)
12
9
 
@@ -17,33 +14,41 @@ class OpenAIInstrumentor(BaseInstrumentor):
17
14
  def __init__(
18
15
  self,
19
16
  enrich_assistant: bool = False,
20
- enrich_token_usage: bool = False,
21
17
  exception_logger=None,
22
18
  get_common_metrics_attributes: Callable[[], dict] = lambda: {},
23
19
  upload_base64_image: Optional[
24
20
  Callable[[str, str, str, str], Coroutine[None, None, str]]
25
21
  ] = lambda *args: "",
26
22
  enable_trace_context_propagation: bool = True,
23
+ use_legacy_attributes: bool = True,
27
24
  ):
28
25
  super().__init__()
29
26
  Config.enrich_assistant = enrich_assistant
30
- Config.enrich_token_usage = enrich_token_usage
31
27
  Config.exception_logger = exception_logger
32
28
  Config.get_common_metrics_attributes = get_common_metrics_attributes
33
29
  Config.upload_base64_image = upload_base64_image
34
30
  Config.enable_trace_context_propagation = enable_trace_context_propagation
31
+ Config.use_legacy_attributes = use_legacy_attributes
35
32
 
36
33
  def instrumentation_dependencies(self) -> Collection[str]:
37
34
  return _instruments
38
35
 
39
36
  def _instrument(self, **kwargs):
40
37
  if is_openai_v1():
38
+ from opentelemetry.instrumentation.openai.v1 import OpenAIV1Instrumentor
39
+
41
40
  OpenAIV1Instrumentor().instrument(**kwargs)
42
41
  else:
42
+ from opentelemetry.instrumentation.openai.v0 import OpenAIV0Instrumentor
43
+
43
44
  OpenAIV0Instrumentor().instrument(**kwargs)
44
45
 
45
46
  def _uninstrument(self, **kwargs):
46
47
  if is_openai_v1():
48
+ from opentelemetry.instrumentation.openai.v1 import OpenAIV1Instrumentor
49
+
47
50
  OpenAIV1Instrumentor().uninstrument(**kwargs)
48
51
  else:
52
+ from opentelemetry.instrumentation.openai.v0 import OpenAIV0Instrumentor
53
+
49
54
  OpenAIV0Instrumentor().uninstrument(**kwargs)
@@ -1,42 +1,41 @@
1
- import os
2
- import openai
3
1
  import json
4
- import types
5
2
  import logging
6
-
3
+ import types
4
+ import openai
5
+ import pydantic
7
6
  from importlib.metadata import version
8
7
 
9
- from opentelemetry import context as context_api
10
- from opentelemetry.trace.propagation import set_span_in_context
11
- from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
12
-
13
8
  from opentelemetry.instrumentation.openai.shared.config import Config
14
- from opentelemetry.semconv_ai import SpanAttributes
15
9
  from opentelemetry.instrumentation.openai.utils import (
16
10
  dont_throw,
17
11
  is_openai_v1,
18
- should_record_stream_token_usage,
19
12
  )
13
+ from opentelemetry.semconv._incubating.attributes import (
14
+ gen_ai_attributes as GenAIAttributes,
15
+ openai_attributes as OpenAIAttributes,
16
+ )
17
+ from opentelemetry.semconv_ai import SpanAttributes
18
+ from opentelemetry.trace.propagation import set_span_in_context
19
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
20
20
 
21
21
  OPENAI_LLM_USAGE_TOKEN_TYPES = ["prompt_tokens", "completion_tokens"]
22
22
  PROMPT_FILTER_KEY = "prompt_filter_results"
23
23
  PROMPT_ERROR = "prompt_error"
24
24
 
25
- # tiktoken encodings map for different model, key is model_name, value is tiktoken encoding
26
- tiktoken_encodings = {}
25
+ _PYDANTIC_VERSION = version("pydantic")
26
+
27
27
 
28
28
  logger = logging.getLogger(__name__)
29
29
 
30
30
 
31
- def should_send_prompts():
32
- return (
33
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
34
- ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
31
+ def _set_span_attribute(span, name, value):
32
+ if value is None or value == "":
33
+ return
35
34
 
35
+ if hasattr(openai, "NOT_GIVEN") and value == openai.NOT_GIVEN:
36
+ return
36
37
 
37
- def _set_span_attribute(span, name, value):
38
- if value is not None and value != "" and value != openai.NOT_GIVEN:
39
- span.set_attribute(name, value)
38
+ span.set_attribute(name, value)
40
39
 
41
40
 
42
41
  def _set_client_attributes(span, instance):
@@ -103,20 +102,30 @@ def set_tools_attributes(span, tools):
103
102
  )
104
103
 
105
104
 
106
- def _set_request_attributes(span, kwargs):
105
+ def _set_request_attributes(span, kwargs, instance=None):
107
106
  if not span.is_recording():
108
107
  return
109
108
 
110
109
  _set_api_attributes(span)
111
- _set_span_attribute(span, SpanAttributes.LLM_SYSTEM, "OpenAI")
112
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model"))
110
+
111
+ base_url = _get_openai_base_url(instance) if instance else ""
112
+ vendor = _get_vendor_from_url(base_url)
113
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_SYSTEM, vendor)
114
+
115
+ model = kwargs.get("model")
116
+ if vendor == "AWS" and model and "." in model:
117
+ model = _cross_region_check(model)
118
+ elif vendor == "OpenRouter":
119
+ model = _extract_model_name_from_provider_format(model)
120
+
121
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, model)
113
122
  _set_span_attribute(
114
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")
123
+ span, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")
115
124
  )
116
125
  _set_span_attribute(
117
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
126
+ span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, kwargs.get("temperature")
118
127
  )
119
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
128
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_P, kwargs.get("top_p"))
120
129
  _set_span_attribute(
121
130
  span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
122
131
  )
@@ -133,6 +142,52 @@ def _set_request_attributes(span, kwargs):
133
142
  _set_span_attribute(
134
143
  span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
135
144
  )
145
+ _set_span_attribute(
146
+ span, OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER, kwargs.get("service_tier")
147
+ )
148
+ if response_format := kwargs.get("response_format"):
149
+ # backward-compatible check for
150
+ # openai.types.shared_params.response_format_json_schema.ResponseFormatJSONSchema
151
+ if (
152
+ isinstance(response_format, dict)
153
+ and response_format.get("type") == "json_schema"
154
+ and response_format.get("json_schema")
155
+ ):
156
+ schema = dict(response_format.get("json_schema")).get("schema")
157
+ if schema:
158
+ _set_span_attribute(
159
+ span,
160
+ SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA,
161
+ json.dumps(schema),
162
+ )
163
+ elif (
164
+ isinstance(response_format, pydantic.BaseModel)
165
+ or (
166
+ hasattr(response_format, "model_json_schema")
167
+ and callable(response_format.model_json_schema)
168
+ )
169
+ ):
170
+ _set_span_attribute(
171
+ span,
172
+ SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA,
173
+ json.dumps(response_format.model_json_schema()),
174
+ )
175
+ else:
176
+ schema = None
177
+ try:
178
+ schema = json.dumps(pydantic.TypeAdapter(response_format).json_schema())
179
+ except Exception:
180
+ try:
181
+ schema = json.dumps(response_format)
182
+ except Exception:
183
+ pass
184
+
185
+ if schema:
186
+ _set_span_attribute(
187
+ span,
188
+ SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA,
189
+ schema,
190
+ )
136
191
 
137
192
 
138
193
  @dont_throw
@@ -143,20 +198,28 @@ def _set_response_attributes(span, response):
143
198
  if "error" in response:
144
199
  _set_span_attribute(
145
200
  span,
146
- f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_ERROR}",
201
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{PROMPT_ERROR}",
147
202
  json.dumps(response.get("error")),
148
203
  )
149
204
  return
150
205
 
151
- _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
206
+ response_model = response.get("model")
207
+ if response_model:
208
+ response_model = _extract_model_name_from_provider_format(response_model)
209
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model)
210
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_ID, response.get("id"))
152
211
 
153
212
  _set_span_attribute(
154
213
  span,
155
214
  SpanAttributes.LLM_OPENAI_RESPONSE_SYSTEM_FINGERPRINT,
156
215
  response.get("system_fingerprint"),
157
216
  )
217
+ _set_span_attribute(
218
+ span,
219
+ OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER,
220
+ response.get("service_tier"),
221
+ )
158
222
  _log_prompt_filter(span, response)
159
-
160
223
  usage = response.get("usage")
161
224
  if not usage:
162
225
  return
@@ -169,11 +232,17 @@ def _set_response_attributes(span, response):
169
232
  )
170
233
  _set_span_attribute(
171
234
  span,
172
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
235
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
173
236
  usage.get("completion_tokens"),
174
237
  )
175
238
  _set_span_attribute(
176
- span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage.get("prompt_tokens")
239
+ span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage.get("prompt_tokens")
240
+ )
241
+ prompt_tokens_details = dict(usage.get("prompt_tokens_details", {}))
242
+ _set_span_attribute(
243
+ span,
244
+ SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS,
245
+ prompt_tokens_details.get("cached_tokens", 0),
177
246
  )
178
247
  return
179
248
 
@@ -182,7 +251,7 @@ def _log_prompt_filter(span, response_dict):
182
251
  if response_dict.get("prompt_filter_results"):
183
252
  _set_span_attribute(
184
253
  span,
185
- f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_FILTER_KEY}",
254
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{PROMPT_FILTER_KEY}",
186
255
  json.dumps(response_dict.get("prompt_filter_results")),
187
256
  )
188
257
 
@@ -192,17 +261,17 @@ def _set_span_stream_usage(span, prompt_tokens, completion_tokens):
192
261
  if not span.is_recording():
193
262
  return
194
263
 
195
- if type(completion_tokens) is int and completion_tokens >= 0:
264
+ if isinstance(completion_tokens, int) and completion_tokens >= 0:
196
265
  _set_span_attribute(
197
- span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
266
+ span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens
198
267
  )
199
268
 
200
- if type(prompt_tokens) is int and prompt_tokens >= 0:
201
- _set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, prompt_tokens)
269
+ if isinstance(prompt_tokens, int) and prompt_tokens >= 0:
270
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens)
202
271
 
203
272
  if (
204
- type(prompt_tokens) is int
205
- and type(completion_tokens) is int
273
+ isinstance(prompt_tokens, int)
274
+ and isinstance(completion_tokens, int)
206
275
  and completion_tokens + prompt_tokens >= 0
207
276
  ):
208
277
  _set_span_attribute(
@@ -221,6 +290,53 @@ def _get_openai_base_url(instance):
221
290
  return ""
222
291
 
223
292
 
293
+ def _get_vendor_from_url(base_url):
294
+ if not base_url:
295
+ return "openai"
296
+
297
+ if "openai.azure.com" in base_url:
298
+ return "Azure"
299
+ elif "amazonaws.com" in base_url or "bedrock" in base_url:
300
+ return "AWS"
301
+ elif "googleapis.com" in base_url or "vertex" in base_url:
302
+ return "Google"
303
+ elif "openrouter.ai" in base_url:
304
+ return "OpenRouter"
305
+
306
+ return "openai"
307
+
308
+
309
+ def _cross_region_check(value):
310
+ if not value or "." not in value:
311
+ return value
312
+
313
+ prefixes = ["us", "us-gov", "eu", "apac"]
314
+ if any(value.startswith(prefix + ".") for prefix in prefixes):
315
+ parts = value.split(".")
316
+ if len(parts) > 2:
317
+ return parts[2]
318
+ else:
319
+ return value
320
+ else:
321
+ vendor, model = value.split(".", 1)
322
+ return model
323
+
324
+
325
+ def _extract_model_name_from_provider_format(model_name):
326
+ """
327
+ Extract model name from provider/model format.
328
+ E.g., 'openai/gpt-4o' -> 'gpt-4o', 'anthropic/claude-3-sonnet' -> 'claude-3-sonnet'
329
+ """
330
+ if not model_name:
331
+ return model_name
332
+
333
+ if "/" in model_name:
334
+ parts = model_name.split("/")
335
+ return parts[-1] # Return the last part (actual model name)
336
+
337
+ return model_name
338
+
339
+
224
340
  def is_streaming_response(response):
225
341
  if is_openai_v1():
226
342
  return isinstance(response, openai.Stream) or isinstance(
@@ -235,7 +351,7 @@ def is_streaming_response(response):
235
351
  def model_as_dict(model):
236
352
  if isinstance(model, dict):
237
353
  return model
238
- if version("pydantic") < "2.0.0":
354
+ if _PYDANTIC_VERSION < "2.0.0":
239
355
  return model.dict()
240
356
  if hasattr(model, "model_dump"):
241
357
  return model.model_dump()
@@ -245,30 +361,6 @@ def model_as_dict(model):
245
361
  return model
246
362
 
247
363
 
248
- def get_token_count_from_string(string: str, model_name: str):
249
- if not should_record_stream_token_usage():
250
- return None
251
-
252
- import tiktoken
253
-
254
- if tiktoken_encodings.get(model_name) is None:
255
- try:
256
- encoding = tiktoken.encoding_for_model(model_name)
257
- except KeyError as ex:
258
- # no such model_name in tiktoken
259
- logger.warning(
260
- f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}"
261
- )
262
- return None
263
-
264
- tiktoken_encodings[model_name] = encoding
265
- else:
266
- encoding = tiktoken_encodings.get(model_name)
267
-
268
- token_count = len(encoding.encode(string))
269
- return token_count
270
-
271
-
272
364
  def _token_type(token_type: str):
273
365
  if token_type == "prompt_tokens":
274
366
  return "input"
@@ -282,11 +374,12 @@ def metric_shared_attributes(
282
374
  response_model: str, operation: str, server_address: str, is_streaming: bool = False
283
375
  ):
284
376
  attributes = Config.get_common_metrics_attributes()
377
+ vendor = _get_vendor_from_url(server_address)
285
378
 
286
379
  return {
287
380
  **attributes,
288
- SpanAttributes.LLM_SYSTEM: "openai",
289
- SpanAttributes.LLM_RESPONSE_MODEL: response_model,
381
+ GenAIAttributes.GEN_AI_SYSTEM: vendor,
382
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_model,
290
383
  "gen_ai.operation.name": operation,
291
384
  "server.address": server_address,
292
385
  "stream": is_streaming,
@@ -294,7 +387,13 @@ def metric_shared_attributes(
294
387
 
295
388
 
296
389
  def propagate_trace_context(span, kwargs):
297
- extra_headers = kwargs.get("extra_headers", {})
298
- ctx = set_span_in_context(span)
299
- TraceContextTextMapPropagator().inject(extra_headers, context=ctx)
300
- kwargs["extra_headers"] = extra_headers
390
+ if is_openai_v1():
391
+ extra_headers = kwargs.get("extra_headers", {})
392
+ ctx = set_span_in_context(span)
393
+ TraceContextTextMapPropagator().inject(extra_headers, context=ctx)
394
+ kwargs["extra_headers"] = extra_headers
395
+ else:
396
+ headers = kwargs.get("headers", {})
397
+ ctx = set_span_in_context(span)
398
+ TraceContextTextMapPropagator().inject(headers, context=ctx)
399
+ kwargs["headers"] = headers