c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
"""Per-session token / cost tracking via a ContextVar scope.
|
|
|
|
Call sites that know the active session_id wrap their work in
|
|
``track_session_usage(session_id, user_id)``. Inside that scope,
|
|
``LLMGateway.acreate_structured_output`` (and any other caller that
|
|
opts in) calls ``record_llm_call`` after each LLM completion. The
|
|
tracker accumulates into the ``SessionRecord`` row.
|
|
|
|
Token counts are approximate — we don't currently extract
|
|
``response.usage`` from the litellm/instructor client (requires
|
|
changes deeper in the stack). A ~chars/4 heuristic is close enough
|
|
for the dashboard's "are we spending?" question without plumbing
|
|
upstream.
|
|
"""
|
|
|
|
from contextlib import asynccontextmanager
|
|
from contextvars import ContextVar
|
|
from typing import Optional
|
|
from uuid import UUID as UUIDType
|
|
|
|
from cognee.shared.logging_utils import get_logger
|
|
|
|
logger = get_logger("session_usage")
|
|
|
|
|
|
# (session_id, user_id) when active, else None.
|
|
_active_session: ContextVar[Optional[tuple[str, UUIDType]]] = ContextVar(
|
|
"cognee_session_usage_target", default=None
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def track_session_usage(session_id: str, user_id: UUIDType):
|
|
"""Bind a session as the target for LLM-usage accumulation inside this scope."""
|
|
if not session_id or user_id is None:
|
|
yield
|
|
return
|
|
token = _active_session.set((session_id, user_id))
|
|
try:
|
|
yield
|
|
finally:
|
|
_active_session.reset(token)
|
|
|
|
|
|
def _estimate_tokens(text: str) -> int:
|
|
"""Very rough char-based estimate. Good enough for dashboard aggregates."""
|
|
if not text:
|
|
return 0
|
|
return max(1, len(text) // 4)
|
|
|
|
|
|
# Minimal per-model pricing table. Conservative and incomplete —
|
|
# unrecognized models cost $0.
|
|
# USD per 1M tokens: (input, output)
|
|
_PRICING_PER_M_TOKENS = {
|
|
"gpt-4o": (2.50, 10.00),
|
|
"gpt-4o-mini": (0.15, 0.60),
|
|
"gpt-4-turbo": (10.00, 30.00),
|
|
"gpt-4": (30.00, 60.00),
|
|
"gpt-3.5-turbo": (0.50, 1.50),
|
|
"claude-3-5-sonnet": (3.00, 15.00),
|
|
"claude-3-opus": (15.00, 75.00),
|
|
"claude-3-haiku": (0.25, 1.25),
|
|
"gemini-1.5-pro": (1.25, 5.00),
|
|
"gemini-1.5-flash": (0.075, 0.30),
|
|
}
|
|
|
|
|
|
# Longest prefix first so specific models (e.g. ``gpt-4o-mini``) win
|
|
# over their more general family (``gpt-4o``). Computed once at import.
|
|
_PRICING_SORTED = sorted(_PRICING_PER_M_TOKENS.items(), key=lambda kv: -len(kv[0]))
|
|
|
|
|
|
def _estimate_cost_usd(model: Optional[str], tokens_in: int, tokens_out: int) -> float:
|
|
if not model:
|
|
return 0.0
|
|
# Normalize: strip provider prefix ("openai/gpt-4o" → "gpt-4o"), drop date suffix.
|
|
key = model.split("/")[-1].lower()
|
|
for base, (pin, pout) in _PRICING_SORTED:
|
|
if key.startswith(base):
|
|
return (tokens_in / 1_000_000) * pin + (tokens_out / 1_000_000) * pout
|
|
return 0.0
|
|
|
|
|
|
async def record_llm_call(
|
|
*,
|
|
input_text: str,
|
|
output_text: str,
|
|
model: Optional[str] = None,
|
|
tokens_in_override: Optional[int] = None,
|
|
tokens_out_override: Optional[int] = None,
|
|
) -> None:
|
|
"""If there's an active session, accumulate this call's usage into it.
|
|
|
|
Pass ``tokens_in_override`` / ``tokens_out_override`` when the
|
|
caller has exact counts from ``response.usage``; otherwise the
|
|
char-based estimate is used.
|
|
"""
|
|
target = _active_session.get()
|
|
if target is None:
|
|
return
|
|
session_id, user_id = target
|
|
|
|
tokens_in = (
|
|
tokens_in_override if tokens_in_override is not None else _estimate_tokens(input_text)
|
|
)
|
|
tokens_out = (
|
|
tokens_out_override if tokens_out_override is not None else _estimate_tokens(output_text)
|
|
)
|
|
cost = _estimate_cost_usd(model, tokens_in, tokens_out)
|
|
|
|
try:
|
|
from cognee.modules.session_lifecycle.metrics import accumulate_usage
|
|
|
|
await accumulate_usage(
|
|
session_id=session_id,
|
|
user_id=user_id,
|
|
tokens_in=tokens_in,
|
|
tokens_out=tokens_out,
|
|
cost_usd=cost,
|
|
model=model,
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("record_llm_call: accumulate failed (%s)", exc)
|