Skip to content

Commit 22e05ee

Browse files
nirgaclaudegalkleinman
authored
fix(anthropic): fix with_raw_response wrapper consistency and re-enable beta API instrumentation (#3297)
Co-authored-by: Claude <[email protected]> Co-authored-by: Gal Kleinman <[email protected]>
1 parent bb8a309 commit 22e05ee

File tree

5 files changed

+221
-109
lines changed

5 files changed

+221
-109
lines changed

packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py

Lines changed: 131 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -81,32 +81,32 @@
8181
"method": "stream",
8282
"span_name": "anthropic.chat",
8383
},
84-
# # Beta API methods (regular Anthropic SDK)
85-
# {
86-
# "package": "anthropic.resources.beta.messages.messages",
87-
# "object": "Messages",
88-
# "method": "create",
89-
# "span_name": "anthropic.chat",
90-
# },
91-
# {
92-
# "package": "anthropic.resources.beta.messages.messages",
93-
# "object": "Messages",
94-
# "method": "stream",
95-
# "span_name": "anthropic.chat",
96-
# },
97-
# # Beta API methods (Bedrock SDK)
98-
# {
99-
# "package": "anthropic.lib.bedrock._beta_messages",
100-
# "object": "Messages",
101-
# "method": "create",
102-
# "span_name": "anthropic.chat",
103-
# },
104-
# {
105-
# "package": "anthropic.lib.bedrock._beta_messages",
106-
# "object": "Messages",
107-
# "method": "stream",
108-
# "span_name": "anthropic.chat",
109-
# },
84+
# Beta API methods (regular Anthropic SDK)
85+
{
86+
"package": "anthropic.resources.beta.messages.messages",
87+
"object": "Messages",
88+
"method": "create",
89+
"span_name": "anthropic.chat",
90+
},
91+
{
92+
"package": "anthropic.resources.beta.messages.messages",
93+
"object": "Messages",
94+
"method": "stream",
95+
"span_name": "anthropic.chat",
96+
},
97+
# Beta API methods (Bedrock SDK)
98+
{
99+
"package": "anthropic.lib.bedrock._beta_messages",
100+
"object": "Messages",
101+
"method": "create",
102+
"span_name": "anthropic.chat",
103+
},
104+
{
105+
"package": "anthropic.lib.bedrock._beta_messages",
106+
"object": "Messages",
107+
"method": "stream",
108+
"span_name": "anthropic.chat",
109+
},
110110
]
111111

112112
WRAPPED_AMETHODS = [
@@ -122,32 +122,32 @@
122122
"method": "create",
123123
"span_name": "anthropic.chat",
124124
},
125-
# # Beta API async methods (regular Anthropic SDK)
126-
# {
127-
# "package": "anthropic.resources.beta.messages.messages",
128-
# "object": "AsyncMessages",
129-
# "method": "create",
130-
# "span_name": "anthropic.chat",
131-
# },
132-
# {
133-
# "package": "anthropic.resources.beta.messages.messages",
134-
# "object": "AsyncMessages",
135-
# "method": "stream",
136-
# "span_name": "anthropic.chat",
137-
# },
138-
# # Beta API async methods (Bedrock SDK)
139-
# {
140-
# "package": "anthropic.lib.bedrock._beta_messages",
141-
# "object": "AsyncMessages",
142-
# "method": "create",
143-
# "span_name": "anthropic.chat",
144-
# },
145-
# {
146-
# "package": "anthropic.lib.bedrock._beta_messages",
147-
# "object": "AsyncMessages",
148-
# "method": "stream",
149-
# "span_name": "anthropic.chat",
150-
# },
125+
# Beta API async methods (regular Anthropic SDK)
126+
{
127+
"package": "anthropic.resources.beta.messages.messages",
128+
"object": "AsyncMessages",
129+
"method": "create",
130+
"span_name": "anthropic.chat",
131+
},
132+
{
133+
"package": "anthropic.resources.beta.messages.messages",
134+
"object": "AsyncMessages",
135+
"method": "stream",
136+
"span_name": "anthropic.chat",
137+
},
138+
# Beta API async methods (Bedrock SDK)
139+
{
140+
"package": "anthropic.lib.bedrock._beta_messages",
141+
"object": "AsyncMessages",
142+
"method": "create",
143+
"span_name": "anthropic.chat",
144+
},
145+
{
146+
"package": "anthropic.lib.bedrock._beta_messages",
147+
"object": "AsyncMessages",
148+
"method": "stream",
149+
"span_name": "anthropic.chat",
150+
},
151151
]
152152

153153

