lmnr 0.7.11__py3-none-any.whl → 0.7.12__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 (24) hide show
  1. lmnr/opentelemetry_lib/__init__.py +6 -0
  2. lmnr/opentelemetry_lib/decorators/__init__.py +1 -1
  3. lmnr/opentelemetry_lib/litellm/__init__.py +277 -32
  4. lmnr/opentelemetry_lib/litellm/utils.py +76 -0
  5. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +136 -44
  6. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +93 -6
  7. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +155 -3
  8. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  9. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +477 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +14 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +10 -1
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +100 -8
  14. lmnr/opentelemetry_lib/tracing/__init__.py +9 -0
  15. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +20 -0
  16. lmnr/opentelemetry_lib/tracing/exporter.py +24 -9
  17. lmnr/opentelemetry_lib/tracing/instruments.py +4 -0
  18. lmnr/opentelemetry_lib/tracing/processor.py +26 -0
  19. lmnr/sdk/laminar.py +14 -0
  20. lmnr/version.py +1 -1
  21. {lmnr-0.7.11.dist-info → lmnr-0.7.12.dist-info}/METADATA +50 -50
  22. {lmnr-0.7.11.dist-info → lmnr-0.7.12.dist-info}/RECORD +24 -21
  23. {lmnr-0.7.11.dist-info → lmnr-0.7.12.dist-info}/WHEEL +0 -0
  24. {lmnr-0.7.11.dist-info → lmnr-0.7.12.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,100 @@
1
+ """OpenTelemetry CUA instrumentation"""
2
+
3
+ import logging
4
+ from typing import Any, AsyncGenerator, Collection
5
+
6
+ from lmnr.opentelemetry_lib.decorators import json_dumps
7
+ from lmnr import Laminar
8
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
9
+ from opentelemetry.instrumentation.utils import unwrap
10
+
11
+ from opentelemetry.trace import Span
12
+ from opentelemetry.trace.status import Status, StatusCode
13
+ from wrapt import wrap_function_wrapper
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _instruments = ("cua-agent >= 0.4.0",)
18
+
19
+
20
+ def _wrap_run(
21
+ wrapped,
22
+ instance,
23
+ args,
24
+ kwargs,
25
+ ):
26
+ parent_span = Laminar.start_span("ComputerAgent.run")
27
+ instance._lmnr_parent_span = parent_span
28
+
29
+ try:
30
+ result: AsyncGenerator[dict[str, Any], None] = wrapped(*args, **kwargs)
31
+ return _abuild_from_streaming_response(parent_span, result)
32
+ except Exception as e:
33
+ if parent_span.is_recording():
34
+ parent_span.set_status(Status(StatusCode.ERROR))
35
+ parent_span.record_exception(e)
36
+ parent_span.end()
37
+ raise
38
+
39
+
40
+ async def _abuild_from_streaming_response(
41
+ parent_span: Span, response: AsyncGenerator[dict[str, Any], None]
42
+ ) -> AsyncGenerator[dict[str, Any], None]:
43
+ with Laminar.use_span(parent_span, end_on_exit=True):
44
+ response_iter = aiter(response)
45
+ while True:
46
+ step = None
47
+ step_span = Laminar.start_span("ComputerAgent.step")
48
+ with Laminar.use_span(step_span):
49
+ try:
50
+ step = await anext(response_iter)
51
+ step_span.set_attribute("lmnr.span.output", json_dumps(step))
52
+ try:
53
+ # When processing tool calls, each output item is processed separately,
54
+ # if the output is message, agent.step returns an empty array
55
+ # https://github.com/trycua/cua/blob/17d670962970a1d1774daaec029ebf92f1f9235e/libs/python/agent/agent/agent.py#L459
56
+ if len(step.get("output", [])) == 0:
57
+ continue
58
+ except Exception:
59
+ pass
60
+ if step_span.is_recording():
61
+ step_span.end()
62
+ except StopAsyncIteration:
63
+ # don't end on purpose, there is no iteration step here.
64
+ break
65
+
66
+ if step is not None:
67
+ yield step
68
+
69
+
70
+ class CuaAgentInstrumentor(BaseInstrumentor):
71
+ def __init__(self):
72
+ super().__init__()
73
+
74
+ def instrumentation_dependencies(self) -> Collection[str]:
75
+ return _instruments
76
+
77
+ def _instrument(self, **kwargs):
78
+ wrap_package = "agent.agent"
79
+ wrap_object = "ComputerAgent"
80
+ wrap_method = "run"
81
+ try:
82
+ wrap_function_wrapper(
83
+ wrap_package,
84
+ f"{wrap_object}.{wrap_method}",
85
+ _wrap_run,
86
+ )
87
+ except ModuleNotFoundError:
88
+ pass # that's ok, we don't want to fail if some methods do not exist
89
+
90
+ def _uninstrument(self, **kwargs):
91
+ wrap_package = "agent.agent"
92
+ wrap_object = "ComputerAgent"
93
+ wrap_method = "run"
94
+ try:
95
+ unwrap(
96
+ f"{wrap_package}.{wrap_object}",
97
+ wrap_method,
98
+ )
99
+ except ModuleNotFoundError:
100
+ pass # that's ok, we don't want to fail if some methods do not exist
@@ -0,0 +1,477 @@
1
+ """OpenTelemetry CUA instrumentation"""
2
+
3
+ import logging
4
+ from typing import Collection
5
+
6
+ from lmnr.opentelemetry_lib.decorators import json_dumps
7
+ from lmnr.sdk.utils import get_input_from_func_args
8
+ from lmnr import Laminar
9
+ from lmnr.opentelemetry_lib.tracing.context import get_current_context
10
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
11
+ from opentelemetry.instrumentation.utils import unwrap
12
+
13
+ from opentelemetry import trace
14
+ from opentelemetry.trace import Span
15
+ from opentelemetry.trace.status import Status, StatusCode
16
+ from wrapt import wrap_function_wrapper
17
+
18
+ from .utils import payload_to_placeholder
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _instruments = ("cua-computer >= 0.4.0",)
23
+
24
+
25
+ WRAPPED_METHODS = [
26
+ {
27
+ "package": "computer.interface.generic",
28
+ "object": "GenericComputerInterface",
29
+ "method": "close",
30
+ },
31
+ {
32
+ "package": "computer.interface.generic",
33
+ "object": "GenericComputerInterface",
34
+ "method": "force_close",
35
+ },
36
+ ]
37
+ WRAPPED_AMETHODS = [
38
+ {
39
+ "package": "computer.computer",
40
+ "object": "Computer",
41
+ "method": "__aenter__",
42
+ "action": "start_parent_span",
43
+ },
44
+ {
45
+ "package": "computer.computer",
46
+ "object": "Computer",
47
+ "method": "__aexit__",
48
+ "action": "end_parent_span",
49
+ },
50
+ {
51
+ "package": "computer.interface.generic",
52
+ "object": "GenericComputerInterface",
53
+ "method": "mouse_down",
54
+ },
55
+ {
56
+ "package": "computer.interface.generic",
57
+ "object": "GenericComputerInterface",
58
+ "method": "mouse_up",
59
+ },
60
+ {
61
+ "package": "computer.interface.generic",
62
+ "object": "GenericComputerInterface",
63
+ "method": "left_click",
64
+ },
65
+ {
66
+ "package": "computer.interface.generic",
67
+ "object": "GenericComputerInterface",
68
+ "method": "right_click",
69
+ },
70
+ {
71
+ "package": "computer.interface.generic",
72
+ "object": "GenericComputerInterface",
73
+ "method": "double_click",
74
+ },
75
+ {
76
+ "package": "computer.interface.generic",
77
+ "object": "GenericComputerInterface",
78
+ "method": "move_cursor",
79
+ },
80
+ {
81
+ "package": "computer.interface.generic",
82
+ "object": "GenericComputerInterface",
83
+ "method": "drag_to",
84
+ },
85
+ {
86
+ "package": "computer.interface.generic",
87
+ "object": "GenericComputerInterface",
88
+ "method": "drag",
89
+ },
90
+ {
91
+ "package": "computer.interface.generic",
92
+ "object": "GenericComputerInterface",
93
+ "method": "key_down",
94
+ },
95
+ {
96
+ "package": "computer.interface.generic",
97
+ "object": "GenericComputerInterface",
98
+ "method": "key_up",
99
+ },
100
+ {
101
+ "package": "computer.interface.generic",
102
+ "object": "GenericComputerInterface",
103
+ "method": "type_text",
104
+ },
105
+ {
106
+ "package": "computer.interface.generic",
107
+ "object": "GenericComputerInterface",
108
+ "method": "press",
109
+ },
110
+ {
111
+ "package": "computer.interface.generic",
112
+ "object": "GenericComputerInterface",
113
+ "method": "hotkey",
114
+ },
115
+ {
116
+ "package": "computer.interface.generic",
117
+ "object": "GenericComputerInterface",
118
+ "method": "scroll",
119
+ },
120
+ {
121
+ "package": "computer.interface.generic",
122
+ "object": "GenericComputerInterface",
123
+ "method": "scroll_down",
124
+ },
125
+ {
126
+ "package": "computer.interface.generic",
127
+ "object": "GenericComputerInterface",
128
+ "method": "scroll_up",
129
+ },
130
+ {
131
+ "package": "computer.interface.generic",
132
+ "object": "GenericComputerInterface",
133
+ "method": "screenshot",
134
+ "output_formatter": payload_to_placeholder,
135
+ },
136
+ {
137
+ "package": "computer.interface.generic",
138
+ "object": "GenericComputerInterface",
139
+ "method": "get_screen_size",
140
+ },
141
+ {
142
+ "package": "computer.interface.generic",
143
+ "object": "GenericComputerInterface",
144
+ "method": "get_cursor_position",
145
+ },
146
+ {
147
+ "package": "computer.interface.generic",
148
+ "object": "GenericComputerInterface",
149
+ "method": "copy_to_clipboard",
150
+ },
151
+ {
152
+ "package": "computer.interface.generic",
153
+ "object": "GenericComputerInterface",
154
+ "method": "set_clipboard",
155
+ },
156
+ {
157
+ "package": "computer.interface.generic",
158
+ "object": "GenericComputerInterface",
159
+ "method": "file_exists",
160
+ },
161
+ {
162
+ "package": "computer.interface.generic",
163
+ "object": "GenericComputerInterface",
164
+ "method": "directory_exists",
165
+ },
166
+ {
167
+ "package": "computer.interface.generic",
168
+ "object": "GenericComputerInterface",
169
+ "method": "list_dir",
170
+ },
171
+ {
172
+ "package": "computer.interface.generic",
173
+ "object": "GenericComputerInterface",
174
+ "method": "read_text",
175
+ },
176
+ {
177
+ "package": "computer.interface.generic",
178
+ "object": "GenericComputerInterface",
179
+ "method": "write_text",
180
+ },
181
+ {
182
+ "package": "computer.interface.generic",
183
+ "object": "GenericComputerInterface",
184
+ "method": "read_bytes",
185
+ },
186
+ {
187
+ "package": "computer.interface.generic",
188
+ "object": "GenericComputerInterface",
189
+ "method": "write_bytes",
190
+ },
191
+ {
192
+ "package": "computer.interface.generic",
193
+ "object": "GenericComputerInterface",
194
+ "method": "delete_file",
195
+ },
196
+ {
197
+ "package": "computer.interface.generic",
198
+ "object": "GenericComputerInterface",
199
+ "method": "create_dir",
200
+ },
201
+ {
202
+ "package": "computer.interface.generic",
203
+ "object": "GenericComputerInterface",
204
+ "method": "delete_dir",
205
+ },
206
+ {
207
+ "package": "computer.interface.generic",
208
+ "object": "GenericComputerInterface",
209
+ "method": "get_file_size",
210
+ },
211
+ {
212
+ "package": "computer.interface.generic",
213
+ "object": "GenericComputerInterface",
214
+ "method": "run_command",
215
+ },
216
+ {
217
+ "package": "computer.interface.generic",
218
+ "object": "GenericComputerInterface",
219
+ "method": "get_accessibility_tree",
220
+ },
221
+ {
222
+ "package": "computer.interface.generic",
223
+ "object": "GenericComputerInterface",
224
+ "method": "to_screen_coordinates",
225
+ },
226
+ {
227
+ "package": "computer.interface.generic",
228
+ "object": "GenericComputerInterface",
229
+ "method": "get_active_window_bounds",
230
+ },
231
+ {
232
+ "package": "computer.interface.generic",
233
+ "object": "GenericComputerInterface",
234
+ "method": "to_screenshot_coordinates",
235
+ },
236
+ ]
237
+
238
+
239
+ def _with_wrapper(func):
240
+ """Helper for providing tracer for wrapper functions. Includes metric collectors."""
241
+
242
+ def wrapper(
243
+ to_wrap,
244
+ ):
245
+ def wrapper(wrapped, instance, args, kwargs):
246
+ return func(
247
+ to_wrap,
248
+ wrapped,
249
+ instance,
250
+ args,
251
+ kwargs,
252
+ )
253
+
254
+ return wrapper
255
+
256
+ return wrapper
257
+
258
+
259
+ def add_input_to_parent_span(span, instance):
260
+ # api_key is skipped on purpose
261
+ params = {}
262
+ if hasattr(instance, "display"):
263
+ params["display"] = instance.display
264
+ if hasattr(instance, "memory"):
265
+ params["memory"] = instance.memory
266
+ if hasattr(instance, "cpu"):
267
+ params["cpu"] = instance.cpu
268
+ if hasattr(instance, "os_type"):
269
+ params["os_type"] = instance.os_type
270
+ if hasattr(instance, "name"):
271
+ params["name"] = instance.name
272
+ if hasattr(instance, "image"):
273
+ params["image"] = instance.image
274
+ if hasattr(instance, "shared_directories"):
275
+ params["shared_directories"] = instance.shared_directories
276
+ if hasattr(instance, "use_host_computer_server"):
277
+ params["use_host_computer_server"] = instance.use_host_computer_server
278
+ if hasattr(instance, "verbosity"):
279
+ if (
280
+ isinstance(instance.verbosity, int)
281
+ and instance.verbosity in logging._levelToName
282
+ ):
283
+ params["verbosity"] = logging._levelToName[instance.verbosity]
284
+ else:
285
+ params["verbosity"] = instance.verbosity
286
+ if hasattr(instance, "telemetry_enabled"):
287
+ params["telemetry_enabled"] = instance.telemetry_enabled
288
+ if hasattr(instance, "provider_type"):
289
+ params["provider_type"] = instance.provider_type
290
+ if hasattr(instance, "port"):
291
+ params["port"] = instance.port
292
+ if hasattr(instance, "noVNC_port"):
293
+ params["noVNC_port"] = instance.noVNC_port
294
+ if hasattr(instance, "host"):
295
+ params["host"] = instance.host
296
+ if hasattr(instance, "storage"):
297
+ params["storage"] = instance.storage
298
+ if hasattr(instance, "ephemeral"):
299
+ params["ephemeral"] = instance.ephemeral
300
+ if hasattr(instance, "experiments"):
301
+ params["experiments"] = instance.experiments
302
+ span.set_attribute("lmnr.span.input", json_dumps(params))
303
+
304
+
305
+ @_with_wrapper
306
+ def _wrap(
307
+ to_wrap,
308
+ wrapped,
309
+ instance,
310
+ args,
311
+ kwargs,
312
+ ):
313
+ if to_wrap.get("action") == "start_parent_span":
314
+ parent_span = Laminar.start_span("computer.run")
315
+ add_input_to_parent_span(parent_span, instance)
316
+ result = wrapped(*args, **kwargs)
317
+ try:
318
+ instance._interface._lmnr_parent_span = parent_span
319
+ except Exception:
320
+ pass
321
+ return result
322
+ elif to_wrap.get("action") == "end_parent_span":
323
+ result = wrapped(*args, **kwargs)
324
+ try:
325
+ parent_span: Span = instance._interface._lmnr_parent_span
326
+ if parent_span and parent_span.is_recording():
327
+ parent_span.end()
328
+ except Exception:
329
+ pass
330
+ return result
331
+
332
+ # if there's no parent span, use
333
+ parent_span = trace.get_current_span(context=get_current_context())
334
+ try:
335
+ if instance._lmnr_parent_span:
336
+ parent_span: Span = instance._lmnr_parent_span
337
+ except Exception:
338
+ pass
339
+
340
+ with Laminar.use_span(parent_span):
341
+ instance_name = "interface"
342
+ with Laminar.start_as_current_span(
343
+ f"{instance_name}.{to_wrap.get('method')}", span_type="TOOL"
344
+ ) as span:
345
+ span.set_attribute(
346
+ "lmnr.span.input",
347
+ json_dumps(get_input_from_func_args(wrapped, True, args, kwargs)),
348
+ )
349
+ try:
350
+ result = wrapped(*args, **kwargs)
351
+ except Exception as e: # pylint: disable=broad-except
352
+ span.set_status(Status(StatusCode.ERROR))
353
+ span.record_exception(e)
354
+ span.end()
355
+ raise
356
+ output_formatter = to_wrap.get("output_formatter") or (
357
+ lambda x: json_dumps(x)
358
+ )
359
+ span.set_attribute("lmnr.span.output", output_formatter(result))
360
+ return result
361
+
362
+
363
+ @_with_wrapper
364
+ async def _wrap_async(
365
+ to_wrap,
366
+ wrapped,
367
+ instance,
368
+ args,
369
+ kwargs,
370
+ ):
371
+ if to_wrap.get("action") == "start_parent_span":
372
+ parent_span = Laminar.start_span("computer.run")
373
+ add_input_to_parent_span(parent_span, instance)
374
+ result = await wrapped(*args, **kwargs)
375
+ try:
376
+ instance._interface._lmnr_parent_span = parent_span
377
+ except Exception:
378
+ pass
379
+ return result
380
+ elif to_wrap.get("action") == "end_parent_span":
381
+ result = await wrapped(*args, **kwargs)
382
+ try:
383
+ parent_span: Span = instance._interface._lmnr_parent_span
384
+ if parent_span and parent_span.is_recording():
385
+ parent_span.end()
386
+ except Exception:
387
+ pass
388
+ return result
389
+
390
+ # if there's no parent span, use
391
+ parent_span = trace.get_current_span(context=get_current_context())
392
+ try:
393
+ parent_span: Span = instance._lmnr_parent_span
394
+ except Exception:
395
+ pass
396
+
397
+ with Laminar.use_span(parent_span):
398
+ instance_name = "interface"
399
+ with Laminar.start_as_current_span(
400
+ f"{instance_name}.{to_wrap.get('method')}",
401
+ span_type="TOOL",
402
+ ) as span:
403
+ span.set_attribute(
404
+ "lmnr.span.input",
405
+ json_dumps(get_input_from_func_args(wrapped, True, args, kwargs)),
406
+ )
407
+ try:
408
+ result = await wrapped(*args, **kwargs)
409
+ except Exception as e: # pylint: disable=broad-except
410
+ span.set_status(Status(StatusCode.ERROR))
411
+ span.record_exception(e)
412
+ span.end()
413
+ raise
414
+ output_formatter = to_wrap.get("output_formatter") or (
415
+ lambda x: json_dumps(x)
416
+ )
417
+ span.set_attribute("lmnr.span.output", output_formatter(result))
418
+ return result
419
+
420
+
421
+ class CuaComputerInstrumentor(BaseInstrumentor):
422
+ def __init__(self):
423
+ super().__init__()
424
+
425
+ def instrumentation_dependencies(self) -> Collection[str]:
426
+ return _instruments
427
+
428
+ def _instrument(self, **kwargs):
429
+ for wrapped_method in WRAPPED_METHODS:
430
+ wrap_package = wrapped_method.get("package")
431
+ wrap_object = wrapped_method.get("object")
432
+ wrap_method = wrapped_method.get("method")
433
+
434
+ try:
435
+ wrap_function_wrapper(
436
+ wrap_package,
437
+ f"{wrap_object}.{wrap_method}",
438
+ _wrap(wrapped_method),
439
+ )
440
+ except ModuleNotFoundError:
441
+ pass # that's ok, we don't want to fail if some methods do not exist
442
+
443
+ for wrapped_method in WRAPPED_AMETHODS:
444
+ wrap_package = wrapped_method.get("package")
445
+ wrap_object = wrapped_method.get("object")
446
+ wrap_method = wrapped_method.get("method")
447
+ try:
448
+ wrap_function_wrapper(
449
+ wrap_package,
450
+ f"{wrap_object}.{wrap_method}",
451
+ _wrap_async(wrapped_method),
452
+ )
453
+ except ModuleNotFoundError:
454
+ pass # that's ok, we don't want to fail if some methods do not exist
455
+
456
+ def _uninstrument(self, **kwargs):
457
+ for wrapped_method in WRAPPED_METHODS:
458
+ wrap_package = wrapped_method.get("package")
459
+ wrap_object = wrapped_method.get("object")
460
+ try:
461
+ unwrap(
462
+ f"{wrap_package}.{wrap_object}",
463
+ wrapped_method.get("method"),
464
+ )
465
+ except ModuleNotFoundError:
466
+ pass # that's ok, we don't want to fail if some methods do not exist
467
+
468
+ for wrapped_method in WRAPPED_AMETHODS:
469
+ wrap_package = wrapped_method.get("package")
470
+ wrap_object = wrapped_method.get("object")
471
+ try:
472
+ unwrap(
473
+ f"{wrap_package}.{wrap_object}",
474
+ wrapped_method.get("method"),
475
+ )
476
+ except ModuleNotFoundError:
477
+ pass # that's ok, we don't want to fail if some methods do not exist
@@ -0,0 +1,12 @@
1
+ import base64
2
+ import orjson
3
+
4
+
5
+ def payload_to_base64url(payload_bytes: bytes) -> bytes:
6
+ data = base64.b64encode(payload_bytes).decode("utf-8")
7
+ url = f"data:image/png;base64,{data}"
8
+ return orjson.dumps({"base64url": url})
9
+
10
+
11
+ def payload_to_placeholder(payload_bytes: bytes) -> str:
12
+ return "<BINARY_BLOB_SCREENSHOT>"
@@ -144,6 +144,11 @@ def _set_request_attributes(span, kwargs, instance=None):
144
144
  _set_span_attribute(
145
145
  span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
146
146
  )
147
+ _set_span_attribute(
148
+ span,
149
+ SpanAttributes.LLM_REQUEST_REASONING_EFFORT,
150
+ kwargs.get("reasoning_effort"),
151
+ )
147
152
  if response_format := kwargs.get("response_format"):
148
153
  # backward-compatible check for
149
154
  # openai.types.shared_params.response_format_json_schema.ResponseFormatJSONSchema
@@ -263,6 +268,15 @@ def _set_response_attributes(span, response):
263
268
  SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS,
264
269
  prompt_tokens_details.get("cached_tokens", 0),
265
270
  )
