Skip to content

Commit 2f718ec

Browse files
dinmukhamedmnirga
andauthored
fix(anthropic): various fixes around tools parsing (#3204)
Co-authored-by: Nir Gazit <[email protected]>
1 parent 391094f commit 2f718ec

10 files changed

+2152
-152
lines changed

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import asdict
22
from enum import Enum
3+
import json
34
from typing import Optional, Union
45

56
from opentelemetry._events import Event, EventLogger
@@ -127,8 +128,29 @@ def emit_streaming_response_events(
127128
event_logger: Optional[EventLogger], complete_response: dict
128129
):
129130
for message in complete_response.get("events", []):
130-
emit_event(
131-
ChoiceEvent(
131+
# Parse tool calls
132+
if message.get("type") == "tool_use":
133+
tool_calls = [
134+
ToolCall(
135+
id=message.get("id"),
136+
function={
137+
"name": message.get("name"),
138+
"arguments": json.loads(message.get("input", '{}')),
139+
},
140+
type="function",
141+
)
142+
]
143+
event = ChoiceEvent(
144+
index=message.get("index", 0),
145+
message={
146+
"content": None,
147+
"role": message.get("role", "assistant"),
148+
},
149+
finish_reason=message.get("finish_reason", "unknown"),
150+
tool_calls=tool_calls,
151+
)
152+
else:
153+
event = ChoiceEvent(
132154
index=message.get("index", 0),
133155
message={
134156
"content": {
@@ -138,9 +160,8 @@ def emit_streaming_response_events(
138160
"role": message.get("role", "assistant"),
139161
},
140162
finish_reason=message.get("finish_reason", "unknown"),
141-
),
142-
event_logger,
143-
)
163+
)
164+
emit_event(event, event_logger)
144165

145166

146167
def emit_event(

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

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,43 @@ async def aset_input_attributes(span, kwargs):
113113
)
114114
for i, message in enumerate(kwargs.get("messages")):
115115
prompt_index = i + (1 if has_system_message else 0)
116+
content = message.get("content")
117+
tool_use_blocks = []
118+
other_blocks = []
119+
if isinstance(content, list):
120+
for block in content:
121+
if dict(block).get("type") == "tool_use":
122+
tool_use_blocks.append(dict(block))
123+
else:
124+
other_blocks.append(block)
125+
content = other_blocks
116126
set_span_attribute(
117127
span,
118128
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
119-
await _dump_content(
120-
message_index=i, span=span, content=message.get("content")
121-
),
129+
await _dump_content(message_index=i, span=span, content=content),
122130
)
123131
set_span_attribute(
124132
span,
125133
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
126134
message.get("role"),
127135
)
136+
if tool_use_blocks:
137+
for tool_num, tool_use_block in enumerate(tool_use_blocks):
138+
set_span_attribute(
139+
span,
140+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.id",
141+
tool_use_block.get("id"),
142+
)
143+
set_span_attribute(
144+
span,
145+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.name",
146+
tool_use_block.get("name"),
147+
)
148+
set_span_attribute(
149+
span,
150+
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.arguments",
151+
json.dumps(tool_use_block.get("input")),
152+
)
128153

129154
if kwargs.get("tools") is not None:
130155
for i, tool in enumerate(kwargs.get("tools")):
@@ -160,7 +185,7 @@ def _set_span_completions(span, response):
160185
content_block_type = content.type
161186
# usually, Antrhopic responds with just one text block,
162187
# but the API allows for multiple text blocks, so concatenate them
163-
if content_block_type == "text":
188+
if content_block_type == "text" and hasattr(content, "text"):
164189
text += content.text
165190
elif content_block_type == "thinking":
166191
content = dict(content)
@@ -242,15 +267,32 @@ def set_streaming_response_attributes(span, complete_response_events):
242267
if not span.is_recording() or not complete_response_events:
243268
return
244269

245-
try:
246-
for event in complete_response_events:
247-
index = event.get("index")
248-
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
270+
index = 0
271+
for event in complete_response_events:
272+
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
273+
set_span_attribute(span, f"{prefix}.finish_reason", event.get("finish_reason"))
274+
role = "thinking" if event.get("type") == "thinking" else "assistant"
275+
# Thinking is added as a separate completion, so we need to increment the index
276+
if event.get("type") == "thinking":
277+
index += 1
278+
set_span_attribute(span, f"{prefix}.role", role)
279+
if event.get("type") == "tool_use":
280+
set_span_attribute(
281+
span,
282+
f"{prefix}.tool_calls.0.id",
283+
event.get("id"),
284+
)
249285
set_span_attribute(
250-
span, f"{prefix}.finish_reason", event.get("finish_reason")
286+
span,
287+
f"{prefix}.tool_calls.0.name",
288+
event.get("name"),
251289
)
252-
role = "thinking" if event.get("type") == "thinking" else "assistant"
253-
set_span_attribute(span, f"{prefix}.role", role)
290+
tool_arguments = event.get("input")
291+
if tool_arguments is not None:
292+
set_span_attribute(
293+
span,
294+
f"{prefix}.tool_calls.0.arguments",
295+
tool_arguments,
296+
)
297+
else:
254298
set_span_attribute(span, f"{prefix}.content", event.get("text"))
255-
except Exception as e:
256-
logger.warning("Failed to set completion attributes, error: %s", str(e))

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,18 @@ def _process_response_item(item, complete_response):
4040
complete_response["events"].append(
4141
{"index": index, "text": "", "type": item.content_block.type}
4242
)
43-
elif item.type == "content_block_delta" and item.delta.type in [
44-
"thinking_delta",
45-
"text_delta",
46-
]:
43+
if item.content_block.type == "tool_use":
44+
complete_response["events"][index]["id"] = item.content_block.id
45+
complete_response["events"][index]["name"] = item.content_block.name
46+
complete_response["events"][index]["input"] = """"""
47+
elif item.type == "content_block_delta":
4748
index = item.index
4849
if item.delta.type == "thinking_delta":
4950
complete_response["events"][index]["text"] += item.delta.thinking
5051
elif item.delta.type == "text_delta":
5152
complete_response["events"][index]["text"] += item.delta.text
53+
elif item.delta.type == "input_json_delta":
54+
complete_response["events"][index]["input"] += item.delta.partial_json
5255
elif item.type == "message_delta":
5356
for event in complete_response.get("events", []):
5457
event["finish_reason"] = item.delta.stop_reason
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
interactions:
2+
- request:
3+
body: '{"max_tokens":1024,"messages":[{"role":"user","content":"What is the weather
4+
and current time in San Francisco?"},{"role":"assistant","content":[{"type":"text","text":"I''ll
5+
help you get the weather and current time in San Francisco."},{"id":"call_1","type":"tool_use","name":"get_weather","input":{"location":"San
6+
Francisco, CA"}}]},{"role":"user","content":[{"type":"tool_result","content":"Sunny
7+
and 65 degrees Fahrenheit","tool_use_id":"call_1"}]}],"model":"claude-3-5-haiku-20241022","tools":[{"name":"get_weather","description":"Get
8+
the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The
9+
city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"The
10+
unit of temperature, either ''celsius'' or ''fahrenheit''"}},"required":["location"]}},{"name":"get_time","description":"Get
11+
the current time in a given time zone","input_schema":{"type":"object","properties":{"timezone":{"type":"string","description":"The
12+
IANA time zone name, e.g. America/Los_Angeles"}},"required":["timezone"]}}]}'
13+
headers:
14+
accept:
15+
- application/json
16+
accept-encoding:
17+
- gzip, deflate, zstd
18+
anthropic-version:
19+
- '2023-06-01'
20+
connection:
21+
- keep-alive
22+
content-length:
23+
- '1117'
24+
content-type:
25+
- application/json
26+
host:
27+
- api.anthropic.com
28+
user-agent:
29+
- Anthropic/Python 0.57.1
30+
x-stainless-arch:
31+
- arm64
32+
x-stainless-async:
33+
- 'false'
34+
x-stainless-lang:
35+
- python
36+
x-stainless-os:
37+
- MacOS
38+
x-stainless-package-version:
39+
- 0.57.1
40+
x-stainless-read-timeout:
41+
- '600'
42+
x-stainless-retry-count:
43+
- '0'
44+
x-stainless-runtime:
45+
- CPython
46+
x-stainless-runtime-version:
47+
- 3.13.5
48+
x-stainless-timeout:
49+
- '600'
50+
method: POST
51+
uri: https://api.anthropic.com/v1/messages
52+
response:
53+
body:
54+
string: !!binary |
55+
H4sIAAAAAAAAA2RQS0vEMBD+L3NOsVu7uuTWxfWwD2HZk4iEkAxtaJrUZCJq6X+XVPawePzme83M
56+
BEYDhyG2olyd908dni++7i7hq6XjuNs/9w0woO8RswpjlC0Cg+BtHsgYTSTpCBgMXqMFDsrKpLG4
57+
L9ZFJ02fiqqs6lVZVcBAeUfoCPjbdM0k761IMYcum2ScRLk6rA/daafb6qReHnXTb+vXujPAwMkh
58+
+1okQWZYfG5MBHyCjH+8y3QzYDBK3h19FI1r0WKEeX5nEMmPIqCM3t22L0TEj4ROIXCXrGWQlnv5
59+
9NchyPfoIvD1w4aBkqpDoQJKMt6JW0V55QNK/Z/ziW7yNgwihk+jUJDBABzyW7UMGub5FwAA//8D
60+
ALuiEKakAQAA
61+
headers:
62+
CF-RAY:
63+
- 9665173afb72342f-LHR
64+
Connection:
65+
- keep-alive
66+
Content-Encoding:
67+
- gzip
68+
Content-Type:
69+
- application/json
70+
Date:
71+
- Mon, 28 Jul 2025 14:33:18 GMT
72+
Server:
73+
- cloudflare
74+
Transfer-Encoding:
75+
- chunked
76+
X-Robots-Tag:
77+
- none
78+
anthropic-organization-id:
79+
- 04aa8588-6567-40cb-9042-a54b20ebaf4f
80+
anthropic-ratelimit-input-tokens-limit:
81+
- '400000'
82+
anthropic-ratelimit-input-tokens-remaining:
83+
- '400000'
84+
anthropic-ratelimit-input-tokens-reset:
85+
- '2025-07-28T14:33:17Z'
86+
anthropic-ratelimit-output-tokens-limit:
87+
- '80000'
88+
anthropic-ratelimit-output-tokens-remaining:
89+
- '80000'
90+
anthropic-ratelimit-output-tokens-reset:
91+
- '2025-07-28T14:33:18Z'
92+
anthropic-ratelimit-requests-limit:
93+
- '4000'
94+
anthropic-ratelimit-requests-remaining:
95+
- '3999'
96+
anthropic-ratelimit-requests-reset:
97+
- '2025-07-28T14:33:17Z'
98+
anthropic-ratelimit-tokens-limit:
99+
- '480000'
100+
anthropic-ratelimit-tokens-remaining:
101+
- '480000'
102+
anthropic-ratelimit-tokens-reset:
103+
- '2025-07-28T14:33:17Z'
104+
cf-cache-status:
105+
- DYNAMIC
106+
request-id:
107+
- req_011CRZaepSK9Jc89WTRSudjf
108+
strict-transport-security:
109+
- max-age=31536000; includeSubDomains; preload
110+
via:
111+
- 1.1 google
112+
status:
113+
code: 200
114+
message: OK
115+
version: 1

0 commit comments

Comments
 (0)