@@ -182,14 +182,35 @@ async def _aset_token_usage(
182182
token_histogram: Histogram = None,
183183
choice_counter: Counter = None,
184184
):
185-
from opentelemetry.instrumentation.anthropic.utils import _aextract_response_data
185+
import inspect
186+
187+
# If we get a coroutine, await it
188+
if inspect.iscoroutine(response):
189+
try:
190+
response = await response
191+
except Exception as e:
192+
import logging
193+
logger = logging.getLogger(__name__)
194+
logger.debug(f"Failed to await coroutine response: {e}")
195+
return
196+
197+
# Handle with_raw_response wrapped responses first
198+
if response and hasattr(response, "parse") and callable(response.parse):
199+
try:
200+
response = response.parse()
201+
except Exception as e:
202+
import logging
203+
logger = logging.getLogger(__name__)
204+
logger.debug(f"Failed to parse with_raw_response: {e}")
205+
return
186206

187-
response = await _aextract_response_data(response)
207+
# Safely get usage attribute without extracting the whole object
208+
usage = getattr(response, "usage", None) if response else None
188209

189-
if usage := response.get("usage"):
190-
prompt_tokens = usage.input_tokens
191-
cache_read_tokens = dict(usage).get("cache_read_input_tokens", 0) or 0
192-
cache_creation_tokens = dict(usage).get("cache_creation_input_tokens", 0) or 0
210+
if usage:
211+
prompt_tokens = getattr(usage, "input_tokens", 0)
212+
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
213+
cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0) or 0
193214
else:
194215
prompt_tokens = await acount_prompt_tokens_from_request(anthropic, request)
195216
cache_read_tokens = 0
@@ -206,18 +227,18 @@ async def _aset_token_usage(
206227
},
207228
)
208229

209-
if usage := response.get("usage"):
210-
completion_tokens = usage.output_tokens
230+
if usage:
231+
completion_tokens = getattr(usage, "output_tokens", 0)
211232
else:
212233
completion_tokens = 0
213234
if hasattr(anthropic, "count_tokens"):
214-
if response.get("completion"):
235+
completion_attr = getattr(response, "completion", None)
236+
content_attr = getattr(response, "content", None)
237+
if completion_attr:
238+
completion_tokens = await anthropic.count_tokens(completion_attr)
239+
elif content_attr and len(content_attr) > 0:
215240
completion_tokens = await anthropic.count_tokens(
216-
response.get("completion")
217-
)
218-
elif response.get("content"):
219-
completion_tokens = await anthropic.count_tokens(
220-
response.get("content")[0].text
241+
content_attr[0].text
221242
)
222243

223244
if (
@@ -236,17 +257,19 @@ async def _aset_token_usage(
236257
total_tokens = input_tokens + completion_tokens
237258

238259
choices = 0
239-
if isinstance(response.get("content"), list):
240-
choices = len(response.get("content"))
241-
elif response.get("completion"):
260+
content_attr = getattr(response, "content", None)
261+
completion_attr = getattr(response, "completion", None)
262+
if isinstance(content_attr, list):
263+
choices = len(content_attr)
264+
elif completion_attr:
242265
choices = 1
243266

244267
if choices > 0 and choice_counter:
245268
choice_counter.add(
246269
choices,
247270
attributes={
248271
**metric_attributes,
249-
SpanAttributes.LLM_RESPONSE_STOP_REASON: response.get("stop_reason"),
272+
SpanAttributes.LLM_RESPONSE_STOP_REASON: getattr(response, "stop_reason", None),
250273
},
251274
)
252275

@@ -276,14 +299,32 @@ def _set_token_usage(
276299
token_histogram: Histogram = None,
277300
choice_counter: Counter = None,
278301
):
279-
from opentelemetry.instrumentation.anthropic.utils import _extract_response_data
302+
import inspect
303+
304+
# If we get a coroutine, we cannot process it in sync context
305+
if inspect.iscoroutine(response):
306+
import logging
307+
logger = logging.getLogger(__name__)
308+
logger.warning(f"_set_token_usage received coroutine {response} - token usage processing skipped")
309+
return
310+
311+
# Handle with_raw_response wrapped responses first
312+
if response and hasattr(response, "parse") and callable(response.parse):
313+
try:
314+
response = response.parse()
315+
except Exception as e:
316+
import logging
317+
logger = logging.getLogger(__name__)
318+
logger.debug(f"Failed to parse with_raw_response: {e}")
319+
return
280320

281-
response = _extract_response_data(response)
321+
# Safely get usage attribute without extracting the whole object
322+
usage = getattr(response, "usage", None) if response else None
282323

283-
if usage := response.get("usage"):
284-
prompt_tokens = usage.input_tokens
285-
cache_read_tokens = dict(usage).get("cache_read_input_tokens", 0) or 0
286-
cache_creation_tokens = dict(usage).get("cache_creation_input_tokens", 0) or 0
324+
if usage:
325+
prompt_tokens = getattr(usage, "input_tokens", 0)
326+
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
327+
cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0) or 0
287328
else:
288329
prompt_tokens = count_prompt_tokens_from_request(anthropic, request)
289330
cache_read_tokens = 0
@@ -300,16 +341,18 @@ def _set_token_usage(
300341
},
301342
)
302343

