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,451 @@
1
+ import importlib
2
+ import os
3
+ import sys
4
+
5
+ from lmnr import Laminar
6
+ from lmnr.opentelemetry_lib.tracing import get_current_context
7
+ from lmnr.opentelemetry_lib.tracing.attributes import SPAN_IDS_PATH, SPAN_PATH
8
+ from lmnr.sdk.log import get_default_logger
9
+ from lmnr.sdk.utils import get_input_from_func_args, is_method, json_dumps
10
+
11
+ from opentelemetry import trace
12
+ from opentelemetry.sdk.trace import ReadableSpan
13
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
14
+ from opentelemetry.instrumentation.utils import unwrap
15
+ from opentelemetry.trace import Status, StatusCode
16
+ from typing import Any, Collection
17
+ from typing_extensions import TypedDict
18
+ from wrapt import FunctionWrapper, wrap_function_wrapper
19
+
20
+ from .proxy import start_proxy, release_proxy, get_cc_proxy_base_url, set_trace_to_proxy
21
+
22
+ logger = get_default_logger(__name__)
23
+
24
+
25
+ class SpanContextPayload(TypedDict):
26
+ trace_id: str
27
+ span_id: str
28
+ project_api_key: str
29
+ span_ids_path: list[str]
30
+ span_path: list[str]
31
+ laminar_url: str
32
+
33
+
34
+ _instruments = ("claude-agent-sdk >= 0.1.0",)
35
+
36
+ WRAPPED_METHODS = [
37
+ {
38
+ "package": "claude_agent_sdk.client",
39
+ "object": "ClaudeSDKClient",
40
+ "method": "connect",
41
+ "class_name": "ClaudeSDKClient",
42
+ "is_async": True,
43
+ "is_streaming": False,
44
+ # start proxy on connection
45
+ "is_start_proxy": True,
46
+ "is_publish_span_context": True, # TODO: is there a need to publish span context here?
47
+ "is_release_proxy": False,
48
+ },
49
+ {
50
+ "package": "claude_agent_sdk.client",
51
+ "object": "ClaudeSDKClient",
52
+ "method": "query",
53
+ "class_name": "ClaudeSDKClient",
54
+ "is_async": True,
55
+ "is_streaming": False,
56
+ # only publish span context here as start/close managed by connect/disconnect
57
+ "is_start_proxy": False,
58
+ "is_publish_span_context": True,
59
+ "is_release_proxy": False,
60
+ },
61
+ {
62
+ "package": "claude_agent_sdk.client",
63
+ "object": "ClaudeSDKClient",
64
+ "method": "receive_messages",
65
+ "class_name": "ClaudeSDKClient",
66
+ "is_async": True,
67
+ "is_streaming": True,
68
+ },
69
+ {
70
+ "package": "claude_agent_sdk.client",
71
+ "object": "ClaudeSDKClient",
72
+ "method": "receive_response",
73
+ "class_name": "ClaudeSDKClient",
74
+ "is_async": True,
75
+ "is_streaming": True,
76
+ },
77
+ {
78
+ "package": "claude_agent_sdk.client",
79
+ "object": "ClaudeSDKClient",
80
+ "method": "interrupt",
81
+ "class_name": "ClaudeSDKClient",
82
+ "is_async": True,
83
+ "is_streaming": False,
84
+ },
85
+ {
86
+ "package": "claude_agent_sdk.client",
87
+ "object": "ClaudeSDKClient",
88
+ "method": "disconnect",
89
+ "class_name": "ClaudeSDKClient",
90
+ "is_async": True,
91
+ "is_streaming": False,
92
+ # close proxy on a connection drop
93
+ "is_start_proxy": False,
94
+ "is_publish_span_context": False,
95
+ "is_release_proxy": True,
96
+ },
97
+ {
98
+ # No "object" and "class_name" fields for module-level functions
99
+ "package": "claude_agent_sdk",
100
+ "method": "query",
101
+ "is_async": True,
102
+ "is_streaming": True,
103
+ # start, send span to, and release proxy here as it is a module-level function doing all on its own
104
+ "is_start_proxy": True,
105
+ "is_publish_span_context": True,
106
+ "is_release_proxy": True,
107
+ },
108
+ {
109
+ # No "object" and "class_name" fields for module-level functions
110
+ "package": "claude_agent_sdk",
111
+ "method": "create_sdk_mcp_server",
112
+ "is_async": False,
113
+ "is_streaming": False,
114
+ },
115
+ ]
116
+
117
+ _MODULE_FUNCTION_ORIGINALS: dict[tuple[str, str], Any] = {}
118
+
119
+
120
+ def _with_wrapper(func):
121
+ """Helper for providing tracer for wrapper functions. Includes metric collectors."""
122
+
123
+ def wrapper(
124
+ to_wrap,
125
+ ):
126
+ def wrapper(wrapped, instance, args, kwargs):
127
+ return func(
128
+ to_wrap,
129
+ wrapped,
130
+ instance,
131
+ args,
132
+ kwargs,
133
+ )
134
+
135
+ return wrapper
136
+
137
+ return wrapper
138
+
139
+
140
+ def _replace_function_aliases(original, wrapped):
141
+ for module in list(sys.modules.values()):
142
+ module_dict = getattr(module, "__dict__", None)
143
+ if not module_dict:
144
+ continue
145
+ for attr, value in list(module_dict.items()):
146
+ if value is original:
147
+ setattr(module, attr, wrapped)
148
+
149
+
150
+ def _wrap_module_function(module_name: str, function_name: str, wrapper):
151
+ try:
152
+ module = sys.modules.get(module_name) or importlib.import_module(module_name)
153
+ except ModuleNotFoundError:
154
+ return
155
+
156
+ try:
157
+ original = getattr(module, function_name)
158
+ except AttributeError:
159
+ return
160
+
161
+ key = (module_name, function_name)
162
+ if key not in _MODULE_FUNCTION_ORIGINALS:
163
+ _MODULE_FUNCTION_ORIGINALS[key] = original
164
+
165
+ wrapped_function = FunctionWrapper(original, wrapper)
166
+ setattr(module, function_name, wrapped_function)
167
+ _replace_function_aliases(original, wrapped_function)
168
+
169
+
170
+ def _unwrap_module_function(module_name: str, function_name: str):
171
+ key = (module_name, function_name)
172
+ original = _MODULE_FUNCTION_ORIGINALS.get(key)
173
+ if not original:
174
+ return
175
+
176
+ module = sys.modules.get(module_name)
177
+ if not module:
178
+ return
179
+
180
+ current = getattr(module, function_name, None)
181
+ setattr(module, function_name, original)
182
+ if current is not None:
183
+ _replace_function_aliases(current, original)
184
+ del _MODULE_FUNCTION_ORIGINALS[key]
185
+
186
+
187
+ def _span_name(to_wrap: dict[str, str]) -> str:
188
+ class_name = to_wrap.get("class_name")
189
+ method = to_wrap.get("method")
190
+ return f"{class_name}.{method}" if class_name else method
191
+
192
+
193
+ def _record_input(span, wrapped, args, kwargs):
194
+ try:
195
+ span.set_attribute(
196
+ "lmnr.span.input",
197
+ json_dumps(
198
+ get_input_from_func_args(
199
+ wrapped,
200
+ is_method=is_method(wrapped),
201
+ func_args=list(args),
202
+ func_kwargs=kwargs,
203
+ )
204
+ ),
205
+ )
206
+ except Exception:
207
+ pass
208
+
209
+
210
+ def _record_output(span, to_wrap, value):
211
+ try:
212
+ span.set_attribute("lmnr.span.output", json_dumps(value))
213
+ except Exception:
214
+ pass
215
+
216
+
217
+ def _get_span_context_payload() -> dict[str, str] | None:
218
+ current_span: ReadableSpan = trace.get_current_span(context=get_current_context())
219
+ if current_span is trace.INVALID_SPAN:
220
+ return None
221
+
222
+ span_context = current_span.get_span_context()
223
+ if span_context is None or not span_context.is_valid:
224
+ return None
225
+
226
+ span_ids_path = []
227
+ span_path = []
228
+ if hasattr(current_span, "attributes"):
229
+ readable_span: ReadableSpan = current_span
230
+ span_ids_path = list(readable_span.attributes.get(SPAN_IDS_PATH, tuple()))
231
+ span_path = list(readable_span.attributes.get(SPAN_PATH, tuple()))
232
+
233
+ project_api_key = Laminar.get_project_api_key()
234
+ laminar_url = Laminar.get_base_http_url()
235
+
236
+ return {
237
+ "trace_id": f"{span_context.trace_id:032x}",
238
+ "span_id": f"{span_context.span_id:016x}",
239
+ "project_api_key": project_api_key or "",
240
+ "span_ids_path": span_ids_path,
241
+ "span_path": span_path,
242
+ "laminar_url": laminar_url,
243
+ }
244
+
245
+
246
+ def _publish_span_context() -> None:
247
+ payload = _get_span_context_payload()
248
+ if not payload:
249
+ return
250
+
251
+ set_trace_to_proxy(
252
+ trace_id=payload["trace_id"],
253
+ span_id=payload["span_id"],
254
+ project_api_key=payload["project_api_key"],
255
+ span_ids_path=payload["span_ids_path"],
256
+ span_path=payload["span_path"],
257
+ laminar_url=payload["laminar_url"],
258
+ )
259
+
260
+
261
+ @_with_wrapper
262
+ def _wrap_sync(to_wrap, wrapped, instance, args, kwargs):
263
+ with Laminar.start_as_current_span(
264
+ _span_name(to_wrap),
265
+ span_type=to_wrap.get("span_type", "DEFAULT"),
266
+ ) as span:
267
+ _record_input(span, wrapped, args, kwargs)
268
+
269
+ try:
270
+ result = wrapped(*args, **kwargs)
271
+ except Exception as e: # pylint: disable=broad-except
272
+ span.set_status(Status(StatusCode.ERROR))
273
+ span.record_exception(e)
274
+ raise
275
+
276
+ _record_output(span, to_wrap, result)
277
+ return result
278
+
279
+
280
+ @_with_wrapper
281
+ async def _wrap_async(to_wrap, wrapped, instance, args, kwargs):
282
+ with Laminar.start_as_current_span(
283
+ _span_name(to_wrap),
284
+ span_type=to_wrap.get("span_type", "DEFAULT"),
285
+ ) as span:
286
+ _record_input(span, wrapped, args, kwargs)
287
+
288
+ original_base_url = None
289
+ if to_wrap.get("is_start_proxy"):
290
+ original_base_url = os.environ.get("ANTHROPIC_BASE_URL")
291
+ start_proxy()
292
+
293
+ if to_wrap.get("is_publish_span_context"):
294
+ proxy_base_url = get_cc_proxy_base_url()
295
+ if proxy_base_url:
296
+ _publish_span_context()
297
+ else:
298
+ logger.debug(
299
+ "No claude proxy server found. Skipping span context publication."
300
+ )
301
+
302
+ try:
303
+ result = await wrapped(*args, **kwargs)
304
+ except Exception as e: # pylint: disable=broad-except
305
+ span.set_status(Status(StatusCode.ERROR))
306
+ span.record_exception(e)
307
+ raise
308
+ finally:
309
+ if original_base_url is not None:
310
+ if original_base_url:
311
+ os.environ["ANTHROPIC_BASE_URL"] = original_base_url
312
+ else:
313
+ os.environ.pop("ANTHROPIC_BASE_URL", None)
314
+ if to_wrap.get("is_release_proxy"):
315
+ release_proxy()
316
+
317
+ _record_output(span, to_wrap, result)
318
+
319
+ return result
320
+
321
+
322
+ @_with_wrapper
323
+ def _wrap_async_gen(to_wrap, wrapped, instance, args, kwargs):
324
+ async def generator():
325
+ span = Laminar.start_span(
326
+ _span_name(to_wrap),
327
+ span_type=to_wrap.get("span_type", "DEFAULT"),
328
+ )
329
+ collected = []
330
+ async_iter = None
331
+
332
+ original_base_url = None
333
+ if to_wrap.get("is_start_proxy"):
334
+ original_base_url = os.environ.get("ANTHROPIC_BASE_URL")
335
+ start_proxy()
336
+
337
+ if to_wrap.get("is_publish_span_context"):
338
+ with Laminar.use_span(span):
339
+ proxy_base_url = get_cc_proxy_base_url()
340
+ if proxy_base_url:
341
+ _publish_span_context()
342
+ else:
343
+ logger.debug(
344
+ "No claude proxy server found. Skipping span context publication."
345
+ )
346
+
347
+ try:
348
+ with Laminar.use_span(span):
349
+ _record_input(span, wrapped, args, kwargs)
350
+ async_source = wrapped(*args, **kwargs)
351
+ async_iter = (
352
+ async_source.__aiter__()
353
+ if hasattr(async_source, "__aiter__")
354
+ else async_source
355
+ )
356
+
357
+ while True:
358
+ try:
359
+ with Laminar.use_span(
360
+ span, record_exception=False, set_status_on_exception=False
361
+ ):
362
+ item = await async_iter.__anext__()
363
+ collected.append(item)
364
+ except StopAsyncIteration:
365
+ break
366
+ yield item
367
+ except Exception as e: # pylint: disable=broad-except
368
+ with Laminar.use_span(span):
369
+ span.set_status(Status(StatusCode.ERROR))
370
+ span.record_exception(e)
371
+ raise
372
+ finally:
373
+ if original_base_url is not None:
374
+ if original_base_url:
375
+ os.environ["ANTHROPIC_BASE_URL"] = original_base_url
376
+ else:
377
+ os.environ.pop("ANTHROPIC_BASE_URL", None)
378
+ if async_iter and hasattr(async_iter, "aclose"):
379
+ try:
380
+ with Laminar.use_span(span):
381
+ await async_iter.aclose()
382
+ except Exception: # pylint: disable=broad-except
383
+ pass
384
+ with Laminar.use_span(span):
385
+ _record_output(span, to_wrap, collected)
386
+ span.end()
387
+
388
+ if to_wrap.get("is_release_proxy"):
389
+ release_proxy()
390
+
391
+ return generator()
392
+
393
+
394
+ class ClaudeAgentInstrumentor(BaseInstrumentor):
395
+ def __init__(self):
396
+ super().__init__()
397
+
398
+ def instrumentation_dependencies(self) -> Collection[str]:
399
+ return _instruments
400
+
401
+ def _instrument(self, **kwargs):
402
+ for wrapped_method in WRAPPED_METHODS:
403
+ wrap_package = wrapped_method.get("package")
404
+ wrap_object = wrapped_method.get("object")
405
+ wrap_method = wrapped_method.get("method")
406
+ is_streaming = wrapped_method.get("is_streaming", False)
407
+ is_async = wrapped_method.get("is_async", False)
408
+
409
+ if is_streaming:
410
+ wrapper_factory = _wrap_async_gen
411
+ elif is_async:
412
+ wrapper_factory = _wrap_async
413
+ else:
414
+ wrapper_factory = _wrap_sync
415
+
416
+ wrapper = wrapper_factory(wrapped_method)
417
+
418
+ if wrap_object:
419
+ target = f"{wrap_object}.{wrap_method}"
420
+ try:
421
+ wrap_function_wrapper(
422
+ wrap_package,
423
+ target,
424
+ wrapper,
425
+ )
426
+ except (ModuleNotFoundError, AttributeError):
427
+ pass # that's ok, we don't want to fail if some methods do not exist
428
+ else:
429
+ try:
430
+ _wrap_module_function(
431
+ wrap_package,
432
+ wrap_method,
433
+ wrapper,
434
+ )
435
+ except (ModuleNotFoundError, AttributeError):
436
+ pass # that's ok
437
+
438
+ def _uninstrument(self, **kwargs):
439
+ for wrapped_method in WRAPPED_METHODS:
440
+ wrap_package = wrapped_method.get("package")
441
+ wrap_object = wrapped_method.get("object")
442
+ wrap_method = wrapped_method.get("method")
443
+
444
+ if wrap_object:
445
+ module_path = f"{wrap_package}.{wrap_object}"
446
+ try:
447
+ unwrap(module_path, wrap_method)
448
+ except (ModuleNotFoundError, AttributeError):
449
+ pass # that's ok, we don't want to fail if some methods do not exist
450
+ else:
451
+ _unwrap_module_function(wrap_package, wrap_method)
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import os
5
+ import socket
6
+ import threading
7
+ import time
8
+ from typing import Optional
9
+
10
+ from lmnr.sdk.log import get_default_logger
11
+
12
+ from lmnr_claude_code_proxy import run_server, set_current_trace, stop_server
13
+
14
+ logger = get_default_logger(__name__)
15
+
16
+ DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
17
+ DEFAULT_CC_PROXY_PORT = 45667
18
+ CC_PROXY_PORT_ATTEMPTS = 5
19
+
20
+ _CC_PROXY_LOCK = threading.Lock()
21
+ _CC_PROXY_PORT: int | None = None
22
+ _CC_PROXY_BASE_URL: str | None = None
23
+ _CC_PROXY_TARGET_URL: str | None = None
24
+ _CC_PROXY_SHUTDOWN_REGISTERED = False
25
+
26
+
27
+ def _find_available_port(start_port: int, attempts: int) -> Optional[int]:
28
+ for offset in range(attempts):
29
+ candidate = start_port + offset
30
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
31
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
32
+ try:
33
+ sock.bind(("127.0.0.1", candidate))
34
+ except OSError:
35
+ continue
36
+ return candidate
37
+ return None
38
+
39
+
40
+ def _wait_for_port(port: int, timeout: float = 5.0) -> bool:
41
+ deadline = time.monotonic() + timeout
42
+ while time.monotonic() < deadline:
43
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
44
+ sock.settimeout(0.2)
45
+ try:
46
+ sock.connect(("127.0.0.1", port))
47
+ return True
48
+ except OSError:
49
+ time.sleep(0.1)
50
+ return False
51
+
52
+
53
+ def _stop_cc_proxy_locked():
54
+ global _CC_PROXY_PORT, _CC_PROXY_BASE_URL
55
+
56
+ try:
57
+ stop_server()
58
+ except Exception as e:
59
+ logger.debug("Unable to stop cc-proxy: %s", e)
60
+
61
+ if _CC_PROXY_TARGET_URL:
62
+ os.environ["ANTHROPIC_BASE_URL"] = _CC_PROXY_TARGET_URL
63
+
64
+ _CC_PROXY_PORT = None
65
+ _CC_PROXY_BASE_URL = None
66
+
67
+
68
+ def _stop_cc_proxy():
69
+ with _CC_PROXY_LOCK:
70
+ _stop_cc_proxy_locked()
71
+
72
+
73
+ def _register_proxy_shutdown():
74
+ global _CC_PROXY_SHUTDOWN_REGISTERED
75
+ if not _CC_PROXY_SHUTDOWN_REGISTERED:
76
+ atexit.register(_stop_cc_proxy)
77
+ _CC_PROXY_SHUTDOWN_REGISTERED = True
78
+
79
+
80
+ def get_cc_proxy_base_url() -> str | None:
81
+ return _CC_PROXY_BASE_URL
82
+
83
+
84
+ def start_proxy() -> Optional[str]:
85
+ with _CC_PROXY_LOCK:
86
+ global _CC_PROXY_PORT, _CC_PROXY_BASE_URL, _CC_PROXY_TARGET_URL
87
+
88
+ port = _find_available_port(DEFAULT_CC_PROXY_PORT, CC_PROXY_PORT_ATTEMPTS)
89
+ if port is None:
90
+ logger.warning("Unable to allocate port for cc-proxy.")
91
+ return None
92
+
93
+ target_url = (
94
+ _CC_PROXY_TARGET_URL
95
+ or os.environ.get("ANTHROPIC_ORIGINAL_BASE_URL")
96
+ or os.environ.get("ANTHROPIC_BASE_URL")
97
+ or DEFAULT_ANTHROPIC_BASE_URL
98
+ )
99
+ _CC_PROXY_TARGET_URL = target_url
100
+ os.environ.setdefault("ANTHROPIC_ORIGINAL_BASE_URL", target_url)
101
+
102
+ try:
103
+ run_server(target_url, port=port)
104
+ except OSError as exc: # pragma: no cover
105
+ logger.warning("Unable to start cc-proxy: %s", exc)
106
+ return None
107
+
108
+ if not _wait_for_port(port):
109
+ logger.warning("cc-proxy failed to start on port %s", port)
110
+ stop_server()
111
+ return None
112
+
113
+ proxy_base_url = f"http://127.0.0.1:{port}"
114
+ _CC_PROXY_PORT = port
115
+ _CC_PROXY_BASE_URL = proxy_base_url
116
+ os.environ["ANTHROPIC_BASE_URL"] = proxy_base_url
117
+ _register_proxy_shutdown()
118
+
119
+ logger.info("Started claude proxy server on: " + str(proxy_base_url))
120
+ return proxy_base_url
121
+
122
+
123
+ def release_proxy() -> None:
124
+ with _CC_PROXY_LOCK:
125
+ _stop_cc_proxy_locked()
126
+ logger.debug("Released claude proxy server")
127
+
128
+
129
+ def set_trace_to_proxy(
130
+ trace_id: str,
131
+ span_id: str,
132
+ project_api_key: str,
133
+ span_path: list[str] = [],
134
+ span_ids_path: list[str] = [],
135
+ laminar_url: str = "https://api.lmnr.ai",
136
+ ):
137
+ set_current_trace(
138
+ trace_id=trace_id,
139
+ span_id=span_id,
140
+ project_api_key=project_api_key,
141
+ span_path=span_path,
142
+ span_ids_path=span_ids_path,
143
+ laminar_url=laminar_url,
144
+ )
@@ -0,0 +1,100 @@
1
+ """OpenTelemetry CUA instrumentation"""
2
+
3
+ import logging
4
+ from typing import Any, AsyncGenerator, Collection
5
+
6
+ from lmnr import Laminar
7
+ from lmnr.sdk.utils import json_dumps
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