feat(cost): persist reasoning tokens and the peak/off-peak tier
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:
xsl
2026-08-14 01:02:13 +08:00
parent f46ecc5eb0
commit c97f8ae3e1
9 changed files with 119 additions and 2 deletions
@@ -0,0 +1,44 @@
"""ob_ai_call: record reasoning tokens and the peak/off-peak tier
两列都是为了让 ``ob_ai_call`` 的成本可复核 —— 这张表是 V2 定价的唯一依据
(架构文档 §13),少一列就少一块无法事后补齐的事实。
``tokens_reasoning``
思考模式产生的 token,按输出价计费。不并入 ``tokens_out``:混在一起就看不出
开关思考模式的成本差异,而 ``deepseek-v4-flash`` 默认开启该模式(T-P1-24a)。
``rate_tier``
该次调用适用的峰谷档位。DeepSeek 自 2026-08-17 起高峰单价为空闲时段的 2 倍,
成本在调用当时算好落库;没有档位列,事后就无法判断某行的 ``cost_cents``
是按哪套单价算出来的(T-P1-25c)。
Reversible: 是。降级只丢这两列的数据,不影响既有行。
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "0002"
down_revision: str | None = "0001"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# server_default 让历史行取 0 而不是 NULL:成本聚合里 NULL 会把整个 SUM 变成 NULL。
op.add_column(
"ob_ai_call",
sa.Column("tokens_reasoning", sa.Integer(), nullable=False, server_default="0"),
)
# 允许 NULL:0001 之后写入的历史行确实无法判定档位,
# 用 NULL 表达「未知」比塞一个可能错的 'flat' 更诚实。
op.add_column("ob_ai_call", sa.Column("rate_tier", sa.String(length=16), nullable=True))
def downgrade() -> None:
op.drop_column("ob_ai_call", "rate_tier")
op.drop_column("ob_ai_call", "tokens_reasoning")