lmnr 0.4.53.dev0__py3-none-any.whl → 0.7.26__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 (133) hide show
  1. lmnr/__init__.py +32 -11
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/cli/evals.py +111 -0
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +70 -0
  7. lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
  8. lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
  9. lmnr/opentelemetry_lib/litellm/utils.py +100 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  56. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
  57. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  58. lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
  59. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
  60. lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
  61. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  62. lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
  63. lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
  64. lmnr/opentelemetry_lib/tracing/processor.py +193 -0
  65. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  66. lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
  67. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  68. lmnr/opentelemetry_lib/utils/package_check.py +18 -0
  69. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  70. lmnr/sdk/browser/__init__.py +0 -0
  71. lmnr/sdk/browser/background_send_events.py +158 -0
  72. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  73. lmnr/sdk/browser/browser_use_otel.py +142 -0
  74. lmnr/sdk/browser/bubus_otel.py +71 -0
  75. lmnr/sdk/browser/cdp_utils.py +518 -0
  76. lmnr/sdk/browser/inject_script.js +514 -0
  77. lmnr/sdk/browser/patchright_otel.py +151 -0
  78. lmnr/sdk/browser/playwright_otel.py +322 -0
  79. lmnr/sdk/browser/pw_utils.py +363 -0
  80. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  81. lmnr/sdk/browser/utils.py +70 -0
  82. lmnr/sdk/client/asynchronous/async_client.py +180 -0
  83. lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
  84. lmnr/sdk/client/asynchronous/resources/base.py +32 -0
  85. lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
  86. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  87. lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
  88. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  89. lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
  90. lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
  91. lmnr/sdk/client/synchronous/resources/base.py +32 -0
  92. lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
  93. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  94. lmnr/sdk/client/synchronous/resources/evals.py +263 -0
  95. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  96. lmnr/sdk/client/synchronous/resources/tags.py +83 -0
  97. lmnr/sdk/client/synchronous/sync_client.py +191 -0
  98. lmnr/sdk/datasets/__init__.py +94 -0
  99. lmnr/sdk/datasets/file_utils.py +91 -0
  100. lmnr/sdk/decorators.py +163 -26
  101. lmnr/sdk/eval_control.py +3 -2
  102. lmnr/sdk/evaluations.py +403 -191
  103. lmnr/sdk/laminar.py +1080 -549
  104. lmnr/sdk/log.py +7 -2
  105. lmnr/sdk/types.py +246 -134
  106. lmnr/sdk/utils.py +151 -7
  107. lmnr/version.py +46 -0
  108. {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
  109. lmnr-0.7.26.dist-info/RECORD +116 -0
  110. lmnr-0.7.26.dist-info/WHEEL +4 -0
  111. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  112. lmnr/cli.py +0 -101
  113. lmnr/openllmetry_sdk/.python-version +0 -1
  114. lmnr/openllmetry_sdk/__init__.py +0 -72
  115. lmnr/openllmetry_sdk/config/__init__.py +0 -9
  116. lmnr/openllmetry_sdk/decorators/base.py +0 -185
  117. lmnr/openllmetry_sdk/instruments.py +0 -38
  118. lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
  119. lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
  120. lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
  121. lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
  122. lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
  123. lmnr/openllmetry_sdk/utils/package_check.py +0 -7
  124. lmnr/openllmetry_sdk/version.py +0 -1
  125. lmnr/sdk/datasets.py +0 -55
  126. lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
  127. lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
  128. lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
  129. lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
  130. /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
  131. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
  132. /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
  133. /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
@@ -0,0 +1,305 @@
1
+ import logging
2
+
3
+ from opentelemetry import context as context_api
4
+ from ..shared import (
5
+ _set_client_attributes,
6
+ _set_functions_attributes,
7
+ _set_request_attributes,
8
+ _set_response_attributes,
9
+ _set_span_attribute,
10
+ _set_span_stream_usage,
11
+ get_token_count_from_string,
12
+ is_streaming_response,
13
+ model_as_dict,
14
+ propagate_trace_context,
15
+ should_record_stream_token_usage,
16
+ )
17
+ from ..shared.config import Config
18
+ from ..shared.event_emitter import emit_event
19
+ from ..shared.event_models import (
20
+ ChoiceEvent,
21
+ MessageEvent,
22
+ )
23
+ from ..utils import (
24
+ _with_tracer_wrapper,
25
+ dont_throw,
26
+ is_openai_v1,
27
+ should_emit_events,
28
+ should_send_prompts,
29
+ )
30
+ from lmnr.opentelemetry_lib.tracing.context import (
31
+ get_current_context,
32
+ get_event_attributes_from_context,
33
+ )
34
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
35
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
36
+ from opentelemetry.semconv_ai import (
37
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
38
+ LLMRequestTypeValues,
39
+ SpanAttributes,
40
+ )
41
+ from opentelemetry.trace import SpanKind
42
+ from opentelemetry.trace.status import Status, StatusCode
43
+
44
+ SPAN_NAME = "openai.completion"
45
+ LLM_REQUEST_TYPE = LLMRequestTypeValues.COMPLETION
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ @_with_tracer_wrapper
51
+ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
52
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
53
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
54
+ ):
55
+ return wrapped(*args, **kwargs)
56
+
57
+ # span needs to be opened and closed manually because the response is a generator
58
+ span = tracer.start_span(
59
+ SPAN_NAME,
60
+ kind=SpanKind.CLIENT,
61
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
62
+ context=get_current_context(),
63
+ )
64
+
65
+ _handle_request(span, kwargs, instance)
66
+
67
+ try:
68
+ response = wrapped(*args, **kwargs)
69
+ except Exception as e:
70
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
71
+ attributes = get_event_attributes_from_context()
72
+ span.record_exception(e, attributes=attributes)
73
+ span.set_status(Status(StatusCode.ERROR, str(e)))
74
+ span.end()
75
+ raise
76
+
77
+ if is_streaming_response(response):
78
+ # span will be closed after the generator is done
79
+ return _build_from_streaming_response(span, kwargs, response)
80
+ else:
81
+ _handle_response(response, span, instance)
82
+
83
+ span.end()
84
+ return response
85
+
86
+
87
+ @_with_tracer_wrapper
88
+ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
89
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
90
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
91
+ ):
92
+ return await wrapped(*args, **kwargs)
93
+
94
+ span = tracer.start_span(
95
+ name=SPAN_NAME,
96
+ kind=SpanKind.CLIENT,
97
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
98
+ context=get_current_context(),
99
+ )
100
+
101
+ _handle_request(span, kwargs, instance)
102
+
103
+ try:
104
+ response = await wrapped(*args, **kwargs)
105
+ except Exception as e:
106
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
107
+ attributes = get_event_attributes_from_context()
108
+ span.record_exception(e, attributes=attributes)
109
+ span.set_status(Status(StatusCode.ERROR, str(e)))
110
+ span.end()
111
+ raise
112
+
113
+ if is_streaming_response(response):
114
+ # span will be closed after the generator is done
115
+ return _abuild_from_streaming_response(span, kwargs, response)
116
+ else:
117
+ _handle_response(response, span, instance)
118
+
119
+ span.end()
120
+ return response
121
+
122
+
123
+ @dont_throw
124
+ def _handle_request(span, kwargs, instance):
125
+ _set_request_attributes(span, kwargs, instance)
126
+ if should_emit_events():
127
+ _emit_prompts_events(kwargs)
128
+ else:
129
+ if should_send_prompts():
130
+ _set_prompts(span, kwargs.get("prompt"))
131
+ _set_functions_attributes(span, kwargs.get("functions"))
132
+ _set_client_attributes(span, instance)
133
+ if Config.enable_trace_context_propagation:
134
+ propagate_trace_context(span, kwargs)
135
+
136
+
137
+ def _emit_prompts_events(kwargs):
138
+ prompt = kwargs.get("prompt")
139
+ if isinstance(prompt, list):
140
+ for p in prompt:
141
+ emit_event(MessageEvent(content=p))
142
+ elif isinstance(prompt, str):
143
+ emit_event(MessageEvent(content=prompt))
144
+
145
+
146
+ @dont_throw
147
+ def _handle_response(response, span, instance=None):
148
+ if is_openai_v1():
149
+ response_dict = model_as_dict(response)
150
+ else:
151
+ response_dict = response
152
+
153
+ _set_response_attributes(span, response_dict)
154
+ if should_emit_events():
155
+ for choice in response.choices:
156
+ emit_event(_parse_choice_event(choice))
157
+ else:
158
+ if should_send_prompts():
159
+ _set_completions(span, response_dict.get("choices"))
160
+
161
+
162
+ def _set_prompts(span, prompt):
163
+ if not span.is_recording() or not prompt:
164
+ return
165
+
166
+ _set_span_attribute(
167
+ span,
168
+ f"{SpanAttributes.LLM_PROMPTS}.0.user",
169
+ prompt[0] if isinstance(prompt, list) else prompt,
170
+ )
171
+
172
+
173
+ @dont_throw
174
+ def _set_completions(span, choices):
175
+ if not span.is_recording() or not choices:
176
+ return
177
+
178
+ for choice in choices:
179
+ index = choice.get("index")
180
+ prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
181
+ _set_span_attribute(
182
+ span, f"{prefix}.finish_reason", choice.get("finish_reason")
183
+ )
184
+ _set_span_attribute(span, f"{prefix}.content", choice.get("text"))
185
+
186
+
187
+ @dont_throw
188
+ def _build_from_streaming_response(span, request_kwargs, response):
189
+ complete_response = {"choices": [], "model": "", "id": ""}
190
+ for item in response:
191
+ yield item
192
+ _accumulate_streaming_response(complete_response, item)
193
+
194
+ _set_response_attributes(span, complete_response)
195
+
196
+ _set_token_usage(span, request_kwargs, complete_response)
197
+
198
+ if should_emit_events():
199
+ _emit_streaming_response_events(complete_response)
200
+ else:
201
+ if should_send_prompts():
202
+ _set_completions(span, complete_response.get("choices"))
203
+
204
+ span.set_status(Status(StatusCode.OK))
205
+ span.end()
206
+
207
+
208
+ @dont_throw
209
+ async def _abuild_from_streaming_response(span, request_kwargs, response):
210
+ complete_response = {"choices": [], "model": "", "id": ""}
211
+ async for item in response:
212
+ yield item
213
+ _accumulate_streaming_response(complete_response, item)
214
+
215
+ _set_response_attributes(span, complete_response)
216
+
217
+ _set_token_usage(span, request_kwargs, complete_response)
218
+
219
+ if should_emit_events():
220
+ _emit_streaming_response_events(complete_response)
221
+ else:
222
+ if should_send_prompts():
223
+ _set_completions(span, complete_response.get("choices"))
224
+
225
+ span.set_status(Status(StatusCode.OK))
226
+ span.end()
227
+
228
+
229
+ def _emit_streaming_response_events(complete_response):
230
+ for i, choice in enumerate(complete_response["choices"]):
231
+ emit_event(
232
+ ChoiceEvent(
233
+ index=choice.get("index", i),
234
+ message={"content": choice.get("text"), "role": "assistant"},
235
+ finish_reason=choice.get("finish_reason", "unknown"),
236
+ )
237
+ )
238
+
239
+
240
+ @dont_throw
241
+ def _set_token_usage(span, request_kwargs, complete_response):
242
+ # use tiktoken calculate token usage
243
+ if should_record_stream_token_usage():
244
+ prompt_usage = -1
245
+ completion_usage = -1
246
+
247
+ # prompt_usage
248
+ if request_kwargs and request_kwargs.get("prompt"):
249
+ prompt_content = request_kwargs.get("prompt")
250
+ model_name = complete_response.get("model") or None
251
+
252
+ if model_name:
253
+ prompt_usage = get_token_count_from_string(prompt_content, model_name)
254
+
255
+ # completion_usage
256
+ if complete_response.get("choices"):
257
+ completion_content = ""
258
+ model_name = complete_response.get("model") or None
259
+
260
+ for choice in complete_response.get("choices"):
261
+ if choice.get("text"):
262
+ completion_content += choice.get("text")
263
+
264
+ if model_name:
265
+ completion_usage = get_token_count_from_string(
266
+ completion_content, model_name
267
+ )
268
+
269
+ # span record
270
+ _set_span_stream_usage(span, prompt_usage, completion_usage)
271
+
272
+
273
+ @dont_throw
274
+ def _accumulate_streaming_response(complete_response, item):
275
+ if is_openai_v1():
276
+ item = model_as_dict(item)
277
+
278
+ complete_response["model"] = item.get("model")
279
+ complete_response["id"] = item.get("id")
280
+ for choice in item.get("choices"):
281
+ index = choice.get("index")
282
+ if len(complete_response.get("choices")) <= index:
283
+ complete_response["choices"].append({"index": index, "text": ""})
284
+ complete_choice = complete_response.get("choices")[index]
285
+ if choice.get("finish_reason"):
286
+ complete_choice["finish_reason"] = choice.get("finish_reason")
287
+
288
+ if choice.get("text"):
289
+ complete_choice["text"] += choice.get("text")
290
+
291
+ return complete_response
292
+
293
+
294
+ def _parse_choice_event(choice) -> ChoiceEvent:
295
+ has_message = choice.text is not None
296
+ has_finish_reason = choice.finish_reason is not None
297
+
298
+ content = choice.text if has_message else None
299
+ finish_reason = choice.finish_reason if has_finish_reason else "unknown"
300
+
301
+ return ChoiceEvent(
302
+ index=choice.index,
303
+ message={"content": content, "role": "assistant"},
304
+ finish_reason=finish_reason,
305
+ )
@@ -0,0 +1,16 @@
1
+ from typing import Callable, Optional
2
+
3
+ from opentelemetry._events import EventLogger
4
+
5
+
6
+ class Config:
7
+ enrich_token_usage = False
8
+ enrich_assistant = False
9
+ exception_logger = None
10
+ get_common_metrics_attributes: Callable[[], dict] = lambda: {}
11
+ upload_base64_image: Callable[[str, str, str], str] = (
12
+ lambda trace_id, span_id, base64_image_url: ""
13
+ )
14
+ enable_trace_context_propagation: bool = True
15
+ use_legacy_attributes = True
16
+ event_logger: Optional[EventLogger] = None
@@ -0,0 +1,312 @@
1
+ import logging
2
+ import time
3
+ from collections.abc import Iterable
4
+
5
+ from opentelemetry import context as context_api
6
+
7
+ from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
8
+ from ..shared import (
9
+ OPENAI_LLM_USAGE_TOKEN_TYPES,
10
+ _get_openai_base_url,
11
+ _set_client_attributes,
12
+ _set_request_attributes,
13
+ _set_response_attributes,
14
+ _set_span_attribute,
15
+ _token_type,
16
+ metric_shared_attributes,
17
+ model_as_dict,
18
+ propagate_trace_context,
19
+ )
20
+ from ..shared.config import Config
21
+ from ..shared.event_emitter import emit_event
22
+ from ..shared.event_models import (
23
+ ChoiceEvent,
24
+ MessageEvent,
25
+ )
26
+ from ..utils import (
27
+ _with_embeddings_telemetry_wrapper,
28
+ dont_throw,
29
+ is_openai_v1,
30
+ should_emit_events,
31
+ should_send_prompts,
32
+ start_as_current_span_async,
33
+ )
34
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
35
+ from opentelemetry.metrics import Counter, Histogram
36
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
37
+ from opentelemetry.semconv_ai import (
38
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
39
+ LLMRequestTypeValues,
40
+ SpanAttributes,
41
+ )
42
+ from opentelemetry.trace import SpanKind, Status, StatusCode
43
+
44
+ from openai._legacy_response import LegacyAPIResponse
45
+ from openai.types.create_embedding_response import CreateEmbeddingResponse
46
+
47
+ SPAN_NAME = "openai.embeddings"
48
+ LLM_REQUEST_TYPE = LLMRequestTypeValues.EMBEDDING
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ @_with_embeddings_telemetry_wrapper
54
+ def embeddings_wrapper(
55
+ tracer,
56
+ token_counter: Counter,
57
+ vector_size_counter: Counter,
58
+ duration_histogram: Histogram,
59
+ exception_counter: Counter,
60
+ wrapped,
61
+ instance,
62
+ args,
63
+ kwargs,
64
+ ):
65
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
66
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
67
+ ):
68
+ return wrapped(*args, **kwargs)
69
+
70
+ with tracer.start_as_current_span(
71
+ name=SPAN_NAME,
72
+ kind=SpanKind.CLIENT,
73
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
74
+ ) as span:
75
+ _handle_request(span, kwargs, instance)
76
+
77
+ try:
78
+ # record time for duration
79
+ start_time = time.time()
80
+ response = wrapped(*args, **kwargs)
81
+ end_time = time.time()
82
+ except Exception as e: # pylint: disable=broad-except
83
+ end_time = time.time()
84
+ duration = end_time - start_time if "start_time" in locals() else 0
85
+ attributes = {
86
+ "error.type": e.__class__.__name__,
87
+ }
88
+
89
+ # if there are legal duration, record it
90
+ if duration > 0 and duration_histogram:
91
+ duration_histogram.record(duration, attributes=attributes)
92
+ if exception_counter:
93
+ exception_counter.add(1, attributes=attributes)
94
+
95
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
96
+ attributes = get_event_attributes_from_context()
97
+ span.record_exception(e, attributes=attributes)
98
+ span.set_status(Status(StatusCode.ERROR, str(e)))
99
+ span.end()
100
+
101
+ raise
102
+
103
+ duration = end_time - start_time
104
+
105
+ _handle_response(
106
+ response,
107
+ span,
108
+ instance,
109
+ token_counter,
110
+ vector_size_counter,
111
+ duration_histogram,
112
+ duration,
113
+ )
114
+
115
+ return response
116
+
117
+
118
+ @_with_embeddings_telemetry_wrapper
119
+ async def aembeddings_wrapper(
120
+ tracer,
121
+ token_counter: Counter,
122
+ vector_size_counter: Counter,
123
+ duration_histogram: Histogram,
124
+ exception_counter: Counter,
125
+ wrapped,
126
+ instance,
127
+ args,
128
+ kwargs,
129
+ ):
130
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
131
+ SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
132
+ ):
133
+ return await wrapped(*args, **kwargs)
134
+
135
+ async with start_as_current_span_async(
136
+ tracer=tracer,
137
+ name=SPAN_NAME,
138
+ kind=SpanKind.CLIENT,
139
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
140
+ ) as span:
141
+ _handle_request(span, kwargs, instance)
142
+
143
+ try:
144
+ # record time for duration
145
+ start_time = time.time()
146
+ response = await wrapped(*args, **kwargs)
147
+ end_time = time.time()
148
+ except Exception as e: # pylint: disable=broad-except
149
+ end_time = time.time()
150
+ duration = end_time - start_time if "start_time" in locals() else 0
151
+ attributes = {
152
+ "error.type": e.__class__.__name__,
153
+ }
154
+
155
+ # if there are legal duration, record it
156
+ if duration > 0 and duration_histogram:
157
+ duration_histogram.record(duration, attributes=attributes)
158
+ if exception_counter:
159
+ exception_counter.add(1, attributes=attributes)
160
+
161
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
162
+ attributes = get_event_attributes_from_context()
163
+ span.record_exception(e, attributes=attributes)
164
+ span.set_status(Status(StatusCode.ERROR, str(e)))
165
+ span.end()
166
+
167
+ raise
168
+
169
+ duration = end_time - start_time
170
+
171
+ _handle_response(
172
+ response,
173
+ span,
174
+ instance,
175
+ token_counter,
176
+ vector_size_counter,
177
+ duration_histogram,
178
+ duration,
179
+ )
180
+
181
+ return response
182
+
183
+
184
+ @dont_throw
185
+ def _handle_request(span, kwargs, instance):
186
+ _set_request_attributes(span, kwargs, instance)
187
+
188
+ if should_emit_events():
189
+ _emit_embeddings_message_event(kwargs.get("input"))
190
+ else:
191
+ if should_send_prompts():
192
+ _set_prompts(span, kwargs.get("input"))
193
+
194
+ _set_client_attributes(span, instance)
195
+
196
+ if Config.enable_trace_context_propagation:
197
+ propagate_trace_context(span, kwargs)
198
+
199
+
200
+ @dont_throw
201
+ def _handle_response(
202
+ response,
203
+ span,
204
+ instance=None,
205
+ token_counter=None,
206
+ vector_size_counter=None,
207
+ duration_histogram=None,
208
+ duration=None,
209
+ ):
210
+ if is_openai_v1():
211
+ response_dict = model_as_dict(response)
212
+ else:
213
+ response_dict = response
214
+ # metrics record
215
+ _set_embeddings_metrics(
216
+ instance,
217
+ token_counter,
218
+ vector_size_counter,
219
+ duration_histogram,
220
+ response_dict,
221
+ duration,
222
+ )
223
+ # span attributes
224
+ _set_response_attributes(span, response_dict)
225
+
226
+ # emit events
227
+ if should_emit_events():
228
+ _emit_embeddings_choice_event(response)
229
+
230
+
231
+ def _set_embeddings_metrics(
232
+ instance,
233
+ token_counter,
234
+ vector_size_counter,
235
+ duration_histogram,
236
+ response_dict,
237
+ duration,
238
+ ):
239
+ shared_attributes = metric_shared_attributes(
240
+ response_model=response_dict.get("model") or None,
241
+ operation="embeddings",
242
+ server_address=_get_openai_base_url(instance),
243
+ )
244
+
245
+ # token count metrics
246
+ usage = response_dict.get("usage")
247
+ if usage and token_counter:
248
+ for name, val in usage.items():
249
+ if name in OPENAI_LLM_USAGE_TOKEN_TYPES:
250
+ if val is None:
251
+ logging.error(f"Received None value for {name} in usage")
252
+ continue
253
+ attributes_with_token_type = {
254
+ **shared_attributes,
255
+ SpanAttributes.LLM_TOKEN_TYPE: _token_type(name),
256
+ }
257
+ token_counter.record(val, attributes=attributes_with_token_type)
258
+
259
+ # vec size metrics
260
+ # should use counter for vector_size?
261
+ vec_embedding = (response_dict.get("data") or [{}])[0].get("embedding", [])
262
+ vec_size = len(vec_embedding)
263
+ if vector_size_counter:
264
+ vector_size_counter.add(vec_size, attributes=shared_attributes)
265
+
266
+ # duration metrics
267
+ if duration and isinstance(duration, (float, int)) and duration_histogram:
268
+ duration_histogram.record(duration, attributes=shared_attributes)
269
+
270
+
271
+ def _set_prompts(span, prompt):
272
+ if not span.is_recording() or not prompt:
273
+ return
274
+
275
+ if isinstance(prompt, list):
276
+ for i, p in enumerate(prompt):
277
+ _set_span_attribute(span, f"{SpanAttributes.LLM_PROMPTS}.{i}.content", p)
278
+ else:
279
+ _set_span_attribute(
280
+ span,
281
+ f"{SpanAttributes.LLM_PROMPTS}.0.content",
282
+ prompt,
283
+ )
284
+
285
+
286
+ def _emit_embeddings_message_event(embeddings) -> None:
287
+ if isinstance(embeddings, str):
288
+ emit_event(MessageEvent(content=embeddings))
289
+ elif isinstance(embeddings, Iterable):
290
+ for i in embeddings:
291
+ emit_event(MessageEvent(content=i))
292
+
293
+
294
+ def _emit_embeddings_choice_event(response) -> None:
295
+ if isinstance(response, CreateEmbeddingResponse):
296
+ for embedding in response.data:
297
+ emit_event(
298
+ ChoiceEvent(
299
+ index=embedding.index,
300
+ message={"content": embedding.embedding, "role": "assistant"},
301
+ )
302
+ )
303
+
304
+ elif isinstance(response, LegacyAPIResponse):
305
+ parsed_response = response.parse()
306
+ for embedding in parsed_response.data:
307
+ emit_event(
308
+ ChoiceEvent(
309
+ index=embedding.index,
310
+ message={"content": embedding.embedding, "role": "assistant"},
311
+ )
312
+ )