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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
interactions:
- request:
body:
'{"max_tokens":1024,"messages":[{"role":"user","content":"what is 1+1?,
just return the number"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "145"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- AsyncAnthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- async:asyncio
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01Hwq7nXVJreMkdBttJ9H46B","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":18,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":2,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"2"}
}


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c89e6a908c63-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:49 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1499000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:49Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:48Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9499000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:48Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5r8E8kny9TZdxz4Q6v
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
interactions:
- request:
body:
'{"max_tokens":300,"messages":[{"role":"user","content":"what is 2+2? (just
the number)"}],"model":"claude-3-haiku-20240307","stream":true}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
anthropic-version:
- "2023-06-01"
connection:
- keep-alive
content-length:
- "138"
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.52.1
x-stainless-arch:
- arm64
x-stainless-async:
- "false"
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.52.1
x-stainless-read-timeout:
- "600"
x-stainless-retry-count:
- "0"
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-stream-helper:
- messages
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: 'event: message_start

data: {"type":"message_start","message":{"id":"msg_01NR8LiXqHhf3PCEc3k3m3hJ","type":"message","role":"assistant","model":"claude-3-haiku-20240307","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":19,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":4,"service_tier":"standard"}} }


event: content_block_start

data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }


event: ping

data: {"type": "ping"}


event: content_block_delta

data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"4"} }


event: content_block_stop

data: {"type":"content_block_stop","index":0 }


event: message_delta

data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":5} }


event: message_stop

data: {"type":"message_stop" }


'
headers:
CF-RAY:
- 9472c8a8e81323dd-EWR
Cache-Control:
- no-cache
Connection:
- keep-alive
Content-Type:
- text/event-stream; charset=utf-8
Date:
- Thu, 29 May 2025 03:07:50 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 27796668-7351-40ac-acc4-024aee8995a5
anthropic-ratelimit-input-tokens-limit:
- "8000000"
anthropic-ratelimit-input-tokens-remaining:
- "8000000"
anthropic-ratelimit-input-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-output-tokens-limit:
- "1500000"
anthropic-ratelimit-output-tokens-remaining:
- "1500000"
anthropic-ratelimit-output-tokens-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-requests-limit:
- "10000"
anthropic-ratelimit-requests-remaining:
- "9999"
anthropic-ratelimit-requests-reset:
- "2025-05-29T03:07:50Z"
anthropic-ratelimit-tokens-limit:
- "9500000"
anthropic-ratelimit-tokens-remaining:
- "9500000"
anthropic-ratelimit-tokens-reset:
- "2025-05-29T03:07:50Z"
cf-cache-status:
- DYNAMIC
request-id:
- req_011CPb5rFK3186WDZPzKJEuC
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- 1.1 google
status:
code: 200
message: OK
version: 1
81 changes: 81 additions & 0 deletions py/src/braintrust/integrations/anthropic/test_anthropic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -582,6 +582,87 @@ def test_anthropic_messages_streaming_sync(memory_logger):
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_streaming_sync_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_client())
msg_in = {"role": "user", "content": "what is 2+2? (just the number)"}

start = time.time()
with client.messages.stream(model=MODEL, max_tokens=300, messages=[msg_in]) as stream:
texts = list(stream.text_stream)
end = time.time()
msg_out = stream.get_final_message()
usage = msg_out.usage

text = "".join(texts)
assert "4" in text
assert "4" in msg_out.content[0].text

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "2+2" in str(log["input"])
assert "4" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 300
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_async_text_stream(memory_logger):
"""time_to_first_token is captured when iterating via .text_stream on async streams (BT-4702)."""
assert not memory_logger.pop()

client = wrap_anthropic(_get_async_client())
msgs_in = [{"role": "user", "content": "what is 1+1?, just return the number"}]

start = time.time()
async with client.messages.stream(max_tokens=1024, messages=msgs_in, model=MODEL) as stream:
texts = [t async for t in stream.text_stream]
msg_out = await stream.get_final_message()
usage = msg_out.usage
end = time.time()

text = "".join(texts)
assert "2" in text
assert msg_out.content[0].text == "2"

logs = memory_logger.pop()
assert len(logs) == 1
log = logs[0]
assert "user" in str(log["input"])
assert "1+1" in str(log["input"])
assert "2" in str(log["output"])
assert log["project_id"] == PROJECT_NAME
assert log["span_attributes"]["type"] == "llm"
assert log["metadata"]["model"] == MODEL
assert log["metadata"]["max_tokens"] == 1024
assert log["output"]["role"] == "assistant"
assert log["output"]["model"] == msg_out.model
assert log["output"]["stop_reason"] == msg_out.stop_reason
_assert_metrics_are_valid(log["metrics"], start, end)
assert log["metrics"]["prompt_tokens"] == usage.input_tokens
assert log["metrics"]["completion_tokens"] == usage.output_tokens
assert log["metrics"]["tokens"] == usage.input_tokens + usage.output_tokens
assert log["metrics"]["prompt_cached_tokens"] == usage.cache_read_input_tokens
assert log["metrics"]["prompt_cache_creation_tokens"] == usage.cache_creation_input_tokens


@pytest.mark.vcr
def test_anthropic_messages_sync(memory_logger):
assert not memory_logger.pop()
Expand Down
20 changes: 20 additions & 0 deletions py/src/braintrust/integrations/anthropic/tracing.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,26 @@ def __next__(self):
self.__process_message(m)
return m

@property
def text_stream(self):
if hasattr(self.__msg_stream, "__aiter__"):
return self.__async_text_stream()
return self.__sync_text_stream()

def __sync_text_stream(self):
for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

async def __async_text_stream(self):
async for event in self:
if getattr(event, "type", None) == "content_block_delta":
delta = getattr(event, "delta", None)
if getattr(delta, "type", None) == "text_delta":
yield getattr(delta, "text", "")

def __process_message(self, m):
if self.__time_to_first_token is None:
self.__time_to_first_token = time.time() - self.__request_start_time
Expand Down
Loading