Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import asdict
from enum import Enum
import json
from typing import Optional, Union

from opentelemetry._events import Event, EventLogger
Expand Down Expand Up @@ -127,8 +128,29 @@ def emit_streaming_response_events(
event_logger: Optional[EventLogger], complete_response: dict
):
for message in complete_response.get("events", []):
emit_event(
ChoiceEvent(
# Parse tool calls
if message.get("type") == "tool_use":
tool_calls = [
ToolCall(
id=message.get("id"),
function={
"name": message.get("name"),
"arguments": json.loads(message.get("input", '{}')),
},
type="function",
)
]
event = ChoiceEvent(
index=message.get("index", 0),
message={
"content": None,
"role": message.get("role", "assistant"),
},
finish_reason=message.get("finish_reason", "unknown"),
tool_calls=tool_calls,
)
else:
event = ChoiceEvent(
index=message.get("index", 0),
message={
"content": {
Expand All @@ -138,9 +160,8 @@ def emit_streaming_response_events(
"role": message.get("role", "assistant"),
},
finish_reason=message.get("finish_reason", "unknown"),
),
event_logger,
)
)
emit_event(event, event_logger)


def emit_event(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,43 @@ async def aset_input_attributes(span, kwargs):
)
for i, message in enumerate(kwargs.get("messages")):
prompt_index = i + (1 if has_system_message else 0)
content = message.get("content")
tool_use_blocks = []
other_blocks = []
if isinstance(content, list):
for block in content:
if dict(block).get("type") == "tool_use":
tool_use_blocks.append(dict(block))
else:
other_blocks.append(block)
content = other_blocks
set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
await _dump_content(
message_index=i, span=span, content=message.get("content")
),
await _dump_content(message_index=i, span=span, content=content),
)
set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
message.get("role"),
)
if tool_use_blocks:
for tool_num, tool_use_block in enumerate(tool_use_blocks):
set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.id",
tool_use_block.get("id"),
)
set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.name",
tool_use_block.get("name"),
)
set_span_attribute(
span,
f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.tool_calls.{tool_num}.arguments",
json.dumps(tool_use_block.get("input")),
)

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

try:
for event in complete_response_events:
index = event.get("index")
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
index = 0
for event in complete_response_events:
prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
set_span_attribute(span, f"{prefix}.finish_reason", event.get("finish_reason"))
role = "thinking" if event.get("type") == "thinking" else "assistant"
# Thinking is added as a separate completion, so we need to increment the index
if event.get("type") == "thinking":
index += 1
set_span_attribute(span, f"{prefix}.role", role)
if event.get("type") == "tool_use":
set_span_attribute(
span,
f"{prefix}.tool_calls.0.id",
event.get("id"),
)
set_span_attribute(
span, f"{prefix}.finish_reason", event.get("finish_reason")
span,
f"{prefix}.tool_calls.0.name",
event.get("name"),
)
role = "thinking" if event.get("type") == "thinking" else "assistant"
set_span_attribute(span, f"{prefix}.role", role)
tool_arguments = event.get("input")
if tool_arguments is not None:
set_span_attribute(
span,
f"{prefix}.tool_calls.0.arguments",
tool_arguments,
)
else:
set_span_attribute(span, f"{prefix}.content", event.get("text"))
except Exception as e:
logger.warning("Failed to set completion attributes, error: %s", str(e))
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,18 @@ def _process_response_item(item, complete_response):
complete_response["events"].append(
{"index": index, "text": "", "type": item.content_block.type}
)
elif item.type == "content_block_delta" and item.delta.type in [
"thinking_delta",
"text_delta",
]:
if item.content_block.type == "tool_use":
complete_response["events"][index]["id"] = item.content_block.id
complete_response["events"][index]["name"] = item.content_block.name
complete_response["events"][index]["input"] = """"""
elif item.type == "content_block_delta":
index = item.index
if item.delta.type == "thinking_delta":
complete_response["events"][index]["text"] += item.delta.thinking
elif item.delta.type == "text_delta":
complete_response["events"][index]["text"] += item.delta.text
elif item.delta.type == "input_json_delta":
complete_response["events"][index]["input"] += item.delta.partial_json
elif item.type == "message_delta":
for event in complete_response.get("events", []):
event["finish_reason"] = item.delta.stop_reason
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
interactions:
- request:
body: '{"max_tokens":1024,"messages":[{"role":"user","content":"What is the weather
and current time in San Francisco?"},{"role":"assistant","content":[{"type":"text","text":"I''ll
help you get the weather and current time in San Francisco."},{"id":"call_1","type":"tool_use","name":"get_weather","input":{"location":"San
Francisco, CA"}}]},{"role":"user","content":[{"type":"tool_result","content":"Sunny
and 65 degrees Fahrenheit","tool_use_id":"call_1"}]}],"model":"claude-3-5-haiku-20241022","tools":[{"name":"get_weather","description":"Get
the current weather in a given location","input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The
city and state, e.g. San Francisco, CA"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"The
unit of temperature, either ''celsius'' or ''fahrenheit''"}},"required":["location"]}},{"name":"get_time","description":"Get
the current time in a given time zone","input_schema":{"type":"object","properties":{"timezone":{"type":"string","description":"The
IANA time zone name, e.g. America/Los_Angeles"}},"required":["timezone"]}}]}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '1117'
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.57.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.57.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.5
x-stainless-timeout:
- '600'
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: !!binary |
H4sIAAAAAAAAA2RQS0vEMBD+L3NOsVu7uuTWxfWwD2HZk4iEkAxtaJrUZCJq6X+XVPawePzme83M
BEYDhyG2olyd908dni++7i7hq6XjuNs/9w0woO8RswpjlC0Cg+BtHsgYTSTpCBgMXqMFDsrKpLG4
L9ZFJ02fiqqs6lVZVcBAeUfoCPjbdM0k761IMYcum2ScRLk6rA/daafb6qReHnXTb+vXujPAwMkh
+1okQWZYfG5MBHyCjH+8y3QzYDBK3h19FI1r0WKEeX5nEMmPIqCM3t22L0TEj4ROIXCXrGWQlnv5
9NchyPfoIvD1w4aBkqpDoQJKMt6JW0V55QNK/Z/ziW7yNgwihk+jUJDBABzyW7UMGub5FwAA//8D
ALuiEKakAQAA
headers:
CF-RAY:
- 9665173afb72342f-LHR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Mon, 28 Jul 2025 14:33:18 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 04aa8588-6567-40cb-9042-a54b20ebaf4f
anthropic-ratelimit-input-tokens-limit:
- '400000'
anthropic-ratelimit-input-tokens-remaining:
- '400000'
anthropic-ratelimit-input-tokens-reset:
- '2025-07-28T14:33:17Z'
anthropic-ratelimit-output-tokens-limit:
- '80000'
anthropic-ratelimit-output-tokens-remaining:
- '80000'
anthropic-ratelimit-output-tokens-reset:
- '2025-07-28T14:33:18Z'
anthropic-ratelimit-requests-limit:
- '4000'
anthropic-ratelimit-requests-remaining:
- '3999'
anthropic-ratelimit-requests-reset:
- '2025-07-28T14:33:17Z'
anthropic-ratelimit-tokens-limit:
- '480000'
anthropic-ratelimit-tokens-remaining:
- '480000'
anthropic-ratelimit-tokens-reset:
- '2025-07-28T14:33:17Z'
cf-cache-status:
- DYNAMIC
request-id:
- req_011CRZaepSK9Jc89WTRSudjf
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Loading