Surfaced while reviewing the weekly dependency bump (#783), which moved anthropic 1.5.0 → 1.7.0. Everything below is verified against the installed 1.7.0 unless marked otherwise. Nothing here is a regression from the bump — these are gaps the bump made visible, plus a few pre-existing ones in the same code paths.
All six are small, independent changes in py/src/braintrust/integrations/anthropic/.
Cost & accounting correctness
These change numbers Braintrust reports, so they're the ones that actually matter.
1. usage.speed is dropped — fast-mode calls are mis-costed
_ANTHROPIC_USAGE_METADATA_FIELDS (_utils.py:31) is {"service_tier", "inference_geo"}. speed is present on BetaUsage but not in the set, so it never reaches span metadata.
Fast mode on Opus 5 bills $10/$50 per MTok vs $5/$25 standard. A fast-mode span currently looks like it cost half what it did.
Fix: add "speed" to the frozenset.
2. usage.iterations (compaction per-iteration tokens) is never read
BetaUsage.iterations is a list of BetaCompactionIterationUsage, each carrying its own input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens. extract_anthropic_usage (_utils.py:56) never touches it.
⚠️ Needs verification first. I could not determine whether the top-level usage totals already roll these up — the type stubs carry no field descriptions. One recorded compaction call settles it. If they don't roll up, we under-report cost on exactly the long-running workloads people enable compaction for.
This one should be its own PR, gated on that check.
3. usage.fallback_credit is never read
Also on BetaUsage, also unhandled. Relevant to refusal fallbacks: when a fallback model rescues a declined request, billing and cache pricing shift. Same shape of fix as #1.
Span metadata completeness
4. compaction request param not captured
METADATA_PARAMS (tracing.py:62) is an allowlist of request params tracked as span metadata. compaction — added in anthropic 1.6.0 — isn't in it, so a span gives no indication compaction was configured for the call.
5. context_management request param not captured
Same allowlist. This is the context-editing config (clear_tool_uses_20250919, clear_thinking_20251015). It changes what the model actually saw, which makes it meaningful trace context.
6. stop_details not captured
_message_output (tracing.py:1505) records stop_reason and stop_sequence but not the adjacent stop_details, which the API populates when stop_reason == "refusal" with a category ("cyber", "bio", …) and explanation.
That's the difference between a trace showing that a call was refused and one showing why. Guard the read — stop_details is null for every non-refusal stop reason.
Not in scope here
Output-side handling is already correct and needs no change: _message_output passes message.content through wholesale rather than filtering by block type, so BetaCompactionBlock already lands in span output.
Two further items were considered and deliberately left out, as product/scoping calls rather than defects:
tool_runner loop structure. Each turn is already traced (the runner calls beta.messages.parse, and we rewrite client.beta in place at tracing.py:1599). What's missing is a parent span grouping the agentic run and child spans for client-side tool executions — our tool spans cover Anthropic's server tools only.
anthropic-beta header formatting change in 1.7.0 (values now comma-joined with no spaces). No action needed; playback is green.
Testing
Per CLAUDE.md, each of these should get cassette-backed coverage in test_anthropic.py rather than a mock, since they depend on the real response shape. Session:
cd py && nox -s "test_anthropic(latest)"
Suggested split: #1, #3, #4, #5, #6 are all small allowlist additions and could land as one PR. #2 needs the rollup question answered first.
Surfaced while reviewing the weekly dependency bump (#783), which moved
anthropic1.5.0 → 1.7.0. Everything below is verified against the installed 1.7.0 unless marked otherwise. Nothing here is a regression from the bump — these are gaps the bump made visible, plus a few pre-existing ones in the same code paths.All six are small, independent changes in
py/src/braintrust/integrations/anthropic/.Cost & accounting correctness
These change numbers Braintrust reports, so they're the ones that actually matter.
1.
usage.speedis dropped — fast-mode calls are mis-costed_ANTHROPIC_USAGE_METADATA_FIELDS(_utils.py:31) is{"service_tier", "inference_geo"}.speedis present onBetaUsagebut not in the set, so it never reaches span metadata.Fast mode on Opus 5 bills $10/$50 per MTok vs $5/$25 standard. A fast-mode span currently looks like it cost half what it did.
Fix: add
"speed"to the frozenset.2.
usage.iterations(compaction per-iteration tokens) is never readBetaUsage.iterationsis a list ofBetaCompactionIterationUsage, each carrying its owninput_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens.extract_anthropic_usage(_utils.py:56) never touches it.usagetotals already roll these up — the type stubs carry no field descriptions. One recorded compaction call settles it. If they don't roll up, we under-report cost on exactly the long-running workloads people enable compaction for.This one should be its own PR, gated on that check.
3.
usage.fallback_creditis never readAlso on
BetaUsage, also unhandled. Relevant to refusal fallbacks: when a fallback model rescues a declined request, billing and cache pricing shift. Same shape of fix as #1.Span metadata completeness
4.
compactionrequest param not capturedMETADATA_PARAMS(tracing.py:62) is an allowlist of request params tracked as span metadata.compaction— added in anthropic 1.6.0 — isn't in it, so a span gives no indication compaction was configured for the call.5.
context_managementrequest param not capturedSame allowlist. This is the context-editing config (
clear_tool_uses_20250919,clear_thinking_20251015). It changes what the model actually saw, which makes it meaningful trace context.6.
stop_detailsnot captured_message_output(tracing.py:1505) recordsstop_reasonandstop_sequencebut not the adjacentstop_details, which the API populates whenstop_reason == "refusal"with acategory("cyber","bio", …) andexplanation.That's the difference between a trace showing that a call was refused and one showing why. Guard the read —
stop_detailsisnullfor every non-refusal stop reason.Not in scope here
Output-side handling is already correct and needs no change:
_message_outputpassesmessage.contentthrough wholesale rather than filtering by block type, soBetaCompactionBlockalready lands in span output.Two further items were considered and deliberately left out, as product/scoping calls rather than defects:
tool_runnerloop structure. Each turn is already traced (the runner callsbeta.messages.parse, and we rewriteclient.betain place attracing.py:1599). What's missing is a parent span grouping the agentic run and child spans for client-side tool executions — our tool spans cover Anthropic's server tools only.anthropic-betaheader formatting change in 1.7.0 (values now comma-joined with no spaces). No action needed; playback is green.Testing
Per
CLAUDE.md, each of these should get cassette-backed coverage intest_anthropic.pyrather than a mock, since they depend on the real response shape. Session:Suggested split: #1, #3, #4, #5, #6 are all small allowlist additions and could land as one PR. #2 needs the rollup question answered first.