opentelemetry-instrumentation-openai 0.38.6__tar.gz → 0.49.6__tar.gz

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 (24) hide show
  1. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/PKG-INFO +7 -7
  2. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/__init__.py +3 -4
  3. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/shared/__init__.py +148 -65
  4. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +537 -231
  5. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +292 -0
  6. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/shared/config.py +15 -0
  7. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +85 -31
  8. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/shared/event_emitter.py +108 -0
  9. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  10. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +1 -1
  11. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
  12. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/utils.py +40 -9
  13. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/v0/__init__.py +32 -11
  14. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/v1/__init__.py +177 -69
  15. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +329 -0
  16. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +35 -17
  17. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +1113 -0
  18. opentelemetry_instrumentation_openai-0.49.6/opentelemetry/instrumentation/openai/version.py +1 -0
  19. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/pyproject.toml +8 -8
  20. opentelemetry_instrumentation_openai-0.38.6/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +0 -240
  21. opentelemetry_instrumentation_openai-0.38.6/opentelemetry/instrumentation/openai/shared/config.py +0 -10
  22. opentelemetry_instrumentation_openai-0.38.6/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -231
  23. opentelemetry_instrumentation_openai-0.38.6/opentelemetry/instrumentation/openai/version.py +0 -1
  24. {opentelemetry_instrumentation_openai-0.38.6 → opentelemetry_instrumentation_openai-0.49.6}/README.md +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-openai
3
- Version: 0.38.6
3
+ Version: 0.49.6
4
4
  Summary: OpenTelemetry OpenAI instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -13,12 +13,12 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
16
17
  Provides-Extra: instruments
17
- Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
18
- Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
19
- Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
20
- Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.2)
21
- Requires-Dist: tiktoken (>=0.6.0,<1)
18
+ Requires-Dist: opentelemetry-api (>=1.38.0,<2.0.0)
19
+ Requires-Dist: opentelemetry-instrumentation (>=0.59b0)
20
+ Requires-Dist: opentelemetry-semantic-conventions (>=0.59b0)
21
+ Requires-Dist: opentelemetry-semantic-conventions-ai (>=0.4.13,<0.5.0)
22
22
  Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-openai
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -1,10 +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
6
+ from typing_extensions import Coroutine
8
7
 
9
8
  _instruments = ("openai >= 0.27.0",)
10
9
 
@@ -15,21 +14,21 @@ class OpenAIInstrumentor(BaseInstrumentor):
15
14
  def __init__(
16
15
  self,
17
16
  enrich_assistant: bool = False,
18
- enrich_token_usage: bool = False,
19
17
  exception_logger=None,
20
18
  get_common_metrics_attributes: Callable[[], dict] = lambda: {},
21
19
  upload_base64_image: Optional[
22
20
  Callable[[str, str, str, str], Coroutine[None, None, str]]
23
21
  ] = lambda *args: "",
24
22
  enable_trace_context_propagation: bool = True,
23
+ use_legacy_attributes: bool = True,
25
24
  ):
26
25
  super().__init__()
27
26
  Config.enrich_assistant = enrich_assistant
28
- Config.enrich_token_usage = enrich_token_usage
29
27
  Config.exception_logger = exception_logger
30
28
  Config.get_common_metrics_attributes = get_common_metrics_attributes
31
29
  Config.upload_base64_image = upload_base64_image
32
30
  Config.enable_trace_context_propagation = enable_trace_context_propagation
31
+ Config.use_legacy_attributes = use_legacy_attributes
33
32
 
34
33
  def instrumentation_dependencies(self) -> Collection[str]:
35
34
  return _instruments
@@ -1,25 +1,22 @@
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._incubating.attributes.gen_ai_attributes import (
15
- GEN_AI_RESPONSE_ID,
16
- )
17
- from opentelemetry.semconv_ai import SpanAttributes
18
9
  from opentelemetry.instrumentation.openai.utils import (
19
10
  dont_throw,
20
11
  is_openai_v1,
21
- should_record_stream_token_usage,
22
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
23
20
 
24
21
  OPENAI_LLM_USAGE_TOKEN_TYPES = ["prompt_tokens", "completion_tokens"]
25
22
  PROMPT_FILTER_KEY = "prompt_filter_results"
@@ -27,18 +24,10 @@ PROMPT_ERROR = "prompt_error"
27
24
 
28
25
  _PYDANTIC_VERSION = version("pydantic")
29
26
 
30
- # tiktoken encodings map for different model, key is model_name, value is tiktoken encoding
31
- tiktoken_encodings = {}
32
27
 
33
28
  logger = logging.getLogger(__name__)
34
29
 
35
30
 
36
- def should_send_prompts():
37
- return (
38
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
39
- ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
40
-
41
-
42
31
  def _set_span_attribute(span, name, value):
43
32
  if value is None or value == "":
44
33
  return
@@ -113,20 +102,30 @@ def set_tools_attributes(span, tools):
113
102
  )
114
103
 
115
104
 
116
- def _set_request_attributes(span, kwargs):
105
+ def _set_request_attributes(span, kwargs, instance=None):
117
106
  if not span.is_recording():
118
107
  return
119
108
 
120
109
  _set_api_attributes(span)
121
- _set_span_attribute(span, SpanAttributes.LLM_SYSTEM, "OpenAI")
122
- _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)
123
122
  _set_span_attribute(
124
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")
123
+ span, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")
125
124
  )
