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:
@@ -31,6 +31,11 @@ PEAK_WINDOWS: tuple[tuple[time, time], ...] = (
|
||||
(time(14, 0), time(18, 0)),
|
||||
)
|
||||
|
||||
# 记入 ob_ai_call.rate_tier,用于事后复核 cost_cents。
|
||||
TIER_PEAK = "peak"
|
||||
TIER_OFF_PEAK = "off_peak"
|
||||
TIER_FLAT = "flat" # 只有一套单价(未启用峰谷,或该 provider 不分时段)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModelRate:
|
||||
@@ -63,6 +68,9 @@ class RateTable:
|
||||
def known_models(self) -> tuple[str, ...]:
|
||||
return tuple(sorted(self._rates))
|
||||
|
||||
def current_tier(self, at: datetime | None = None) -> str:
|
||||
return TIER_FLAT
|
||||
|
||||
|
||||
def is_peak(at: datetime | None = None) -> bool:
|
||||
"""调用时刻是否落在供应商的高峰时段(按北京时间判定)。"""
|
||||
@@ -94,6 +102,11 @@ class PeakAwareRateTable:
|
||||
def known_models(self) -> tuple[str, ...]:
|
||||
return self._off_peak.known_models()
|
||||
|
||||
def current_tier(self, at: datetime | None = None) -> str:
|
||||
if not self._enabled:
|
||||
return TIER_FLAT
|
||||
return TIER_PEAK if is_peak(at) else TIER_OFF_PEAK
|
||||
|
||||
|
||||
def compute_cost_cents(usage: Usage, rate: ModelRate) -> Decimal:
|
||||
"""Cost of one call, in cents, with cache-hit tokens billed separately."""
|
||||
@@ -110,6 +123,10 @@ class RateResolver(Protocol):
|
||||
|
||||
def for_model(self, model: str) -> ModelRate: ...
|
||||
|
||||
def current_tier(self) -> str:
|
||||
"""此刻适用的档位,随成本一起落库以便事后复核。"""
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AICallRecord:
|
||||
@@ -126,6 +143,7 @@ class AICallRecord:
|
||||
user_id: int | None = None
|
||||
prompt_id: str | None = None
|
||||
prompt_version: str | None = None
|
||||
rate_tier: str | None = None
|
||||
error_code: str | None = None
|
||||
error_message: str | None = None
|
||||
ref_type: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user