feat(cost): persist reasoning tokens and the peak/off-peak tier
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
ob_ai_call is the only input to V2 pricing, and it was dropping the two numbers needed to interpret its own cost column. tokens_reasoning was captured from the provider response but never written to the table, so the cost of thinking mode was invisible once it left the process. Folding it into tokens_out was not an option either -- it is billed at the output rate, and mixing the two hides exactly the difference we now have to watch. rate_tier records which price list applied. Without it a row's cost_cents cannot be rechecked: DeepSeek's peak rate is double off-peak from 2026-08-17, so recomputing an old row at "the current rate" silently produces a different answer than what was actually billed. Migration 0002 adds both columns. tokens_reasoning defaults to 0 rather than NULL so cost aggregates do not collapse to NULL on historical rows; rate_tier stays nullable because rows written before this change genuinely have an unknown tier, and NULL says that more honestly than a guessed 'flat'. The smoke script now asserts both: reasoning tokens must be 0 (a non-zero value means a provider default re-enabled thinking mode behind our backs) and rate_tier must be present. Verified against the real API -- tokens_reasoning=0, rate_tier=off_peak, 3.9s round trip.
This commit is contained in:
@@ -392,6 +392,7 @@ async def test_every_call_records_a_cost_row(prompts_dir: Path) -> None:
|
||||
assert record.usage.tokens_cached == 800
|
||||
assert record.cost_cents == result.cost_cents > Decimal("0")
|
||||
assert record.created_at is not None
|
||||
assert record.rate_tier # 复核成本要知道当次单价档位
|
||||
|
||||
|
||||
async def test_failed_calls_are_recorded_too(prompts_dir: Path) -> None:
|
||||
@@ -405,6 +406,8 @@ async def test_failed_calls_are_recorded_too(prompts_dir: Path) -> None:
|
||||
assert record.success is False
|
||||
assert record.error_code == "llm_timeout"
|
||||
assert record.cost_cents == Decimal("0")
|
||||
# 失败行也要带档位:排查「为什么这天成本高」时失败重试也是成本的一部分
|
||||
assert record.rate_tier
|
||||
|
||||
|
||||
async def test_a_broken_recorder_never_breaks_the_user_request(prompts_dir: Path) -> None:
|
||||
|
||||
@@ -12,6 +12,9 @@ from decimal import Decimal
|
||||
import pytest
|
||||
|
||||
from packages.llm.cost import (
|
||||
TIER_FLAT,
|
||||
TIER_OFF_PEAK,
|
||||
TIER_PEAK,
|
||||
ModelRate,
|
||||
PeakAwareRateTable,
|
||||
RateTable,
|
||||
@@ -84,3 +87,15 @@ def test_unknown_model_still_costs_money_in_both_tiers() -> None:
|
||||
for hour in (10, 13):
|
||||
rate = table.for_model("some-new-model", datetime(2026, 8, 20, hour, tzinfo=BEIJING))
|
||||
assert rate.input_cents_per_mtok > 0
|
||||
|
||||
|
||||
def test_tier_is_reported_so_cost_can_be_rechecked_later() -> None:
|
||||
# 没有档位,事后就无法判断某行 cost_cents 是按哪套单价算的
|
||||
table = _table()
|
||||
assert table.current_tier(datetime(2026, 8, 20, 10, 0, tzinfo=BEIJING)) == TIER_PEAK
|
||||
assert table.current_tier(datetime(2026, 8, 20, 13, 0, tzinfo=BEIJING)) == TIER_OFF_PEAK
|
||||
|
||||
|
||||
def test_single_rate_tables_report_a_flat_tier() -> None:
|
||||
assert RateTable({MODEL: OFF_PEAK}, default=OFF_PEAK).current_tier() == TIER_FLAT
|
||||
assert _table(enabled=False).current_tier() == TIER_FLAT
|
||||
|
||||
Reference in New Issue
Block a user