feat(billing): add billing source classification for sessions and update cost handling
This commit is contained in:
parent
28e103452b
commit
a8e560a586
|
|
@ -66,6 +66,7 @@ def _session_to_read(s: reader.ClaudeSession) -> ClaudeSessionRead:
|
|||
total=s.tokens.total,
|
||||
),
|
||||
cost_usd=s.cost_usd,
|
||||
billing_source=s.billing_source,
|
||||
message_count=s.message_count,
|
||||
first_message_at=s.first_message_at,
|
||||
last_message_at=s.last_message_at,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ class ClaudeSessionRead(SQLModel):
|
|||
title: str | None = None
|
||||
models: list[str]
|
||||
tokens: SessionTokensRead
|
||||
cost_usd: float
|
||||
cost_usd: float # always $0 for subscription sessions
|
||||
billing_source: str # "subscription" | "api"
|
||||
message_count: int
|
||||
first_message_at: datetime | None = None
|
||||
last_message_at: datetime | None = None
|
||||
|
|
|
|||
|
|
@ -23,6 +23,32 @@ logger = get_logger(__name__)
|
|||
|
||||
ACTIVE_WINDOW_MINUTES = 30
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Billing classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Entrypoints that run against the user's Claude subscription (flat-rate, not
|
||||
# per-token API billing). Sessions from these clients cost $0 in real money
|
||||
# even though they consume tokens.
|
||||
_SUBSCRIPTION_ENTRYPOINTS: frozenset[str] = frozenset({
|
||||
"claude-vscode",
|
||||
"claude-jetbrains",
|
||||
"claude-web",
|
||||
})
|
||||
|
||||
|
||||
def _billing_source(entrypoints: set[str]) -> str:
|
||||
"""Return 'subscription' or 'api' based on the session's entrypoints.
|
||||
|
||||
Any session that touched a subscription client is treated as subscription
|
||||
(cost = $0). Sessions with no entrypoint metadata default to subscription
|
||||
because JSONL files are written by local Claude Code clients, not the API.
|
||||
"""
|
||||
if not entrypoints or entrypoints & _SUBSCRIPTION_ENTRYPOINTS:
|
||||
return "subscription"
|
||||
return "api"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pricing (USD per million tokens) — mirrors runtime_usage.DEFAULT_MODEL_PRICING
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -86,7 +112,8 @@ class ClaudeSession:
|
|||
title: str | None
|
||||
models: list[str]
|
||||
tokens: SessionTokens
|
||||
cost_usd: float
|
||||
cost_usd: float # $0 for subscription sessions, real cost for api sessions
|
||||
billing_source: str # "subscription" | "api"
|
||||
message_count: int # assistant turns
|
||||
first_message_at: datetime | None
|
||||
last_message_at: datetime | None
|
||||
|
|
@ -221,11 +248,15 @@ def _parse_session_file(path: Path) -> ClaudeSession | None:
|
|||
return None
|
||||
|
||||
model_list = sorted(models)
|
||||
primary_model = model_list[0] if model_list else ""
|
||||
cost = _price(primary_model, tokens.input, tokens.output, tokens.cache_read, tokens.cache_write)
|
||||
for m in model_list[1:]:
|
||||
# Additional models — approximate with same token split (rare)
|
||||
cost += _price(m, 0, 0, 0, 0)
|
||||
billing = _billing_source(entrypoints)
|
||||
|
||||
if billing == "subscription":
|
||||
cost = 0.0
|
||||
else:
|
||||
primary_model = model_list[0] if model_list else ""
|
||||
cost = _price(primary_model, tokens.input, tokens.output, tokens.cache_read, tokens.cache_write)
|
||||
for m in model_list[1:]:
|
||||
cost += _price(m, 0, 0, 0, 0)
|
||||
|
||||
now = datetime.utcnow()
|
||||
is_active = bool(last_ts and (now - last_ts) < timedelta(minutes=ACTIVE_WINDOW_MINUTES))
|
||||
|
|
@ -238,6 +269,7 @@ def _parse_session_file(path: Path) -> ClaudeSession | None:
|
|||
models=model_list,
|
||||
tokens=tokens,
|
||||
cost_usd=round(cost, 6),
|
||||
billing_source=billing,
|
||||
message_count=message_count,
|
||||
first_message_at=first_ts,
|
||||
last_message_at=last_ts,
|
||||
|
|
|
|||
Loading…
Reference in New Issue