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
@@ -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
- if not isinstance(response, dict):
138
- response = response.__dict__
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 := response.get("usage"):
141
- prompt_tokens = usage.input_tokens
142
- cache_read_tokens = dict(usage).get("cache_read_input_tokens", 0) or 0
143
- cache_creation_tokens = dict(usage).get("cache_creation_input_tokens", 0) or 0
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 := response.get("usage"):
161
- completion_tokens = usage.output_tokens
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
- if response.get("completion"):
166
- completion_tokens = await anthropic.count_tokens(
167
- response.get("completion")
168
- )
169
- elif response.get("content"):
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
- if isinstance(response.get("content"), list):
191
- choices = len(response.get("content"))
192
- elif response.get("completion"):
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: response.get("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
- if not isinstance(response, dict):
231
- response = response.__dict__
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
- if usage := response.get("usage"):
234
- prompt_tokens = usage.input_tokens
235
- cache_read_tokens = dict(usage).get("cache_read_input_tokens", 0) or 0
236
- cache_creation_tokens = dict(usage).get("cache_creation_input_tokens", 0) or 0
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 := response.get("usage"):
254
- completion_tokens = usage.output_tokens
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
- if response.get("completion"):
259
- completion_tokens = anthropic.count_tokens(response.get("completion"))
260
- elif response.get("content"):
261
- completion_tokens = anthropic.count_tokens(
262
- response.get("content")[0].text
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: response.get("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 = shared_metrics_attributes(response)
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
- _handle_response(span, event_logger, response)
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 ModuleNotFoundError:
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 set_response_attributes(span, response):
237
- from .utils import set_span_attribute
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 not isinstance(response, dict):
240
- response = response.__dict__
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
- if not isinstance(response, dict):
67
- response = response.__dict__
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: response.get("model"),
226
+ SpanAttributes.LLM_RESPONSE_MODEL: model,
75
227
  }
76
228
 
77
229