fin-hub/src/fin_hub/schemas/session_tokens.py
tegwick 033ad3f41f feat: publish unblended AI-plan effectiveness series
Report booked cost, entitlement, token coverage, and work as three
series: work per measured token, work per measured+estimated token,
and work per euro. Months below 50% coverage are unfit for token
trends. There is no blended efficiency number.
2026-08-15 19:53:28 +02:00

96 lines
3.2 KiB
Python

"""Session-token evidence ingested from State Hub aggregates."""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
MEASUREMENT_KINDS = ("measured", "allocated", "estimated", "superseded")
EVIDENCE_BASIS = {
"measured": "measured",
"allocated": "derived",
"estimated": "estimated",
"superseded": "excluded",
}
class SessionTokenSlice(BaseModel):
model_config = ConfigDict(extra="forbid")
tokens_in: int = Field(ge=0)
tokens_out: int = Field(ge=0)
event_count: int = Field(ge=0)
confidence: Decimal | None = None
method: str | None = None
@field_validator("confidence")
@classmethod
def validate_confidence(cls, value: Decimal | None) -> Decimal | None:
if value is None:
return None
if not value.is_finite() or value < 0 or value > 1:
raise ValueError("confidence must be between 0 and 1")
return value
class SessionTokenWork(BaseModel):
model_config = ConfigDict(extra="forbid")
unit: Literal["task", "outcome"] = "task"
count: int = Field(ge=0)
source: str = Field(min_length=1)
class SessionTokenAssociation(BaseModel):
model_config = ConfigDict(extra="forbid")
scope: Literal["repo", "workplan", "model"]
scope_id: str = Field(min_length=1, max_length=256)
label: str | None = None
measurement_kind: Literal["measured", "allocated", "estimated", "superseded"]
tokens_in: int = Field(ge=0)
tokens_out: int = Field(ge=0)
event_count: int = Field(ge=0, default=1)
class SessionTokenEvidence(BaseModel):
"""State Hub session-token snapshot. Not booked spend and not usage_observation."""
model_config = ConfigDict(extra="forbid")
schema_version: Literal["0.1"] = "0.1"
record_type: Literal["session_token_evidence"] = "session_token_evidence"
record_id: str = Field(min_length=1, max_length=256)
revision_of: str | None = None
source_system: Literal["state-hub"] = "state-hub"
period_start: date
period_end: date
captured_at: datetime
source_evidence: list[str] = Field(min_length=1)
totals: dict[str, SessionTokenSlice] = Field(default_factory=dict)
associations: list[SessionTokenAssociation] = Field(default_factory=list)
work: SessionTokenWork | None = None
@field_validator("totals")
@classmethod
def validate_totals(cls, value: dict[str, SessionTokenSlice]) -> dict[str, SessionTokenSlice]:
unknown = set(value) - set(MEASUREMENT_KINDS)
if unknown:
raise ValueError(f"unknown measurement_kind: {', '.join(sorted(unknown))}")
return value
@model_validator(mode="after")
def validate_period_and_methods(self) -> "SessionTokenEvidence":
if self.period_end < self.period_start:
raise ValueError("period_end cannot precede period_start")
allocated = self.totals.get("allocated")
if (
allocated is not None
and allocated.event_count > 0
and not (allocated.method and allocated.method.strip())
):
raise ValueError("allocated totals with events require method")
return self