126
125
  _set_span_attribute(
127
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
126
+ span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, kwargs.get("temperature")
128
127
  )
129
- _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"))
130
129
  _set_span_attribute(
131
130
  span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
132
131
  )
@@ -143,6 +142,52 @@ def _set_request_attributes(span, kwargs):
143
142
  _set_span_attribute(
144
143
  span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
145
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
+ )
146
191
 
147
192
 
148
193
  @dont_throw
@@ -153,19 +198,27 @@ def _set_response_attributes(span, response):
153
198
  if "error" in response:
154
199
  _set_span_attribute(
155
200
  span,
156
- f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_ERROR}",
201
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{PROMPT_ERROR}",
157
202
  json.dumps(response.get("error")),
158
203
  )
159
204
  return
160
205
 
161
- _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
162
- _set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
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"))
163
211
 
164
212
  _set_span_attribute(
165
213
  span,
166
214
  SpanAttributes.LLM_OPENAI_RESPONSE_SYSTEM_FINGERPRINT,
167
215
  response.get("system_fingerprint"),
168
216
  )
217
+ _set_span_attribute(
218
+ span,
219
+ OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER,
220
+ response.get("service_tier"),
221
+ )
169
222
  _log_prompt_filter(span, response)
170
223
  usage = response.get("usage")
171
224
  if not usage:
@@ -179,11 +232,17 @@ def _set_response_attributes(span, response):
179
232
  )
180
233
  _set_span_attribute(
181
234
  span,
182
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
235
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
183
236
  usage.get("completion_tokens"),
184
237
  )
185
238
  _set_span_attribute(
186
- 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),
187
246
  )
188
247
  return
189
248
 
@@ -192,7 +251,7 @@ def _log_prompt_filter(span, response_dict):
192
251
  if response_dict.get("prompt_filter_results"):
193
252
  _set_span_attribute(
194
253
  span,
195
- f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_FILTER_KEY}",
254
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{PROMPT_FILTER_KEY}",
196
255
  json.dumps(response_dict.get("prompt_filter_results")),
197
256
  )
198
257
 
@@ -202,17 +261,17 @@ def _set_span_stream_usage(span, prompt_tokens, completion_tokens):
202
261
  if not span.is_recording():
203
262
  return
204
263
 
205
- if type(completion_tokens) is int and completion_tokens >= 0:
264
+ if isinstance(completion_tokens, int) and completion_tokens >= 0:
206
265
  _set_span_attribute(
207
- span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
266
+ span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens
208
267
  )
209
268
 
210
- if type(prompt_tokens) is int and prompt_tokens >= 0:
211
- _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)
212
271
 
213
272
  if (
214
- type(prompt_tokens) is int
215
- and type(completion_tokens) is int
273
+ isinstance(prompt_tokens, int)
274
+ and isinstance(completion_tokens, int)
216
275
  and completion_tokens + prompt_tokens >= 0
217
276
  ):
218
277
  _set_span_attribute(
@@ -231,6 +290,53 @@ def _get_openai_base_url(instance):
231
290
  return ""
232
291
 
233
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
+
234
340
  def is_streaming_response(response):
235
341
  if is_openai_v1():
236
342
  return isinstance(response, openai.Stream) or isinstance(
@@ -255,30 +361,6 @@ def model_as_dict(model):
255
361
  return model
256
362
 
257
363
 
258
- def get_token_count_from_string(string: str, model_name: str):
259
- if not should_record_stream_token_usage():
260
- return None
261
-
262
- import tiktoken
263
-
264
- if tiktoken_encodings.get(model_name) is None:
265
- try:
266
- encoding = tiktoken.encoding_for_model(model_name)
267
- except KeyError as ex:
268
- # no such model_name in tiktoken
269
- logger.warning(
270
- f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}"
271
- )
272
- return None
273
-
274
- tiktoken_encodings[model_name] = encoding
275
- else:
276
- encoding = tiktoken_encodings.get(model_name)
277
-
278
- token_count = len(encoding.encode(string))
279
- return token_count
280
-
281
-
282
364
  def _token_type(token_type: str):
283
365
  if token_type == "prompt_tokens":
284
366
  return "input"
@@ -292,11 +374,12 @@ def metric_shared_attributes(
292
374
  response_model: str, operation: str, server_address: str, is_streaming: bool = False
293
375
  ):
294
376
  attributes = Config.get_common_metrics_attributes()
377
+ vendor = _get_vendor_from_url(server_address)
295
378
 
296
379
  return {
297
380
  **attributes,
298
- SpanAttributes.LLM_SYSTEM: "openai",
299
- SpanAttributes.LLM_RESPONSE_MODEL: response_model,
381
+ GenAIAttributes.GEN_AI_SYSTEM: vendor,
382
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_model,
300
383
  "gen_ai.operation.name": operation,
301
384
  "server.address": server_address,
302
385
  "stream": is_streaming,