lmnr 0.6.18__py3-none-any.whl → 0.6.19__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.
Files changed (28) hide show
  1. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +55 -20
  2. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +23 -0
  3. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  4. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +442 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1024 -0
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +297 -0
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +308 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +185 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +358 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +319 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +132 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +626 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  19. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +1 -3
  20. lmnr/sdk/browser/browser_use_otel.py +1 -1
  21. lmnr/sdk/browser/patchright_otel.py +0 -14
  22. lmnr/sdk/browser/playwright_otel.py +16 -130
  23. lmnr/sdk/browser/pw_utils.py +45 -31
  24. lmnr/version.py +1 -1
  25. {lmnr-0.6.18.dist-info → lmnr-0.6.19.dist-info}/METADATA +2 -5
  26. {lmnr-0.6.18.dist-info → lmnr-0.6.19.dist-info}/RECORD +28 -11
  27. {lmnr-0.6.18.dist-info → lmnr-0.6.19.dist-info}/WHEEL +1 -1
  28. {lmnr-0.6.18.dist-info → lmnr-0.6.19.dist-info}/entry_points.txt +0 -0
@@ -11,6 +11,7 @@ from google.genai import types
11
11
  from .config import (
12
12
  Config,
13
13
  )
14
+ from .schema_utils import SchemaJSONEncoder, process_schema
14
15
  from .utils import (
15
16
  dont_throw,
16
17
  get_content,
@@ -24,8 +25,9 @@ from opentelemetry.trace import Tracer
24
25
  from wrapt import wrap_function_wrapper
25
26
 
26
27
  from opentelemetry import context as context_api
27
- from opentelemetry.trace import get_tracer, SpanKind, Span
28
+ from opentelemetry.trace import get_tracer, SpanKind, Span, Status, StatusCode
28
29
  from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
30
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
29
31
 
30
32
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
31
33
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
@@ -78,7 +80,7 @@ WRAPPED_METHODS = [
78
80
 
79
81
  def should_send_prompts():
80
82
  return (
81
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
83
+ os.getenv("LAMINAR_TRACE_CONTENT") or "true"
82
84
  ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
83
85
 
84
86
 
@@ -128,6 +130,29 @@ def _set_request_attributes(span, args, kwargs):
128
130
  span, gen_ai_attributes.GEN_AI_REQUEST_SEED, config_dict.get("seed")
129
131
  )
130
132
 
133
+ if schema := config_dict.get("response_schema"):
134
+ try:
135
+ set_span_attribute(
136
+ span,
137
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
138
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
139
+ "gen_ai.request.structured_output_schema",
140
+ json.dumps(process_schema(schema), cls=SchemaJSONEncoder),
141
+ )
142
+ except Exception:
143
+ pass
144
+ elif json_schema := config_dict.get("response_json_schema"):
145
+ try:
146
+ set_span_attribute(
147
+ span,
148
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
149
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
150
+ "gen_ai.request.structured_output_schema",
151
+ json.dumps(json_schema),
152
+ )
153
+ except Exception:
154
+ pass
155
+
131
156
  tools: list[types.FunctionDeclaration] = []
132
157
  arg_tools = config_dict.get("tools", kwargs.get("tools"))
133
158
  if arg_tools:
@@ -454,16 +479,20 @@ def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
454
479
  if span.is_recording():
455
480
  _set_request_attributes(span, args, kwargs)
456
481
 
457
- if to_wrap.get("is_streaming"):
458
- return _build_from_streaming_response(span, wrapped(*args, **kwargs))
459
- else:
482
+ try:
460
483
  response = wrapped(*args, **kwargs)
461
-
462
- if span.is_recording():
463
- _set_response_attributes(span, response)
464
-
465
- span.end()
466
- return response
484
+ if to_wrap.get("is_streaming"):
485
+ return _build_from_streaming_response(span, response)
486
+ if span.is_recording():
487
+ _set_response_attributes(span, response)
488
+ span.end()
489
+ return response
490
+ except Exception as e:
491
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
492
+ span.record_exception(e)
493
+ span.set_status(Status(StatusCode.ERROR, str(e)))
494
+ span.end()
495
+ raise e
467
496
 
468
497
 
469
498
  @with_tracer_wrapper
@@ -485,16 +514,22 @@ async def _awrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
485
514
  if span.is_recording():
486
515
  _set_request_attributes(span, args, kwargs)
487
516
 
488
- if to_wrap.get("is_streaming"):
489
- return _abuild_from_streaming_response(span, await wrapped(*args, **kwargs))
490
- else:
517
+ try:
491
518
  response = await wrapped(*args, **kwargs)
492
-
493
- if span.is_recording():
494
- _set_response_attributes(span, response)
495
-
496
- span.end()
497
- return response
519
+ if to_wrap.get("is_streaming"):
520
+ return _abuild_from_streaming_response(span, response)
521
+ else:
522
+ if span.is_recording():
523
+ _set_response_attributes(span, response)
524
+
525
+ span.end()
526
+ return response
527
+ except Exception as e:
528
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
529
+ span.record_exception(e)
530
+ span.set_status(Status(StatusCode.ERROR, str(e)))
531
+ span.end()
532
+ raise e
498
533
 
499
534
 
500
535
  class GoogleGenAiSdkInstrumentor(BaseInstrumentor):
@@ -0,0 +1,23 @@
1
+ from typing import Any
2
+ from google.genai._api_client import BaseApiClient
3
+ from google.genai._transformers import t_schema
4
+ from google.genai.types import JSONSchemaType
5
+
6
+ import json
7
+
8
+ DUMMY_CLIENT = BaseApiClient(api_key="dummy")
9
+
10
+
11
+ def process_schema(schema: Any) -> dict[str, Any]:
12
+ # The only thing we need from the client is the t_schema function
13
+ json_schema = t_schema(DUMMY_CLIENT, schema).json_schema.model_dump(
14
+ exclude_unset=True, exclude_none=True
15
+ )
16
+ return json_schema
17
+
18
+
19
+ class SchemaJSONEncoder(json.JSONEncoder):
20
+ def default(self, o: Any) -> Any:
21
+ if isinstance(o, JSONSchemaType):
22
+ return o.value
23
+ return super().default(o)
@@ -0,0 +1,61 @@
1
+ """
2
+ Initially copied over from openllmetry, commit
3
+ b3a18c9f7e6ff2368c8fb0bc35fd9123f11121c4
4
+ """
5
+
6
+ from typing import Callable, Collection, Optional
7
+
8
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
9
+ from .shared.config import Config
10
+ from .utils import is_openai_v1
11
+ from typing_extensions import Coroutine
12
+
13
+ _instruments = ("openai >= 0.27.0",)
14
+
15
+
16
+ class OpenAIInstrumentor(BaseInstrumentor):
17
+ """An instrumentor for OpenAI's client library."""
18
+
19
+ def __init__(
20
+ self,
21
+ enrich_assistant: bool = False,
22
+ enrich_token_usage: bool = False,
23
+ exception_logger=None,
24
+ get_common_metrics_attributes: Callable[[], dict] = lambda: {},
25
+ upload_base64_image: Optional[
26
+ Callable[[str, str, str, str], Coroutine[None, None, str]]
27
+ ] = lambda *args: "",
28
+ enable_trace_context_propagation: bool = True,
29
+ use_legacy_attributes: bool = True,
30
+ ):
31
+ super().__init__()
32
+ Config.enrich_assistant = enrich_assistant
33
+ Config.enrich_token_usage = enrich_token_usage
34
+ Config.exception_logger = exception_logger
35
+ Config.get_common_metrics_attributes = get_common_metrics_attributes
36
+ Config.upload_base64_image = upload_base64_image
37
+ Config.enable_trace_context_propagation = enable_trace_context_propagation
38
+ Config.use_legacy_attributes = use_legacy_attributes
39
+
40
+ def instrumentation_dependencies(self) -> Collection[str]:
41
+ return _instruments
42
+
43
+ def _instrument(self, **kwargs):
44
+ if is_openai_v1():
45
+ from .v1 import OpenAIV1Instrumentor
46
+
47
+ OpenAIV1Instrumentor().instrument(**kwargs)
48
+ else:
49
+ from .v0 import OpenAIV0Instrumentor
50
+
51
+ OpenAIV0Instrumentor().instrument(**kwargs)
52
+
53
+ def _uninstrument(self, **kwargs):
54
+ if is_openai_v1():
55
+ from .v1 import OpenAIV1Instrumentor
56
+
57
+ OpenAIV1Instrumentor().uninstrument(**kwargs)
58
+ else:
59
+ from .v0 import OpenAIV0Instrumentor
60
+
61
+ OpenAIV0Instrumentor().uninstrument(**kwargs)
@@ -0,0 +1,442 @@
1
+ import json
2
+ import logging
3
+ import types
4
+ from importlib.metadata import version
5
+
6
+ from ..shared.config import Config
7
+ from ..utils import (
8
+ dont_throw,
9
+ is_openai_v1,
10
+ should_record_stream_token_usage,
11
+ )
12
+ from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
13
+ GEN_AI_RESPONSE_ID,
14
+ )
15
+ from opentelemetry.semconv_ai import SpanAttributes
16
+ from opentelemetry.trace.propagation import set_span_in_context
17
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
18
+ import openai
19
+ import pydantic
20
+
21
+ OPENAI_LLM_USAGE_TOKEN_TYPES = ["prompt_tokens", "completion_tokens"]
22
+ PROMPT_FILTER_KEY = "prompt_filter_results"
23
+ PROMPT_ERROR = "prompt_error"
24
+
25
+ _PYDANTIC_VERSION = version("pydantic")
26
+
27
+ # tiktoken encodings map for different model, key is model_name, value is tiktoken encoding
28
+ tiktoken_encodings = {}
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _set_span_attribute(span, name, value):
34
+ if value is None or value == "":
35
+ return
36
+
37
+ if hasattr(openai, "NOT_GIVEN") and value == openai.NOT_GIVEN:
38
+ return
39
+
40
+ span.set_attribute(name, value)
41
+
42
+
43
+ def _set_client_attributes(span, instance):
44
+ if not span.is_recording():
45
+ return
46
+
47
+ if not is_openai_v1():
48
+ return
49
+
50
+ client = instance._client # pylint: disable=protected-access
51
+ if isinstance(client, (openai.AsyncOpenAI, openai.OpenAI)):
52
+ _set_span_attribute(
53
+ span, SpanAttributes.LLM_OPENAI_API_BASE, str(client.base_url)
54
+ )
55
+ if isinstance(client, (openai.AsyncAzureOpenAI, openai.AzureOpenAI)):
56
+ _set_span_attribute(
57
+ span, SpanAttributes.LLM_OPENAI_API_VERSION, client._api_version
58
+ ) # pylint: disable=protected-access
59
+
60
+
61
+ def _set_api_attributes(span):
62
+ if not span.is_recording():
63
+ return
64
+
65
+ if is_openai_v1():
66
+ return
67
+
68
+ base_url = openai.base_url if hasattr(openai, "base_url") else openai.api_base
69
+
70
+ _set_span_attribute(span, SpanAttributes.LLM_OPENAI_API_BASE, base_url)
71
+ _set_span_attribute(span, SpanAttributes.LLM_OPENAI_API_TYPE, openai.api_type)
72
+ _set_span_attribute(span, SpanAttributes.LLM_OPENAI_API_VERSION, openai.api_version)
73
+
74
+ return
75
+
76
+
77
+ def _set_functions_attributes(span, functions):
78
+ if not functions:
79
+ return
80
+
81
+ for i, function in enumerate(functions):
82
+ prefix = f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{i}"
83
+ _set_span_attribute(span, f"{prefix}.name", function.get("name"))
84
+ _set_span_attribute(span, f"{prefix}.description", function.get("description"))
85
+ _set_span_attribute(
86
+ span, f"{prefix}.parameters", json.dumps(function.get("parameters"))
87
+ )
88
+
89
+
90
+ def set_tools_attributes(span, tools):
91
+ if not tools:
92
+ return
93
+
94
+ for i, tool in enumerate(tools):
95
+ function = tool.get("function")
96
+ if not function:
97
+ continue
98
+
99
+ prefix = f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{i}"
100
+ _set_span_attribute(span, f"{prefix}.name", function.get("name"))
101
+ _set_span_attribute(span, f"{prefix}.description", function.get("description"))
102
+ _set_span_attribute(
103
+ span, f"{prefix}.parameters", json.dumps(function.get("parameters"))
104
+ )
105
+
106
+
107
+ def _set_request_attributes(span, kwargs, instance=None):
108
+ if not span.is_recording():
109
+ return
110
+
111
+ _set_api_attributes(span)
112
+
113
+ base_url = _get_openai_base_url(instance) if instance else ""
114
+ vendor = _get_vendor_from_url(base_url)
115
+ _set_span_attribute(span, SpanAttributes.LLM_SYSTEM, vendor)
116
+
117
+ model = kwargs.get("model")
118
+ if vendor == "AWS" and model and "." in model:
119
+ model = _cross_region_check(model)
120
+ elif vendor == "OpenRouter":
121
+ model = _extract_model_name_from_provider_format(model)
122
+
123
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, model)
124
+ _set_span_attribute(
125
+ span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")
126
+ )
127
+ _set_span_attribute(
128
+ span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
129
+ )
130
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
131
+ _set_span_attribute(
132
+ span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
133
+ )
134
+ _set_span_attribute(
135
+ span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
136
+ )
137
+ _set_span_attribute(span, SpanAttributes.LLM_USER, kwargs.get("user"))
138
+ _set_span_attribute(span, SpanAttributes.LLM_HEADERS, str(kwargs.get("headers")))
139
+ # The new OpenAI SDK removed the `headers` and create new field called `extra_headers`
140
+ if kwargs.get("extra_headers") is not None:
141
+ _set_span_attribute(
142
+ span, SpanAttributes.LLM_HEADERS, str(kwargs.get("extra_headers"))
143
+ )
144
+ _set_span_attribute(
145
+ span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
146
+ )
147
+ if response_format := kwargs.get("response_format"):
148
+ # backward-compatible check for
149
+ # openai.types.shared_params.response_format_json_schema.ResponseFormatJSONSchema
150
+ if (
151
+ isinstance(response_format, dict)
152
+ and response_format.get("type") == "json_schema"
153
+ and response_format.get("json_schema")
154
+ ):
155
+ schema = dict(response_format.get("json_schema")).get("schema")
156
+ if schema:
157
+ _set_span_attribute(
158
+ span,
159
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
160
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
161
+ "gen_ai.request.structured_output_schema",
162
+ json.dumps(schema),
163
+ )
164
+ else:
165
+ try:
166
+ from openai.lib._parsing._completions import (
167
+ type_to_response_format_param,
168
+ )
169
+
170
+ response_format_param = type_to_response_format_param(response_format)
171
+ if response_format_param.get("type") == "json_schema":
172
+ schema = response_format_param.get("json_schema").get("schema")
173
+ if schema:
174
+ _set_span_attribute(
175
+ span,
176
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
177
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
178
+ "gen_ai.request.structured_output_schema",
179
+ json.dumps(schema),
180
+ )
181
+ except (ImportError, TypeError, AttributeError):
182
+ # if we fail to import from openai.lib._parsing._completions,
183
+ # we fallback to the pydantic-based approach
184
+ if isinstance(response_format, pydantic.BaseModel) or (
185
+ hasattr(response_format, "model_json_schema")
186
+ and callable(response_format.model_json_schema)
187
+ ):
188
+ _set_span_attribute(
189
+ span,
190
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
191
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
192
+ "gen_ai.request.structured_output_schema",
193
+ json.dumps(response_format.model_json_schema()),
194
+ )
195
+ else:
196
+ schema = None
197
+ try:
198
+ schema = json.dumps(
199
+ pydantic.TypeAdapter(response_format).json_schema()
200
+ )
201
+ except Exception:
202
+ try:
203
+ schema = json.dumps(response_format)
204
+ except Exception:
205
+ pass
206
+
207
+ if schema:
208
+ _set_span_attribute(
209
+ span,
210
+ # TODO: change to SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA
211
+ # when we upgrade to opentelemetry-semantic-conventions-ai>=0.4.10
212
+ "gen_ai.request.structured_output_schema",
213
+ schema,
214
+ )
215
+
216
+
217
+ @dont_throw
218
+ def _set_response_attributes(span, response):
219
+ if not span.is_recording():
220
+ return
221
+
222
+ if "error" in response:
223
+ _set_span_attribute(
224
+ span,
225
+ f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_ERROR}",
226
+ json.dumps(response.get("error")),
227
+ )
228
+ return
229
+
230
+ response_model = response.get("model")
231
+ if response_model:
232
+ response_model = _extract_model_name_from_provider_format(response_model)
233
+ _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response_model)
234
+ _set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
235
+
236
+ _set_span_attribute(
237
+ span,
238
+ SpanAttributes.LLM_OPENAI_RESPONSE_SYSTEM_FINGERPRINT,
239
+ response.get("system_fingerprint"),
240
+ )
241
+ _log_prompt_filter(span, response)
242
+ usage = response.get("usage")
243
+ if not usage:
244
+ return
245
+
246
+ if is_openai_v1() and not isinstance(usage, dict):
247
+ usage = usage.__dict__
248
+
249
+ _set_span_attribute(
250
+ span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.get("total_tokens")
251
+ )
252
+ _set_span_attribute(
253
+ span,
254
+ SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
255
+ usage.get("completion_tokens"),
256
+ )
257
+ _set_span_attribute(
258
+ span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage.get("prompt_tokens")
259
+ )
260
+ prompt_tokens_details = dict(usage.get("prompt_tokens_details", {}))
261
+ _set_span_attribute(
262
+ span,
263
+ SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS,
264
+ prompt_tokens_details.get("cached_tokens", 0),
265
+ )
266
+ return
267
+
268
+
269
+ def _log_prompt_filter(span, response_dict):
270
+ if response_dict.get("prompt_filter_results"):
271
+ _set_span_attribute(
272
+ span,
273
+ f"{SpanAttributes.LLM_PROMPTS}.{PROMPT_FILTER_KEY}",
274
+ json.dumps(response_dict.get("prompt_filter_results")),
275
+ )
276
+
277
+
278
+ @dont_throw
279
+ def _set_span_stream_usage(span, prompt_tokens, completion_tokens):
280
+ if not span.is_recording():
281
+ return
282
+
283
+ if isinstance(completion_tokens, int) and completion_tokens >= 0:
284
+ _set_span_attribute(
285
+ span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
286
+ )
287
+
288
+ if isinstance(prompt_tokens, int) and prompt_tokens >= 0:
289
+ _set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, prompt_tokens)
290
+
291
+ if (
292
+ isinstance(prompt_tokens, int)
293
+ and isinstance(completion_tokens, int)
294
+ and completion_tokens + prompt_tokens >= 0
295
+ ):
296
+ _set_span_attribute(
297
+ span,
298
+ SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
299
+ completion_tokens + prompt_tokens,
300
+ )
301
+
302
+
303
+ def _get_openai_base_url(instance):
304
+ if hasattr(instance, "_client"):
305
+ client = instance._client # pylint: disable=protected-access
306
+ if isinstance(client, (openai.AsyncOpenAI, openai.OpenAI)):
307
+ return str(client.base_url)
308
+
309
+ return ""
310
+
311
+
312
+ def _get_vendor_from_url(base_url):
313
+ if not base_url:
314
+ return "openai"
315
+
316
+ if "openai.azure.com" in base_url:
317
+ return "Azure"
318
+ elif "amazonaws.com" in base_url or "bedrock" in base_url:
319
+ return "AWS"
320
+ elif "googleapis.com" in base_url or "vertex" in base_url:
321
+ return "Google"
322
+ elif "openrouter.ai" in base_url:
323
+ return "OpenRouter"
324
+
325
+ return "openai"
326
+
327
+
328
+ def _cross_region_check(value):
329
+ if not value or "." not in value:
330
+ return value
331
+
332
+ prefixes = ["us", "us-gov", "eu", "apac"]
333
+ if any(value.startswith(prefix + ".") for prefix in prefixes):
334
+ parts = value.split(".")
335
+ if len(parts) > 2:
336
+ return parts[2]
337
+ else:
338
+ return value
339
+ else:
340
+ vendor, model = value.split(".", 1)
341
+ return model
342
+
343
+
344
+ def _extract_model_name_from_provider_format(model_name):
345
+ """
346
+ Extract model name from provider/model format.
347
+ E.g., 'openai/gpt-4o' -> 'gpt-4o', 'anthropic/claude-3-sonnet' -> 'claude-3-sonnet'
348
+ """
349
+ if not model_name:
350
+ return model_name
351
+
352
+ if "/" in model_name:
353
+ parts = model_name.split("/")
354
+ return parts[-1] # Return the last part (actual model name)
355
+
356
+ return model_name
357
+
358
+
359
+ def is_streaming_response(response):
360
+ if is_openai_v1():
361
+ return isinstance(response, openai.Stream) or isinstance(
362
+ response, openai.AsyncStream
363
+ )
364
+
365
+ return isinstance(response, types.GeneratorType) or isinstance(
366
+ response, types.AsyncGeneratorType
367
+ )
368
+
369
+
370
+ def model_as_dict(model):
371
+ if isinstance(model, dict):
372
+ return model
373
+ if _PYDANTIC_VERSION < "2.0.0":
374
+ return model.dict()
375
+ if hasattr(model, "model_dump"):
376
+ return model.model_dump()
377
+ elif hasattr(model, "parse"): # Raw API response
378
+ return model_as_dict(model.parse())
379
+ else:
380
+ return model
381
+
382
+
383
+ def get_token_count_from_string(string: str, model_name: str):
384
+ if not should_record_stream_token_usage():
385
+ return None
386
+
387
+ import tiktoken
388
+
389
+ if tiktoken_encodings.get(model_name) is None:
390
+ try:
391
+ encoding = tiktoken.encoding_for_model(model_name)
392
+ except KeyError as ex:
393
+ # no such model_name in tiktoken
394
+ logger.warning(
395
+ f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}"
396
+ )
397
+ return None
398
+
399
+ tiktoken_encodings[model_name] = encoding
400
+ else:
401
+ encoding = tiktoken_encodings.get(model_name)
402
+
403
+ token_count = len(encoding.encode(string))
404
+ return token_count
405
+
406
+
407
+ def _token_type(token_type: str):
408
+ if token_type == "prompt_tokens":
409
+ return "input"
410
+ elif token_type == "completion_tokens":
411
+ return "output"
412
+
413
+ return None
414
+
415
+
416
+ def metric_shared_attributes(
417
+ response_model: str, operation: str, server_address: str, is_streaming: bool = False
418
+ ):
419
+ attributes = Config.get_common_metrics_attributes()
420
+ vendor = _get_vendor_from_url(server_address)
421
+
422
+ return {
423
+ **attributes,
424
+ SpanAttributes.LLM_SYSTEM: vendor,
425
+ SpanAttributes.LLM_RESPONSE_MODEL: response_model,
426
+ "gen_ai.operation.name": operation,
427
+ "server.address": server_address,
428
+ "stream": is_streaming,
429
+ }
430
+
431
+
432
+ def propagate_trace_context(span, kwargs):
433
+ if is_openai_v1():
434
+ extra_headers = kwargs.get("extra_headers", {})
435
+ ctx = set_span_in_context(span)
436
+ TraceContextTextMapPropagator().inject(extra_headers, context=ctx)
437
+ kwargs["extra_headers"] = extra_headers
438
+ else:
439
+ headers = kwargs.get("headers", {})
440
+ ctx = set_span_in_context(span)
441
+ TraceContextTextMapPropagator().inject(headers, context=ctx)
442
+ kwargs["headers"] = headers