mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-17 02:13:37 +00:00
feat: better cached token usage logging
This commit is contained in:
1 parent
96cad1f8e2
commit
6d0319fb63
12 files changed
+423
-7
No files matched your search
@@ -0,0 +1,35 @@
|
||||
"""0031 token_usage cache tokens — persist the prompt-cache breakdown.
|
||||
|
||||
Providers report how much of each prompt was served from the prompt cache
|
||||
(``cached_tokens``) and, on newer OpenAI-family models, how much was written
|
||||
to it (``cache_write_tokens``). The LLM clients already parsed both and the
|
||||
usage layer discarded them, so ``token_usage`` could not show a cache hit
|
||||
rate. These two nullable columns carry the breakdown; NULL means the
|
||||
provider reported nothing (distinct from 0). ``prompt_tokens`` keeps its
|
||||
meaning as the provider's total. Idempotent both ways.
|
||||
|
||||
Revision ID: 0031_token_usage_cache_tokens
|
||||
Revises: 0030_superseded_messages
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = "0031_token_usage_cache_tokens"
|
||||
down_revision: Union[str, None] = "0030_superseded_messages"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS cached_tokens integer;")
|
||||
op.execute(
|
||||
"ALTER TABLE token_usage ADD COLUMN IF NOT EXISTS cache_write_tokens integer;"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("ALTER TABLE token_usage DROP COLUMN IF EXISTS cache_write_tokens;")
|
||||
op.execute("ALTER TABLE token_usage DROP COLUMN IF EXISTS cached_tokens;")
|
||||
@@ -710,6 +710,7 @@ class BaseLLM(ABC):
|
||||
completion_tokens,
|
||||
latency_ms,
|
||||
cached_tokens=None,
|
||||
cache_write_tokens=None,
|
||||
error=None,
|
||||
):
|
||||
# Non-streaming counterpart to ``_emit_stream_finished_log``. Paired
|
||||
@@ -730,6 +731,8 @@ class BaseLLM(ABC):
|
||||
}
|
||||
if cached_tokens is not None:
|
||||
extra["cached_tokens"] = int(cached_tokens)
|
||||
if cache_write_tokens is not None:
|
||||
extra["cache_write_tokens"] = int(cache_write_tokens)
|
||||
if error is not None:
|
||||
extra["error_class"] = type(error).__name__
|
||||
logging.info("llm_gen_finished", extra=extra)
|
||||
@@ -757,6 +760,7 @@ class BaseLLM(ABC):
|
||||
completion_tokens,
|
||||
latency_ms,
|
||||
cached_tokens=None,
|
||||
cache_write_tokens=None,
|
||||
error=None,
|
||||
):
|
||||
# Paired with ``llm_stream_start`` so cost dashboards can sum tokens
|
||||
@@ -774,6 +778,8 @@ class BaseLLM(ABC):
|
||||
}
|
||||
if cached_tokens is not None:
|
||||
extra["cached_tokens"] = int(cached_tokens)
|
||||
if cache_write_tokens is not None:
|
||||
extra["cache_write_tokens"] = int(cache_write_tokens)
|
||||
if error is not None:
|
||||
extra["error_class"] = type(error).__name__
|
||||
logging.info("llm_stream_finished", extra=extra)
|
||||
|
||||
@@ -1307,17 +1307,36 @@ class OpenAILLM(BaseLLM):
|
||||
output_details = getattr(usage, "completion_tokens_details", None)
|
||||
try:
|
||||
cached = int(getattr(input_details, "cached_tokens", 0) or 0)
|
||||
written = int(getattr(input_details, "cache_write_tokens", 0) or 0)
|
||||
reasoning = int(getattr(output_details, "reasoning_tokens", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
cached = 0
|
||||
written = 0
|
||||
reasoning = 0
|
||||
if cached:
|
||||
result["prompt_tokens_details"] = {"cached_tokens": cached}
|
||||
details = self._prompt_cache_details(cached, written)
|
||||
if details:
|
||||
result["prompt_tokens_details"] = details
|
||||
if reasoning:
|
||||
result["completion_tokens_details"] = {"reasoning_tokens": reasoning}
|
||||
self._last_usage = result
|
||||
self._last_usage_claimed = False
|
||||
|
||||
@staticmethod
|
||||
def _prompt_cache_details(cached: int, written: int) -> dict:
|
||||
"""Build the ``prompt_tokens_details`` breakdown from the two cache bins.
|
||||
|
||||
Only non-zero bins are included so a provider that never reports
|
||||
caching produces no details (and no misleading zero downstream).
|
||||
``cache_write_tokens`` is reported by newer OpenAI-family deployments
|
||||
that charge for cache writes.
|
||||
"""
|
||||
details = {}
|
||||
if cached:
|
||||
details["cached_tokens"] = cached
|
||||
if written:
|
||||
details["cache_write_tokens"] = written
|
||||
return details
|
||||
|
||||
@staticmethod
|
||||
def _function_call_ids(response):
|
||||
"""call_ids of every ``function_call`` item in a Responses output.
|
||||
@@ -1357,9 +1376,11 @@ class OpenAILLM(BaseLLM):
|
||||
"total_tokens": int(getattr(usage, "total_tokens", 0) or prompt + completion),
|
||||
}
|
||||
cached = int(getattr(input_details, "cached_tokens", 0) or 0)
|
||||
written = int(getattr(input_details, "cache_write_tokens", 0) or 0)
|
||||
reasoning = int(getattr(output_details, "reasoning_tokens", 0) or 0)
|
||||
if cached:
|
||||
result["prompt_tokens_details"] = {"cached_tokens": cached}
|
||||
details = self._prompt_cache_details(cached, written)
|
||||
if details:
|
||||
result["prompt_tokens_details"] = details
|
||||
if reasoning:
|
||||
result["completion_tokens_details"] = {"reasoning_tokens": reasoning}
|
||||
self._last_usage = result
|
||||
|
||||
@@ -235,6 +235,12 @@ token_usage_table = Table(
|
||||
# Added in ``0015_token_usage_model_id``. Canonical model id (catalog
|
||||
# name for built-ins, UUID for BYOM); NULL on un-backfilled rows.
|
||||
Column("model_id", Text),
|
||||
# Added in ``0031_token_usage_cache_tokens``. Prompt-cache breakdowns of
|
||||
# ``prompt_tokens`` as reported by the provider (reads; and writes on
|
||||
# newer OpenAI-family models). NULL = not reported, distinct from 0 = no
|
||||
# cache activity, so hit-rate queries stay honest across providers.
|
||||
Column("cached_tokens", Integer),
|
||||
Column("cache_write_tokens", Integer),
|
||||
)
|
||||
|
||||
user_logs_table = Table(
|
||||
|
||||
@@ -35,6 +35,8 @@ class TokenUsageRepository:
|
||||
request_id: Optional[str] = None,
|
||||
model_id: Optional[str] = None,
|
||||
timestamp: Optional[datetime] = None,
|
||||
cached_tokens: Optional[int] = None,
|
||||
cache_write_tokens: Optional[int] = None,
|
||||
) -> None:
|
||||
# Attribution guard: the ``token_usage_attribution_chk`` CHECK
|
||||
# constraint requires at least one of ``user_id`` / ``api_key``
|
||||
@@ -60,12 +62,14 @@ class TokenUsageRepository:
|
||||
INSERT INTO token_usage (
|
||||
user_id, api_key, agent_id,
|
||||
prompt_tokens, generated_tokens,
|
||||
cached_tokens, cache_write_tokens,
|
||||
source, request_id, model_id, timestamp
|
||||
)
|
||||
VALUES (
|
||||
:user_id, :api_key,
|
||||
CAST(:agent_id AS uuid),
|
||||
:prompt_tokens, :generated_tokens,
|
||||
:cached_tokens, :cache_write_tokens,
|
||||
:source, :request_id, :model_id, COALESCE(:timestamp, now())
|
||||
)
|
||||
"""
|
||||
@@ -76,6 +80,8 @@ class TokenUsageRepository:
|
||||
"agent_id": agent_id_uuid,
|
||||
"prompt_tokens": prompt_tokens,
|
||||
"generated_tokens": generated_tokens,
|
||||
"cached_tokens": cached_tokens,
|
||||
"cache_write_tokens": cache_write_tokens,
|
||||
"source": source,
|
||||
"request_id": request_id,
|
||||
"model_id": model_id,
|
||||
|
||||
+28
-1
@@ -131,6 +131,11 @@ def _persist_call_usage(llm, call_usage):
|
||||
agent_id=str(agent_id) if agent_id else None,
|
||||
prompt_tokens=call_usage["prompt_tokens"],
|
||||
generated_tokens=call_usage["generated_tokens"],
|
||||
# Present only when the provider reported the breakdown;
|
||||
# persisted as NULL otherwise so "unknown" never reads as
|
||||
# "0% cache hits".
|
||||
cached_tokens=call_usage.get("cached_tokens"),
|
||||
cache_write_tokens=call_usage.get("cache_write_tokens"),
|
||||
source=(
|
||||
getattr(llm, "_token_usage_source", None) or "agent_stream"
|
||||
),
|
||||
@@ -151,6 +156,12 @@ def _prefer_provider_usage(llm: Any, call_usage: Dict[str, int]) -> Dict[str, in
|
||||
``*_tokens_details`` breakdowns (``cached_tokens``,
|
||||
``reasoning_tokens``) back out of these bins — that would break
|
||||
parity with what providers bill.
|
||||
|
||||
The prompt-cache sub-bins ARE carried alongside (``cached_tokens``,
|
||||
``cache_write_tokens``; Anthropic's ``cache_creation_tokens`` maps to
|
||||
the latter) so persistence and the finish events can chart them. They
|
||||
are added only when the provider reported them. The rest of
|
||||
``call_usage`` (e.g. ``model``) is preserved rather than replaced.
|
||||
"""
|
||||
reported = getattr(llm, "_last_usage", None)
|
||||
if not isinstance(reported, dict):
|
||||
@@ -174,10 +185,22 @@ def _prefer_provider_usage(llm: Any, call_usage: Dict[str, int]) -> Dict[str, in
|
||||
# Slotted/immutable LLM stand-ins can't record the claim; this
|
||||
# call still gets the provider counts, which is correct for them.
|
||||
pass
|
||||
return {
|
||||
merged = {
|
||||
**call_usage,
|
||||
"prompt_tokens": int(prompt or 0),
|
||||
"generated_tokens": int(completion or 0),
|
||||
}
|
||||
details = reported.get("prompt_tokens_details")
|
||||
if isinstance(details, dict):
|
||||
cached = details.get("cached_tokens")
|
||||
written = details.get("cache_write_tokens")
|
||||
if written is None:
|
||||
written = details.get("cache_creation_tokens")
|
||||
if cached is not None:
|
||||
merged["cached_tokens"] = int(cached or 0)
|
||||
if written is not None:
|
||||
merged["cache_write_tokens"] = int(written or 0)
|
||||
return merged
|
||||
|
||||
|
||||
def gen_token_usage(func):
|
||||
@@ -224,6 +247,8 @@ def gen_token_usage(func):
|
||||
prompt_tokens=call_usage["prompt_tokens"],
|
||||
completion_tokens=call_usage["generated_tokens"],
|
||||
latency_ms=int((time.monotonic() - started_at) * 1000),
|
||||
cached_tokens=call_usage.get("cached_tokens"),
|
||||
cache_write_tokens=call_usage.get("cache_write_tokens"),
|
||||
error=error,
|
||||
)
|
||||
except Exception:
|
||||
@@ -272,6 +297,8 @@ def stream_token_usage(func):
|
||||
prompt_tokens=call_usage["prompt_tokens"],
|
||||
completion_tokens=call_usage["generated_tokens"],
|
||||
latency_ms=int((time.monotonic() - started_at) * 1000),
|
||||
cached_tokens=call_usage.get("cached_tokens"),
|
||||
cache_write_tokens=call_usage.get("cache_write_tokens"),
|
||||
error=error,
|
||||
)
|
||||
except Exception:
|
||||
|
||||
Reference in new issue
Block a user