fix(bedrock): avoid double-counting cache tokens in Anthropic Messages streaming usage
Some checks failed
Unit Tests: Caching (Redis) / caching-redis (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
Sameer Kankute 2026-04-11 00:03:45 +05:30
parent d3baef79b2
commit d781ef42e8
No known key found for this signature in database
2 changed files with 115 additions and 10 deletions

View file

@ -622,9 +622,9 @@ class AmazonAnthropicClaudeMessagesConfig(
merges usage from message_start and message_delta but ignores
message_stop. This method buffers message_delta and, when
message_stop arrives with cache usage, merges those fields into the
message_delta usage and also updates the input_tokens on
message_delta to include the full count (uncached + cache_creation +
cache_read).
message_delta usage. input_tokens is kept as the uncached-only
count; downstream calculate_usage adds cache tokens to
prompt_tokens.
"""
_CACHE_FIELDS = ("cache_creation_input_tokens", "cache_read_input_tokens")
pending_delta = None
@ -653,12 +653,7 @@ class AmazonAnthropicClaudeMessagesConfig(
raw_input = stop_usage.get("input_tokens")
if raw_input is not None:
uncached = raw_input if isinstance(raw_input, int) else 0
raw_cc = delta_usage.get("cache_creation_input_tokens", 0)
cache_creation = raw_cc if isinstance(raw_cc, int) else 0
raw_cr = delta_usage.get("cache_read_input_tokens", 0)
cache_read = raw_cr if isinstance(raw_cr, int) else 0
delta_usage["input_tokens"] = uncached + cache_creation + cache_read
delta_usage["input_tokens"] = raw_input if isinstance(raw_input, int) else 0
if delta_usage:
pending_delta["usage"] = delta_usage # type: ignore[arg-type]

View file

@ -3,6 +3,7 @@ import json
import os
import sys
from datetime import datetime
from unittest.mock import Mock
import pytest
@ -130,7 +131,8 @@ async def test_bedrock_sse_wrapper_keeps_usage_in_message_start_and_message_delt
assert "usage" in delta_json
assert delta_json["usage"]["cache_creation_input_tokens"] == 1562
assert delta_json["usage"]["cache_read_input_tokens"] == 32392
assert delta_json["usage"]["input_tokens"] == 3 + 1562 + 32392
assert delta_json["usage"]["input_tokens"] == 3
assert delta_json["usage"]["output_tokens"] == 8
def test_chunk_parser_usage_transformation():
@ -599,3 +601,111 @@ def test_bedrock_messages_strips_output_config_with_output_format():
assert "output_config" not in result
assert "output_format" not in result
@pytest.mark.asyncio
async def test_promote_message_stop_usage_preserves_message_delta_output_tokens():
"""
Bedrock unified /messages streaming can send full usage on message_delta and a
conflicting smaller usage on message_stop (e.g. output_tokens 9 vs 12).
_promote_message_stop_usage must not replace message_delta output_tokens.
"""
cfg = AmazonAnthropicClaudeMessagesConfig()
async def _stream(): # type: ignore[return-type]
yield {
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {
"input_tokens": 3,
"cache_creation_input_tokens": 10553,
"cache_read_input_tokens": 25490,
"output_tokens": 12,
},
}
yield {
"type": "message_stop",
"usage": {"input_tokens": 3, "output_tokens": 9},
}
merged: list[dict] = []
async for chunk in cfg._promote_message_stop_usage(_stream()):
if isinstance(chunk, dict):
merged.append(chunk)
assert len(merged) >= 1
delta_out = merged[0]
assert delta_out["type"] == "message_delta"
assert delta_out["usage"]["output_tokens"] == 12
assert delta_out["usage"]["cache_creation_input_tokens"] == 10553
assert delta_out["usage"]["cache_read_input_tokens"] == 25490
assert delta_out["usage"]["input_tokens"] == 3
@pytest.mark.asyncio
async def test_unified_bedrock_messages_sse_usage_and_cost_claude_sonnet_46():
"""
End-to-end for Bedrock Invoke Anthropic Messages (unified) streaming path:
dict chunks -> _promote_message_stop_usage -> bedrock_sse_wrapper SSE bytes ->
same logging reconstruction as Anthropic /messages. Ensures token counts and
completion_cost match model_prices for us.anthropic.claude-sonnet-4-6.
"""
from litellm import completion_cost
from litellm.proxy.pass_through_endpoints.llm_provider_handlers.anthropic_passthrough_logging_handler import (
AnthropicPassthroughLoggingHandler,
)
cfg = AmazonAnthropicClaudeMessagesConfig()
async def _stream(): # type: ignore[return-type]
yield {
"type": "message_delta",
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {
"input_tokens": 3,
"cache_creation_input_tokens": 10553,
"cache_read_input_tokens": 25490,
"output_tokens": 12,
},
}
yield {
"type": "message_stop",
"usage": {"input_tokens": 3, "output_tokens": 9},
}
logging_obj = LiteLLMLoggingObj(
model="bedrock/us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
call_type="chat",
start_time=datetime.now(),
litellm_call_id="test_unified_bedrock_messages_sse_cost",
function_id="test_unified_bedrock_messages_sse_cost",
)
collected: list[bytes] = []
async for sse in cfg.bedrock_sse_wrapper(
completion_stream=_stream(),
litellm_logging_obj=logging_obj,
request_body={"model": "us.anthropic.claude-sonnet-4-6"},
):
collected.append(sse)
built = AnthropicPassthroughLoggingHandler._build_complete_streaming_response(
all_chunks=collected,
model="us.anthropic.claude-sonnet-4-6",
litellm_logging_obj=Mock(),
)
assert built.usage is not None
assert built.usage.completion_tokens == 12
assert built.usage.prompt_tokens == 36046
assert built.usage.total_tokens == 36058
assert built.usage.cache_creation_input_tokens == 10553
assert built.usage.cache_read_input_tokens == 25490
cost = completion_cost(
completion_response=built,
model="bedrock/us.anthropic.claude-sonnet-4-6",
custom_llm_provider="bedrock",
)
assert cost == pytest.approx(0.052150725, rel=0, abs=1e-9)