303-
if usage := response.get("usage"):
304-
completion_tokens = usage.output_tokens
344+
if usage:
345+
completion_tokens = getattr(usage, "output_tokens", 0)
305346
else:
306347
completion_tokens = 0
307348
if hasattr(anthropic, "count_tokens"):
308-
if response.get("completion"):
309-
completion_tokens = anthropic.count_tokens(response.get("completion"))
310-
elif response.get("content"):
349+
completion_attr = getattr(response, "completion", None)
350+
content_attr = getattr(response, "content", None)
351+
if completion_attr:
352+
completion_tokens = anthropic.count_tokens(completion_attr)
353+
elif content_attr and len(content_attr) > 0:
311354
completion_tokens = anthropic.count_tokens(
312-
response.get("content")[0].text
355+
content_attr[0].text
313356
)
314357

315358
if (
@@ -328,17 +371,19 @@ def _set_token_usage(
328371
total_tokens = input_tokens + completion_tokens
329372

330373
choices = 0
331-
if isinstance(response.get("content"), list):
332-
choices = len(response.get("content"))
333-
elif response.get("completion"):
374+
content_attr = getattr(response, "content", None)
375+
completion_attr = getattr(response, "completion", None)
376+
if isinstance(content_attr, list):
377+
choices = len(content_attr)
378+
elif completion_attr:
334379
choices = 1
335380

336381
if choices > 0 and choice_counter:
337382
choice_counter.add(
338383
choices,
339384
attributes={
340385
**metric_attributes,
341-
SpanAttributes.LLM_RESPONSE_STOP_REASON: response.get("stop_reason"),
386+
SpanAttributes.LLM_RESPONSE_STOP_REASON: getattr(response, "stop_reason", None),
342387
},
343388
)
344389

packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,27 +136,78 @@ def _extract_response_data(response):
136136

137137
@dont_throw
138138
async def ashared_metrics_attributes(response):
139-
response = await _aextract_response_data(response)
139+
import inspect
140+
141+
# If we get a coroutine, await it
142+
if inspect.iscoroutine(response):
143+
try:
144+
response = await response
145+
except Exception as e:
146+
import logging
147+
logger = logging.getLogger(__name__)
148+
logger.debug(f"Failed to await coroutine response: {e}")
149+
response = None
150+
151+
# If it's already a dict (e.g., from streaming), use it directly
152+
if isinstance(response, dict):
153+
model = response.get("model")
154+
else:
155+
# Handle with_raw_response wrapped responses first
156+
if response and hasattr(response, "parse") and callable(response.parse):
157+
try:
158+
response = response.parse()
159+
except Exception as e:
160+
import logging
161+
logger = logging.getLogger(__name__)
162+
logger.debug(f"Failed to parse with_raw_response: {e}")
163+
response = None
164+
165+
# Safely get model attribute without extracting the whole object
166+
model = getattr(response, "model", None) if response else None
140167

141168
common_attributes = Config.get_common_metrics_attributes()
142169

143170
return {
144171
**common_attributes,
145172
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
146-
SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
173+
SpanAttributes.LLM_RESPONSE_MODEL: model,
147174
}
148175

149176

150177
@dont_throw
151178
def shared_metrics_attributes(response):
152-
response = _extract_response_data(response)
179+
import inspect
180+
181+
# If we get a coroutine, we cannot process it in sync context
182+
if inspect.iscoroutine(response):
183+
import logging
184+
logger = logging.getLogger(__name__)
185+
logger.warning(f"shared_metrics_attributes received coroutine {response} - using None for model")
186+
response = None
187+
188+
# If it's already a dict (e.g., from streaming), use it directly
189+
if isinstance(response, dict):
190+
model = response.get("model")
191+
else:
192+
# Handle with_raw_response wrapped responses first
193+
if response and hasattr(response, "parse") and callable(response.parse):
194+
try:
195+
response = response.parse()
196+
except Exception as e:
197+
import logging
198+
logger = logging.getLogger(__name__)
199+
logger.debug(f"Failed to parse with_raw_response: {e}")
200+
response = None
201+
202+
# Safely get model attribute without extracting the whole object
203+
model = getattr(response, "model", None) if response else None
153204

154205
common_attributes = Config.get_common_metrics_attributes()
155206

156207
return {
157208
**common_attributes,
158209
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
159-
SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
210+
SpanAttributes.LLM_RESPONSE_MODEL: model,
160211
}
161212

162213

0 commit comments

Comments
 (0)