feat: bound AI-plan work to resource-control and stop zeroing missing tokens

Record the FIN-WP-0007 split after checking resource-control: they keep
intelligence resource identity and provision-level class I metering;
session tokens stay with State Hub; work-effectiveness is a reporting
join. Missing token counts are now unknown, not zero.
This commit is contained in:
tegwick 2026-08-15 19:01:45 +02:00
parent 004c041dc0
commit 09ef7fa967
10 changed files with 326 additions and 38 deletions

View file

@ -13,14 +13,28 @@ from fin_hub.ingest._csv import parse_amount, pick, read_csv_rows
class TokenSpendRow:
provider: str
model: str
tokens_in: int
tokens_out: int
tokens_in: int | None
tokens_out: int | None
cost: float
currency: str
session_id: str | None
recorded_at: datetime
def parse_optional_token_count(value: str) -> int | None:
"""Parse a token quantity. Empty is unknown; 0 is measured none."""
if not value:
return None
try:
count = int(value)
except ValueError as error:
raise ValueError(f"token count must be an integer, got {value!r}") from error
if count < 0:
raise ValueError(f"token count cannot be negative: {count}")
return count
def parse_anthropic_billing_csv(path: Path, *, default_currency: str = "USD") -> list[TokenSpendRow]:
rows: list[TokenSpendRow] = []
for row in read_csv_rows(path):
@ -28,8 +42,12 @@ def parse_anthropic_billing_csv(path: Path, *, default_currency: str = "USD") ->
cost_raw = pick(row, "cost", "amount", "total_cost", "usage_cost_usd")
if not model or not cost_raw:
continue
tokens_in = int(pick(row, "input_tokens", "tokens_in", "prompt_tokens") or "0")
tokens_out = int(pick(row, "output_tokens", "tokens_out", "completion_tokens") or "0")
tokens_in = parse_optional_token_count(
pick(row, "input_tokens", "tokens_in", "prompt_tokens")
)
tokens_out = parse_optional_token_count(
pick(row, "output_tokens", "tokens_out", "completion_tokens")
)
recorded_raw = pick(row, "date", "usage_date", "recorded_at", "timestamp")
recorded_at = (
datetime.fromisoformat(recorded_raw.replace("Z", "+00:00"))

View file

@ -69,8 +69,8 @@ class TokenSpend(Base, TimestampMixin):
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
provider: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
model: Mapped[str] = mapped_column(String(64), nullable=False)
tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
tokens_in: Mapped[int | None] = mapped_column(Integer, nullable=True)
tokens_out: Mapped[int | None] = mapped_column(Integer, nullable=True)
cost: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
session_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)