feat: add anthropic cached token values for local backends that support it
All checks were successful
PR Tests / test (pull_request) Successful in 1m19s
NYX Security Scan / nyx-scan (pull_request) Successful in 5m37s

This commit is contained in:
Alpha Nerd 2026-07-06 11:05:59 +02:00
parent 1d012a92ef
commit 04bc69993d
Signed by: alpha-nerd
SSH key fingerprint: SHA256:QkkAgVoYi9TQ0UKPkiKSfnerZy2h4qhi3SVPXJmBN+M
3 changed files with 93 additions and 16 deletions

View file

@ -119,10 +119,17 @@ class TestResponseTranslation:
assert at.finish_reason_to_stop_reason("tool_calls") == "tool_use"
assert at.finish_reason_to_stop_reason("stop", has_tool_use=True) == "tool_use"
def test_usage_mapping(self):
u = at.usage_chat_to_anthropic({"prompt_tokens": 7, "completion_tokens": 3},
cache_read_tokens=5)
def test_usage_mapping_no_cache(self):
u = at.usage_chat_to_anthropic({"prompt_tokens": 7, "completion_tokens": 3})
assert u == {"input_tokens": 7, "output_tokens": 3,
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 0}
def test_usage_mapping_cached_tokens_subtracted(self):
# Backend reports 5 of 7 prompt tokens served from its prefix cache.
u = at.usage_chat_to_anthropic({
"prompt_tokens": 7, "completion_tokens": 3,
"prompt_tokens_details": {"cached_tokens": 5}})
assert u == {"input_tokens": 2, "output_tokens": 3,
"cache_creation_input_tokens": 0, "cache_read_input_tokens": 5}
@ -137,8 +144,11 @@ def _chunk(content=None, tool_calls=None, reasoning=None, finish_reason=None):
return NS(choices=[NS(delta=delta, finish_reason=finish_reason)], usage=None)
def _usage_chunk(p, c):
return NS(choices=[], usage=NS(prompt_tokens=p, completion_tokens=c))
def _usage_chunk(p, c, cached=None):
usage = NS(prompt_tokens=p, completion_tokens=c)
if cached is not None:
usage.prompt_tokens_details = NS(cached_tokens=cached)
return NS(choices=[], usage=usage)
async def _collect(translator, gen):
@ -183,6 +193,17 @@ class TestStreamTranslator:
assert md["usage"]["output_tokens"] == 5
assert tr.content_blocks == [{"type": "text", "text": "Hello"}]
async def test_cached_tokens_surface_as_cache_read(self):
async def gen():
yield _chunk(content="hi", finish_reason="stop")
yield _usage_chunk(10, 2, cached=6)
tr = at.ChatToMessagesStream("msg_1", "m")
events = await _collect(tr, gen())
md = [d for t, d in events if t == "message_delta"][0]
assert md["usage"]["cache_read_input_tokens"] == 6
assert md["usage"]["input_tokens"] == 4 # 10 prompt 6 cached
assert md["usage"]["cache_creation_input_tokens"] == 0
async def test_thinking_then_text(self):
async def gen():
yield _chunk(reasoning="think ")
@ -253,7 +274,7 @@ def _enter(*cms):
def _fake_completion(content="hello", usage=(3, 5), reasoning=None, tool_calls=None,
finish_reason="stop"):
finish_reason="stop", cached=None):
md = {"role": "assistant", "content": content}
if reasoning is not None:
md["reasoning_content"] = reasoning
@ -261,9 +282,12 @@ def _fake_completion(content="hello", usage=(3, 5), reasoning=None, tool_calls=N
md["tool_calls"] = tool_calls
msg = MagicMock()
msg.model_dump.return_value = md
usage_dump = {"prompt_tokens": usage[0], "completion_tokens": usage[1],
"total_tokens": sum(usage)}
if cached is not None:
usage_dump["prompt_tokens_details"] = {"cached_tokens": cached}
usage_obj = MagicMock()
usage_obj.model_dump.return_value = {
"prompt_tokens": usage[0], "completion_tokens": usage[1], "total_tokens": sum(usage)}
usage_obj.model_dump.return_value = usage_dump
return NS(choices=[NS(message=msg, finish_reason=finish_reason)], usage=usage_obj)
@ -295,6 +319,19 @@ class TestTranslatedRoute:
assert body["usage"]["input_tokens"] == 3 and body["usage"]["output_tokens"] == 5
assert body["id"].startswith("msg_")
async def test_nonstream_cached_tokens(self, client):
with _enter(*_patch_backend(native=False),
patch.object(api_messages, "create_chat_with_retries",
AsyncMock(return_value=_fake_completion(
"hi", usage=(10, 4), cached=6)))):
resp = await client.post("/v1/messages",
json={"model": "test-model", "max_tokens": 100,
"messages": [{"role": "user", "content": "hi"}]})
u = resp.json()["usage"]
assert u["cache_read_input_tokens"] == 6
assert u["input_tokens"] == 4
assert u["cache_creation_input_tokens"] == 0
async def test_missing_max_tokens_400(self, client):
with _enter(*_patch_backend(native=False)):
resp = await client.post("/v1/messages",