lmnr 0.7.10__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.
- lmnr/opentelemetry_lib/__init__.py +6 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +1 -1
- lmnr/opentelemetry_lib/litellm/__init__.py +277 -32
- lmnr/opentelemetry_lib/litellm/utils.py +76 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +136 -44
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +93 -6
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +155 -3
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +477 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +14 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +10 -1
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +100 -8
- lmnr/opentelemetry_lib/tracing/__init__.py +9 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +56 -3
- lmnr/opentelemetry_lib/tracing/exporter.py +24 -9
- lmnr/opentelemetry_lib/tracing/instruments.py +8 -0
- lmnr/opentelemetry_lib/tracing/processor.py +26 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +12 -7
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +318 -87
- lmnr/sdk/evaluations.py +22 -2
- lmnr/sdk/laminar.py +17 -3
- lmnr/version.py +1 -1
- {lmnr-0.7.10.dist-info → lmnr-0.7.12.dist-info}/METADATA +50 -50
- {lmnr-0.7.10.dist-info → lmnr-0.7.12.dist-info}/RECORD +28 -24
- {lmnr-0.7.10.dist-info → lmnr-0.7.12.dist-info}/WHEEL +0 -0
- {lmnr-0.7.10.dist-info → lmnr-0.7.12.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ from .event_emitter import (
|
|
13
13
|
)
|
14
14
|
from .span_utils import (
|
15
15
|
aset_input_attributes,
|
16
|
+
aset_response_attributes,
|
16
17
|
set_response_attributes,
|
17
18
|
)
|
18
19
|
from .streaming import (
|
@@ -21,6 +22,7 @@ from .streaming import (
|
|
21
22
|
)
|
22
23
|
from .utils import (
|
23
24
|
acount_prompt_tokens_from_request,
|
25
|
+
ashared_metrics_attributes,
|
24
26
|
count_prompt_tokens_from_request,
|
25
27
|
dont_throw,
|
26
28
|
error_metrics_attributes,
|
@@ -85,6 +87,46 @@ WRAPPED_METHODS = [
|
|
85
87
|
"method": "stream",
|
86
88
|
"span_name": "anthropic.chat",
|
87
89
|
},
|
90
|
+
# Beta API methods (regular Anthropic SDK)
|
91
|
+
{
|
92
|
+
"package": "anthropic.resources.beta.messages.messages",
|
93
|
+
"object": "Messages",
|
94
|
+
"method": "create",
|
95
|
+
"span_name": "anthropic.chat",
|
96
|
+
},
|
97
|
+
{
|
98
|
+
"package": "anthropic.resources.beta.messages.messages",
|
99
|
+
"object": "Messages",
|
100
|
+
"method": "stream",
|
101
|
+
"span_name": "anthropic.chat",
|
102
|
+
},
|
103
|
+
# read note on async with above
|
104
|
+
{
|
105
|
+
"package": "anthropic.resources.beta.messages.messages",
|
106
|
+
"object": "AsyncMessages",
|
107
|
+
"method": "stream",
|
108
|
+
"span_name": "anthropic.chat",
|
109
|
+
},
|
110
|
+
# Beta API methods (Bedrock SDK)
|
111
|
+
{
|
112
|
+
"package": "anthropic.lib.bedrock._beta_messages",
|
113
|
+
"object": "Messages",
|
114
|
+
"method": "create",
|
115
|
+
"span_name": "anthropic.chat",
|
116
|
+
},
|
117
|
+
{
|
118
|
+
"package": "anthropic.lib.bedrock._beta_messages",
|
119
|
+
"object": "Messages",
|
120
|
+
"method": "stream",
|
121
|
+
"span_name": "anthropic.chat",
|
122
|
+
},
|
123
|
+
# read note on async with above
|
124
|
+
{
|
125
|
+
"package": "anthropic.lib.bedrock._beta_messages",
|
126
|
+
"object": "AsyncMessages",
|
127
|
+
"method": "stream",
|
128
|
+
"span_name": "anthropic.chat",
|
129
|
+
},
|
88
130
|
]
|
89
131
|
|
90
132
|
WRAPPED_AMETHODS = [
|
@@ -100,6 +142,20 @@ WRAPPED_AMETHODS = [
|
|
100
142
|
"method": "create",
|
101
143
|
"span_name": "anthropic.chat",
|
102
144
|
},
|
145
|
+
# Beta API async methods (regular Anthropic SDK)
|
146
|
+
{
|
147
|
+
"package": "anthropic.resources.beta.messages.messages",
|
148
|
+
"object": "AsyncMessages",
|
149
|
+
"method": "create",
|
150
|
+
"span_name": "anthropic.chat",
|
151
|
+
},
|
152
|
+
# Beta API async methods (Bedrock SDK)
|
153
|
+
{
|
154
|
+
"package": "anthropic.lib.bedrock._beta_messages",
|
155
|
+
"object": "AsyncMessages",
|
156
|
+
"method": "create",
|
157
|
+
"span_name": "anthropic.chat",
|
158
|
+
},
|
103
159
|
]
|
104
160
|
|
105
161
|
|
@@ -134,13 +190,20 @@ async def _aset_token_usage(
|
|
134
190
|
token_histogram: Histogram = None,
|
135
191
|
choice_counter: Counter = None,
|
136
192
|
):
|
137
|
-
|
138
|
-
|
193
|
+
# Handle with_raw_response wrapped responses first
|
194
|
+
if response and hasattr(response, "parse") and callable(response.parse):
|
195
|
+
try:
|
196
|
+
response = response.parse()
|
197
|
+
except Exception as e:
|
198
|
+
logger.debug(f"Failed to parse with_raw_response: {e}")
|
199
|
+
return
|
200
|
+
|
201
|
+
usage = getattr(response, "usage", None) if response else None
|
139
202
|
|
140
|
-
if usage
|
141
|
-
prompt_tokens = usage
|
142
|
-
cache_read_tokens =
|
143
|
-
cache_creation_tokens =
|
203
|
+
if usage:
|
204
|
+
prompt_tokens = getattr(usage, "input_tokens", 0)
|
205
|
+
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
|
206
|
+
cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
144
207
|
else:
|
145
208
|
prompt_tokens = await acount_prompt_tokens_from_request(anthropic, request)
|
146
209
|
cache_read_tokens = 0
|
@@ -157,19 +220,17 @@ async def _aset_token_usage(
|
|
157
220
|
},
|
158
221
|
)
|
159
222
|
|
160
|
-
if usage
|
161
|
-
completion_tokens = usage
|
223
|
+
if usage:
|
224
|
+
completion_tokens = getattr(usage, "output_tokens", 0)
|
162
225
|
else:
|
163
226
|
completion_tokens = 0
|
164
227
|
if hasattr(anthropic, "count_tokens"):
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
)
|
169
|
-
elif
|
170
|
-
completion_tokens = await anthropic.count_tokens(
|
171
|
-
response.get("content")[0].text
|
172
|
-
)
|
228
|
+
completion_attr = getattr(response, "completion", None)
|
229
|
+
content_attr = getattr(response, "content", None)
|
230
|
+
if completion_attr:
|
231
|
+
completion_tokens = await anthropic.count_tokens(completion_attr)
|
232
|
+
elif content_attr:
|
233
|
+
completion_tokens = await anthropic.count_tokens(content_attr[0].text)
|
173
234
|
|
174
235
|
if (
|
175
236
|
token_histogram
|
@@ -187,9 +248,11 @@ async def _aset_token_usage(
|
|
187
248
|
total_tokens = input_tokens + completion_tokens
|
188
249
|
|
189
250
|
choices = 0
|
190
|
-
|
191
|
-
|
192
|
-
|
251
|
+
content_attr = getattr(response, "content", None)
|
252
|
+
completion_attr = getattr(response, "completion", None)
|
253
|
+
if isinstance(content_attr, list):
|
254
|
+
choices = len(content_attr)
|
255
|
+
elif completion_attr:
|
193
256
|
choices = 1
|
194
257
|
|
195
258
|
if choices > 0 and choice_counter:
|
@@ -197,7 +260,9 @@ async def _aset_token_usage(
|
|
197
260
|
choices,
|
198
261
|
attributes={
|
199
262
|
**metric_attributes,
|
200
|
-
SpanAttributes.LLM_RESPONSE_STOP_REASON:
|
263
|
+
SpanAttributes.LLM_RESPONSE_STOP_REASON: getattr(
|
264
|
+
response, "stop_reason", None
|
265
|
+
),
|
201
266
|
},
|
202
267
|
)
|
203
268
|
|
@@ -227,13 +292,20 @@ def _set_token_usage(
|
|
227
292
|
token_histogram: Histogram = None,
|
228
293
|
choice_counter: Counter = None,
|
229
294
|
):
|
230
|
-
|
231
|
-
|
295
|
+
# Handle with_raw_response wrapped responses first
|
296
|
+
if response and hasattr(response, "parse") and callable(response.parse):
|
297
|
+
try:
|
298
|
+
response = response.parse()
|
299
|
+
except Exception as e:
|
300
|
+
logger.debug(f"Failed to parse with_raw_response: {e}")
|
301
|
+
return
|
232
302
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
303
|
+
usage = getattr(response, "usage", None) if response else None
|
304
|
+
|
305
|
+
if usage:
|
306
|
+
prompt_tokens = getattr(usage, "input_tokens", 0)
|
307
|
+
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
|
308
|
+
cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0) or 0
|
237
309
|
else:
|
238
310
|
prompt_tokens = count_prompt_tokens_from_request(anthropic, request)
|
239
311
|
cache_read_tokens = 0
|
@@ -250,17 +322,17 @@ def _set_token_usage(
|
|
250
322
|
},
|
251
323
|
)
|
252
324
|
|
253
|
-
if usage
|
254
|
-
completion_tokens = usage
|
325
|
+
if usage:
|
326
|
+
completion_tokens = getattr(usage, "output_tokens", 0)
|
255
327
|
else:
|
256
328
|
completion_tokens = 0
|
257
329
|
if hasattr(anthropic, "count_tokens"):
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
completion_tokens = anthropic.count_tokens(
|
262
|
-
|
263
|
-
)
|
330
|
+
completion_attr = getattr(response, "completion", None)
|
331
|
+
content_attr = getattr(response, "content", None)
|
332
|
+
if completion_attr:
|
333
|
+
completion_tokens = anthropic.count_tokens(completion_attr)
|
334
|
+
elif content_attr:
|
335
|
+
completion_tokens = anthropic.count_tokens(content_attr[0].text)
|
264
336
|
|
265
337
|
if (
|
266
338
|
token_histogram
|
@@ -274,21 +346,23 @@ def _set_token_usage(
|
|
274
346
|
SpanAttributes.LLM_TOKEN_TYPE: "output",
|
275
347
|
},
|
276
348
|
)
|
277
|
-
|
278
349
|
total_tokens = input_tokens + completion_tokens
|
279
|
-
|
280
350
|
choices = 0
|
281
|
-
if isinstance(response.get("content"), list):
|
282
|
-
choices = len(response.get("content"))
|
283
|
-
elif response.get("completion"):
|
284
|
-
choices = 1
|
285
351
|
|
352
|
+
content_attr = getattr(response, "content", None)
|
353
|
+
completion_attr = getattr(response, "completion", None)
|
354
|
+
if isinstance(content_attr, list):
|
355
|
+
choices = len(content_attr)
|
356
|
+
elif completion_attr:
|
357
|
+
choices = 1
|
286
358
|
if choices > 0 and choice_counter:
|
287
359
|
choice_counter.add(
|
288
360
|
choices,
|
289
361
|
attributes={
|
290
362
|
**metric_attributes,
|
291
|
-
SpanAttributes.LLM_RESPONSE_STOP_REASON:
|
363
|
+
SpanAttributes.LLM_RESPONSE_STOP_REASON: getattr(
|
364
|
+
response, "stop_reason", None
|
365
|
+
),
|
292
366
|
},
|
293
367
|
)
|
294
368
|
|
@@ -398,6 +472,17 @@ def _handle_response(span: Span, event_logger: Optional[EventLogger], response):
|
|
398
472
|
set_response_attributes(span, response)
|
399
473
|
|
400
474
|
|
475
|
+
@dont_throw
|
476
|
+
async def _ahandle_response(span: Span, event_logger: Optional[EventLogger], response):
|
477
|
+
if should_emit_events():
|
478
|
+
emit_response_events(event_logger, response)
|
479
|
+
else:
|
480
|
+
if not span.is_recording():
|
481
|
+
return
|
482
|
+
|
483
|
+
await aset_response_attributes(span, response)
|
484
|
+
|
485
|
+
|
401
486
|
@_with_chat_telemetry_wrapper
|
402
487
|
def _wrap(
|
403
488
|
tracer: Tracer,
|
@@ -612,7 +697,7 @@ async def _awrap(
|
|
612
697
|
kwargs,
|
613
698
|
)
|
614
699
|
elif response:
|
615
|
-
metric_attributes =
|
700
|
+
metric_attributes = await ashared_metrics_attributes(response)
|
616
701
|
|
617
702
|
if duration_histogram:
|
618
703
|
duration = time.time() - start_time
|
@@ -621,7 +706,7 @@ async def _awrap(
|
|
621
706
|
attributes=metric_attributes,
|
622
707
|
)
|
623
708
|
|
624
|
-
|
709
|
+
await _ahandle_response(span, event_logger, response)
|
625
710
|
|
626
711
|
if span.is_recording():
|
627
712
|
await _aset_token_usage(
|
@@ -716,6 +801,13 @@ class AnthropicInstrumentor(BaseInstrumentor):
|
|
716
801
|
wrapped_method,
|
717
802
|
),
|
718
803
|
)
|
804
|
+
logger.debug(
|
805
|
+
f"Successfully wrapped {wrap_package}.{wrap_object}.{wrap_method}"
|
806
|
+
)
|
807
|
+
except Exception as e:
|
808
|
+
logger.debug(
|
809
|
+
f"Failed to wrap {wrap_package}.{wrap_object}.{wrap_method}: {e}"
|
810
|
+
)
|
719
811
|
except ModuleNotFoundError:
|
720
812
|
pass # that's ok, we don't want to fail if some methods do not exist
|
721
813
|
|
@@ -737,7 +829,7 @@ class AnthropicInstrumentor(BaseInstrumentor):
|
|
737
829
|
wrapped_method,
|
738
830
|
),
|
739
831
|
)
|
740
|
-
except
|
832
|
+
except Exception:
|
741
833
|
pass # that's ok, we don't want to fail if some methods do not exist
|
742
834
|
|
743
835
|
def _uninstrument(self, **kwargs):
|
@@ -8,6 +8,9 @@ from .utils import (
|
|
8
8
|
dont_throw,
|
9
9
|
model_as_dict,
|
10
10
|
should_send_prompts,
|
11
|
+
_extract_response_data,
|
12
|
+
_aextract_response_data,
|
13
|
+
set_span_attribute,
|
11
14
|
)
|
12
15
|
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
|
13
16
|
GEN_AI_RESPONSE_ID,
|
@@ -165,6 +168,73 @@ async def aset_input_attributes(span, kwargs):
|
|
165
168
|
)
|
166
169
|
|
167
170
|
|
171
|
+
async def _aset_span_completions(span, response):
|
172
|
+
if not should_send_prompts():
|
173
|
+
return
|
174
|
+
|
175
|
+
response = await _aextract_response_data(response)
|
176
|
+
index = 0
|
177
|
+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
|
178
|
+
set_span_attribute(span, f"{prefix}.finish_reason", response.get("stop_reason"))
|
179
|
+
if response.get("role"):
|
180
|
+
set_span_attribute(span, f"{prefix}.role", response.get("role"))
|
181
|
+
|
182
|
+
if response.get("completion"):
|
183
|
+
set_span_attribute(span, f"{prefix}.content", response.get("completion"))
|
184
|
+
elif response.get("content"):
|
185
|
+
tool_call_index = 0
|
186
|
+
text = ""
|
187
|
+
for content in response.get("content"):
|
188
|
+
content_block_type = content.type
|
189
|
+
# usually, Antrhopic responds with just one text block,
|
190
|
+
# but the API allows for multiple text blocks, so concatenate them
|
191
|
+
if content_block_type == "text" and hasattr(content, "text"):
|
192
|
+
text += content.text
|
193
|
+
elif content_block_type == "thinking":
|
194
|
+
content = dict(content)
|
195
|
+
# override the role to thinking
|
196
|
+
set_span_attribute(
|
197
|
+
span,
|
198
|
+
f"{prefix}.role",
|
199
|
+
"thinking",
|
200
|
+
)
|
201
|
+
set_span_attribute(
|
202
|
+
span,
|
203
|
+
f"{prefix}.content",
|
204
|
+
content.get("thinking"),
|
205
|
+
)
|
206
|
+
# increment the index for subsequent content blocks
|
207
|
+
index += 1
|
208
|
+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
|
209
|
+
# set the role to the original role on the next completions
|
210
|
+
set_span_attribute(
|
211
|
+
span,
|
212
|
+
f"{prefix}.role",
|
213
|
+
response.get("role"),
|
214
|
+
)
|
215
|
+
elif content_block_type == "tool_use":
|
216
|
+
content = dict(content)
|
217
|
+
set_span_attribute(
|
218
|
+
span,
|
219
|
+
f"{prefix}.tool_calls.{tool_call_index}.id",
|
220
|
+
content.get("id"),
|
221
|
+
)
|
222
|
+
set_span_attribute(
|
223
|
+
span,
|
224
|
+
f"{prefix}.tool_calls.{tool_call_index}.name",
|
225
|
+
content.get("name"),
|
226
|
+
)
|
227
|
+
tool_arguments = content.get("input")
|
228
|
+
if tool_arguments is not None:
|
229
|
+
set_span_attribute(
|
230
|
+
span,
|
231
|
+
f"{prefix}.tool_calls.{tool_call_index}.arguments",
|
232
|
+
json.dumps(tool_arguments),
|
233
|
+
)
|
234
|
+
tool_call_index += 1
|
235
|
+
set_span_attribute(span, f"{prefix}.content", text)
|
236
|
+
|
237
|
+
|
168
238
|
def _set_span_completions(span, response):
|
169
239
|
if not should_send_prompts():
|
170
240
|
return
|
@@ -233,11 +303,30 @@ def _set_span_completions(span, response):
|
|
233
303
|
|
234
304
|
|
235
305
|
@dont_throw
|
236
|
-
def
|
237
|
-
|
306
|
+
async def aset_response_attributes(span, response):
|
307
|
+
response = await _aextract_response_data(response)
|
308
|
+
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
|
309
|
+
set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
|
238
310
|
|
239
|
-
if
|
240
|
-
|
311
|
+
if response.get("usage"):
|
312
|
+
prompt_tokens = response.get("usage").input_tokens
|
313
|
+
completion_tokens = response.get("usage").output_tokens
|
314
|
+
set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, prompt_tokens)
|
315
|
+
set_span_attribute(
|
316
|
+
span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
|
317
|
+
)
|
318
|
+
set_span_attribute(
|
319
|
+
span,
|
320
|
+
SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
|
321
|
+
prompt_tokens + completion_tokens,
|
322
|
+
)
|
323
|
+
|
324
|
+
await _aset_span_completions(span, response)
|
325
|
+
|
326
|
+
|
327
|
+
@dont_throw
|
328
|
+
def set_response_attributes(span, response):
|
329
|
+
response = _extract_response_data(response)
|
241
330
|
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
|
242
331
|
set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
|
243
332
|
|
@@ -262,8 +351,6 @@ def set_streaming_response_attributes(span, complete_response_events):
|
|
262
351
|
if not should_send_prompts():
|
263
352
|
return
|
264
353
|
|
265
|
-
from .utils import set_span_attribute
|
266
|
-
|
267
354
|
if not span.is_recording() or not complete_response_events:
|
268
355
|
return
|
269
356
|
|
@@ -61,17 +61,169 @@ def dont_throw(func):
|
|
61
61
|
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
62
62
|
|
63
63
|
|
64
|
+
async def _aextract_response_data(response):
|
65
|
+
"""Async version of _extract_response_data that can await coroutines."""
|
66
|
+
import inspect
|
67
|
+
|
68
|
+
# If we get a coroutine, await it
|
69
|
+
if inspect.iscoroutine(response):
|
70
|
+
try:
|
71
|
+
response = await response
|
72
|
+
except Exception as e:
|
73
|
+
import logging
|
74
|
+
|
75
|
+
logger = logging.getLogger(__name__)
|
76
|
+
logger.debug(f"Failed to await coroutine response: {e}")
|
77
|
+
return {}
|
78
|
+
|
79
|
+
if isinstance(response, dict):
|
80
|
+
return response
|
81
|
+
|
82
|
+
# Handle with_raw_response wrapped responses
|
83
|
+
if hasattr(response, "parse") and callable(response.parse):
|
84
|
+
try:
|
85
|
+
# For with_raw_response, parse() gives us the actual response object
|
86
|
+
parsed_response = response.parse()
|
87
|
+
if not isinstance(parsed_response, dict):
|
88
|
+
parsed_response = parsed_response.__dict__
|
89
|
+
return parsed_response
|
90
|
+
except Exception as e:
|
91
|
+
import logging
|
92
|
+
|
93
|
+
logger = logging.getLogger(__name__)
|
94
|
+
logger.debug(
|
95
|
+
f"Failed to parse response: {e}, response type: {type(response)}"
|
96
|
+
)
|
97
|
+
|
98
|
+
# Fallback to __dict__ for regular response objects
|
99
|
+
if hasattr(response, "__dict__"):
|
100
|
+
response_dict = response.__dict__
|
101
|
+
return response_dict
|
102
|
+
|
103
|
+
return {}
|
104
|
+
|
105
|
+
|
106
|
+
def _extract_response_data(response):
|
107
|
+
"""Extract the actual response data from both regular and with_raw_response wrapped responses."""
|
108
|
+
import inspect
|
109
|
+
|
110
|
+
# If we get a coroutine, we cannot process it in sync context
|
111
|
+
if inspect.iscoroutine(response):
|
112
|
+
import logging
|
113
|
+
|
114
|
+
logger = logging.getLogger(__name__)
|
115
|
+
logger.warning(
|
116
|
+
f"_extract_response_data received coroutine {response} - response processing skipped"
|
117
|
+
)
|
118
|
+
return {}
|
119
|
+
|
120
|
+
if isinstance(response, dict):
|
121
|
+
return response
|
122
|
+
|
123
|
+
# Handle with_raw_response wrapped responses
|
124
|
+
if hasattr(response, "parse") and callable(response.parse):
|
125
|
+
try:
|
126
|
+
# For with_raw_response, parse() gives us the actual response object
|
127
|
+
parsed_response = response.parse()
|
128
|
+
if not isinstance(parsed_response, dict):
|
129
|
+
parsed_response = parsed_response.__dict__
|
130
|
+
return parsed_response
|
131
|
+
except Exception as e:
|
132
|
+
import logging
|
133
|
+
|
134
|
+
logger = logging.getLogger(__name__)
|
135
|
+
logger.debug(
|
136
|
+
f"Failed to parse response: {e}, response type: {type(response)}"
|
137
|
+
)
|
138
|
+
|
139
|
+
# Fallback to __dict__ for regular response objects
|
140
|
+
if hasattr(response, "__dict__"):
|
141
|
+
response_dict = response.__dict__
|
142
|
+
return response_dict
|
143
|
+
|
144
|
+
return {}
|
145
|
+
|
146
|
+
|
147
|
+
@dont_throw
|
148
|
+
async def ashared_metrics_attributes(response):
|
149
|
+
import inspect
|
150
|
+
|
151
|
+
# If we get a coroutine, await it
|
152
|
+
if inspect.iscoroutine(response):
|
153
|
+
try:
|
154
|
+
response = await response
|
155
|
+
except Exception as e:
|
156
|
+
import logging
|
157
|
+
|
158
|
+
logger = logging.getLogger(__name__)
|
159
|
+
logger.debug(f"Failed to await coroutine response: {e}")
|
160
|
+
response = None
|
161
|
+
|
162
|
+
# If it's already a dict (e.g., from streaming), use it directly
|
163
|
+
if isinstance(response, dict):
|
164
|
+
model = response.get("model")
|
165
|
+
else:
|
166
|
+
# Handle with_raw_response wrapped responses first
|
167
|
+
if response and hasattr(response, "parse") and callable(response.parse):
|
168
|
+
try:
|
169
|
+
response = response.parse()
|
170
|
+
except Exception as e:
|
171
|
+
import logging
|
172
|
+
|
173
|
+
logger = logging.getLogger(__name__)
|
174
|
+
logger.debug(f"Failed to parse with_raw_response: {e}")
|
175
|
+
response = None
|
176
|
+
|
177
|
+
# Safely get model attribute without extracting the whole object
|
178
|
+
model = getattr(response, "model", None) if response else None
|
179
|
+
|
180
|
+
common_attributes = Config.get_common_metrics_attributes()
|
181
|
+
|
182
|
+
return {
|
183
|
+
**common_attributes,
|
184
|
+
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
|
185
|
+
SpanAttributes.LLM_RESPONSE_MODEL: model,
|
186
|
+
}
|
187
|
+
|
188
|
+
|
64
189
|
@dont_throw
|
65
190
|
def shared_metrics_attributes(response):
|
66
|
-
|
67
|
-
|
191
|
+
import inspect
|
192
|
+
|
193
|
+
# If we get a coroutine, we cannot process it in sync context
|
194
|
+
if inspect.iscoroutine(response):
|
195
|
+
import logging
|
196
|
+
|
197
|
+
logger = logging.getLogger(__name__)
|
198
|
+
logger.warning(
|
199
|
+
f"shared_metrics_attributes received coroutine {response} - using None for model"
|
200
|
+
)
|
201
|
+
response = None
|
202
|
+
|
203
|
+
# If it's already a dict (e.g., from streaming), use it directly
|
204
|
+
if isinstance(response, dict):
|
205
|
+
model = response.get("model")
|
206
|
+
else:
|
207
|
+
# Handle with_raw_response wrapped responses first
|
208
|
+
if response and hasattr(response, "parse") and callable(response.parse):
|
209
|
+
try:
|
210
|
+
response = response.parse()
|
211
|
+
except Exception as e:
|
212
|
+
import logging
|
213
|
+
|
214
|
+
logger = logging.getLogger(__name__)
|
215
|
+
logger.debug(f"Failed to parse with_raw_response: {e}")
|
216
|
+
response = None
|
217
|
+
|
218
|
+
# Safely get model attribute without extracting the whole object
|
219
|
+
model = getattr(response, "model", None) if response else None
|
68
220
|
|
69
221
|
common_attributes = Config.get_common_metrics_attributes()
|
70
222
|
|
71
223
|
return {
|
72
224
|
**common_attributes,
|
73
225
|
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
|
74
|
-
SpanAttributes.LLM_RESPONSE_MODEL:
|
226
|
+
SpanAttributes.LLM_RESPONSE_MODEL: model,
|
75
227
|
}
|
76
228
|
|
77
229
|
|