Repository navigation
Python: fix(anthropic): report token usage as CompletionUsage - #14618
Conversation
AnthropicChatCompletion stored the raw anthropic Usage (non-streaming) or a plain dict (streaming) in metadata["usage"], so the model diagnostics never recorded gen_ai.usage.input_tokens / output_tokens for Anthropic calls. Build a CompletionUsage like the other connectors, counting prompt-cache reads and writes in prompt_tokens and cache reads in prompt_tokens_details.cached_tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The focused conversion and streaming changes match the diagnostics contract, with regression coverage and no identified blocking issues.
No open issues 🎉
What changed in this PR
Normalizes Anthropic token usage to CompletionUsage so model diagnostics can report input and output token counts.
Changes:
- Includes cache-read and cache-creation tokens in prompt totals.
- Preserves prompt usage when merging streaming output counts.
- Adds regression tests for response metadata and telemetry.
| File | Description |
|---|---|
| python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py | Tests streaming and non-streaming usage reporting. |
| python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py | Converts Anthropic usage and preserves streaming prompt counts. |
Review effort: Balanced
PRABHU KIRAN VANDRANKI (VANDRANKI)
left a comment
There was a problem hiding this comment.
I ran this end to end and the telemetry fix works. Setup: Python 3.13.3, anthropic SDK 1.15.0, opentelemetry-sdk with an in-memory exporter, SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS=true. The PR diff applied cleanly onto current main (fetched 2026-10-16) and I compared it with a clean main checkout.
Probe: a local HTTP server that speaks the Anthropic Messages API. The response (and the SSE message_start) carries input_tokens=10, cache_read_input_tokens=100, cache_creation_input_tokens=5. The non-streaming output_tokens is 2, and the streaming message_delta carries output_tokens=7. A real AsyncAnthropic(base_url=local) is passed to AnthropicChatCompletion.
| main | PR | |
|---|---|---|
non-stream metadata["usage"] |
raw Anthropic Usage(...) |
CompletionUsage(prompt_tokens=115, cached_tokens=100, completion_tokens=2) |
stream metadata["usage"] |
dicts {'input_tokens': 10} then {'input_tokens': 10, 'output_tokens': 7} |
CompletionUsage with prompt 115, and completion 7 on the last chunk |
SK chat claude-sonnet-4-5 span attributes (non-stream) |
none | gen_ai.usage.input_tokens=115, gen_ai.usage.output_tokens=2 |
SK chat claude-sonnet-4-5 span attributes (stream) |
none | input 115, output 7 |
So the missing token attributes are fixed, and the cached tokens are counted in the prompt side.
Edge cases on the streaming path:
message_deltawithoutput_tokensonly (what I used above): prompt 115 is kept frommessage_start. Good.message_deltawithinput_tokens=10and the two cache fields: prompt 115. Good.message_deltawithinput_tokens=10but no cache fields (I made this up to test the code path): the final usage becomesprompt_tokens=10,prompt_tokens_details=None, so the 105 cache tokens frommessage_startare dropped. The code says "use the delta counts when it reportsinput_tokens". I do not know whether the real API ever sendsinput_tokenswithout the cache fields in a delta, so this may never happen. If you want to be safe, fall back to themessage_startcache values when the delta leaves them out.
Tests: tests/unit/connectors/ai/anthropic gives 37 passed on the PR. When I copy the PR's test file onto main, the two new tests fail (test_usage_is_reported_as_completion_usage, test_streaming_usage_is_reported_as_completion_usage) and 35 pass.
Two notes. The PR text already says this is a behavior change for code that read .input_tokens from the old usage object. I did not search outside this repo for such code. Also, PromptTokensDetails has a cache_write_tokens field and the PR leaves it None even though cache_creation_input_tokens is known. That is optional.
I ran only against a local server, not the real Anthropic API. I did not check how this combines with #14616, which edits nearby lines in the same file.
|
@microsoft-github-policy-service agree |
Motivation and Context
The model diagnostics record token usage only when
metadata["usage"]is aCompletionUsage(utils/telemetry/model_diagnostics/decorators.py). The other chat connectors build one, butAnthropicChatCompletionstores the rawanthropic.types.Usage(non-streaming) or a plain dict like{"input_tokens": ..., "output_tokens": ...}(streaming). As a result, spans for Anthropic calls never getgen_ai.usage.input_tokens/gen_ai.usage.output_tokens.Prompt caching also isn't counted: Anthropic reports
cache_read_input_tokensandcache_creation_input_tokensseparately frominput_tokens, and the streaming path drops them.Description
_get_completion_usagehelper turns Anthropic usage into aCompletionUsage:prompt_tokens=input_tokens+cache_read_input_tokens+cache_creation_input_tokens;prompt_tokens_details.cached_tokens=cache_read_input_tokens(only when present);completion_tokens=output_tokens.metadata["usage"]is now thatCompletionUsage.message_startsets the prompt side. The finalmessage_deltachunk carries the full usage: its own counts when it reportsinput_tokens, otherwise themessage_startprompt counts plus the delta'soutput_tokens. Since the chunk metadata is merged with|, this is the usage of the combined message.getattrbecause olderanthropicreleases (the requirement is~= 0.32) lack some of them.Behavior change:
metadata["usage"]for Anthropic responses is now aCompletionUsage, the same type as the other connectors. Code that read.input_tokens/.output_tokensfrom it should use.prompt_tokens/.completion_tokens. The raw Anthropic usage is still available oninner_content.Contribution Checklist
Tests, in
tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py, with model diagnostics enabled, 10 input + 100 cache-read + 20 cache-write + 7 output tokens:test_usage_is_reported_as_completion_usage(non-streaming):metadata["usage"]is aCompletionUsage(130 prompt, 100 cached, 7 completion), and the span getsgen_ai.usage.input_tokens=130,gen_ai.usage.output_tokens=7;test_streaming_usage_is_reported_as_completion_usage: the same checks on the combined streamed message.Both fail on
main.tests/unit/connectors/ai/anthropic+tests/unit/utils/model_diagnostics: 65 passed (test_connector_decorated.pywas not run here because it imports the Mistral connector).ruff0.15.5 check/format is clean.mypyon the changed module reports the same 3 pre-existing errors asmain, all in other modules.This change was drafted with Claude Code; the tests above were run before opening the PR.
🤖 Generated with Claude Code