271
+
272
+ if completion_token_details := dict(usage.get("completion_tokens_details", {})):
273
+ reasoning_tokens = completion_token_details.get("reasoning_tokens")
274
+ _set_span_attribute(
275
+ span,
276
+ SpanAttributes.LLM_USAGE_REASONING_TOKENS,
277
+ reasoning_tokens or 0,
278
+ )
279
+
266
280
  return
267
281
 
268
282
 
@@ -5,6 +5,7 @@ import threading
5
5
  import traceback
6
6
  from contextlib import asynccontextmanager
7
7
  from importlib.metadata import version
8
+ from packaging.version import parse
8
9
 
9
10
  from opentelemetry import context as context_api
10
11
  from opentelemetry._events import EventLogger
@@ -19,7 +20,15 @@ LMNR_TRACE_CONTENT = "LMNR_TRACE_CONTENT"
19
20
 
20
21
 
21
22
  def is_openai_v1():
22
- return _OPENAI_VERSION >= "1.0.0"
23
+ return parse(_OPENAI_VERSION) >= parse("1.0.0")
24
+
25
+
26
+ def is_reasoning_supported():
27
+ # Reasoning has been introduced in OpenAI API on Dec 17, 2024
28
+ # as per https://platform.openai.com/docs/changelog.
29
+ # The updated OpenAI library version is 1.58.0
30
+ # as per https://pypi.org/project/openai/.
31
+ return parse(_OPENAI_VERSION) >= parse("1.58.0")
23
32
 
24
33
 
25
34
  def is_azure_openai(instance):