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

@ -257,15 +257,46 @@ def finish_reason_to_stop_reason(finish_reason, has_tool_use=False):
return _STOP_REASON_MAP.get(finish_reason, "end_turn")
def _cached_prompt_tokens(usage):
"""Read ``prompt_tokens_details.cached_tokens`` from a chat usage (dict or SDK obj).
OpenAI-compatible backends with automatic prefix caching (vLLM, recent
llama-server) report the reused-prefix token count here; backends that don't
populate it yield 0.
"""
if not usage:
return 0
details = usage.get("prompt_tokens_details") if isinstance(usage, dict) \
else getattr(usage, "prompt_tokens_details", None)
if details is None:
return 0
if isinstance(details, dict):
return details.get("cached_tokens") or 0
return getattr(details, "cached_tokens", 0) or 0
def usage_chat_to_anthropic(usage, cache_read_tokens=0, cache_creation_tokens=0):
"""Map chat usage → Anthropic usage, folding in nomyo-cache attribution."""
prompt = (usage or {}).get("prompt_tokens") or 0
completion = (usage or {}).get("completion_tokens") or 0
"""Map chat usage → Anthropic usage.
``cached_tokens`` reported by the backend (automatic prefix-cache reuse) plus any
explicit ``cache_read_tokens`` become ``cache_read_input_tokens``; ``input_tokens``
is the remaining uncached prompt so ``input + cache_read`` equals the prompt total.
``cache_creation_input_tokens`` stays at ``cache_creation_tokens`` (0 by default):
local backends do automatic KV-prefix reuse with no explicit ``cache_control``
write/breakpoint concept, so there is no honest "tokens written" figure to report
that value is only real on the native Anthropic passthrough path.
"""
prompt = (usage or {}).get("prompt_tokens") or 0 if isinstance(usage, dict) \
else (getattr(usage, "prompt_tokens", 0) or 0)
completion = (usage or {}).get("completion_tokens") or 0 if isinstance(usage, dict) \
else (getattr(usage, "completion_tokens", 0) or 0)
cache_read = _cached_prompt_tokens(usage) + cache_read_tokens
return {
"input_tokens": prompt,
"input_tokens": max(prompt - cache_read, 0),
"output_tokens": completion,
"cache_creation_input_tokens": cache_creation_tokens,
"cache_read_input_tokens": cache_read_tokens,
"cache_read_input_tokens": cache_read,
}
@ -430,6 +461,7 @@ class ChatToMessagesStream:
self.usage = {
"prompt_tokens": getattr(usage, "prompt_tokens", 0) or 0,
"completion_tokens": getattr(usage, "completion_tokens", 0) or 0,
"prompt_tokens_details": {"cached_tokens": _cached_prompt_tokens(usage)},
}
choices = getattr(chunk, "choices", None)
if not choices:
@ -512,8 +544,15 @@ class ChatToMessagesStream:
"type": "tool_use", "id": state["id"], "name": state["name"], "input": parsed})
self.stop_reason = finish_reason_to_stop_reason(finish_reason, has_tool_use=bool(tc_state))
out_tokens = (self.usage or {}).get("completion_tokens", 0)
final_usage = usage_chat_to_anthropic(
self.usage, cache_read_tokens=self.cache_read_tokens,
cache_creation_tokens=self.cache_creation_tokens)
yield _sse("message_delta", {
"delta": {"stop_reason": self.stop_reason, "stop_sequence": None},
"usage": {"output_tokens": out_tokens}})
"usage": {
"input_tokens": final_usage["input_tokens"],
"output_tokens": final_usage["output_tokens"],
"cache_read_input_tokens": final_usage["cache_read_input_tokens"],
"cache_creation_input_tokens": final_usage["cache_creation_input_tokens"],
}})
yield _sse("message_stop", {})