Two defects that only surface against the real DeepSeek API, both of which break the cost model rather than the code: deepseek-v4-flash enables thinking mode by default. The same single-turn question took 13.5s with it on and 1.8s with it off, and the thinking tokens are billed at the output rate. That misses NFR-PERF-02b (15s per turn) and inflates every conversational call. SceneConfig now carries an explicit `thinking` flag, all four V1 scenes set it to false, and the provider always sends the parameter rather than relying on the upstream default. Reasoning tokens are recorded separately in Usage.tokens_reasoning so the cost of ever turning it back on is visible. DeepSeek moves to peak/off-peak pricing on 2026-08-17, with peak (Beijing 09:00-12:00 and 14:00-18:00) at twice the off-peak rate. Reports are generated mostly during peak hours, so a single blended rate would leave ob_ai_call -- the only input to V2 pricing -- off by nearly a factor of two. PeakAwareRateTable resolves the rate at call time; cost is computed and stored then, never recomputed later at whatever the current rate happens to be. Also here: - Replace the live API key in the redaction test with a synthetic one. The test that proves we redact secrets was itself a secret leak waiting for its first commit. - Correct LLM_COST_RATES_JSON, which held placeholder rates. - Make ids.SnowflakeGenerator take an injectable clock so rollover and clock-rollback are testable without patching a slots class. - Rename exceptions to the -Error suffix (N818) and clear the remaining ruff, mypy and import-linter findings, including two dead lines: a duplicate pytestmark assignment and a no-op assert on a None return. Gates: ruff, mypy (96 files incl. tests), 4/4 import contracts, detect-secrets, 166 unit + 3 integration tests, migration up/down round-trip.
packages/domain/ — 纯领域逻辑
架构 §3.2 硬性规则 2:领域逻辑放在这里,不依赖 FastAPI、不依赖数据库会话, 可纯单元测试。
能放什么
- 数据类与值对象(配额状态、任务状态、诊断维度…)
- 判定与计算(配额是否够、状态能否流转、置信度冲突消解、色差计算…)
- 纯函数、不可变数据结构
绝对不能放什么
import fastapi/sqlalchemy/httpx/redis/celeryimport packages.shared(连日志和配置都不行)- 任何 IO:读文件、发请求、查库、读环境变量
async def里真的 await 了 IO 的东西
违反会被 import-linter 的 packages.domain is pure 契约在 CI 拦下。
为什么这么严
这是 AI 大量写代码时唯一还能快速验证正确性的地方 —— 没有 mock、没有 fixture、没有容器,几毫秒跑完几百个用例。一旦让 IO 渗进来,这个性质就没了, 后面所有业务规则的测试都会退化成集成测试。
mypy strict
本目录与 packages/llm/ 一起,是 mypy strict 模式覆盖的范围
(架构 §16.2)。所有函数必须有完整类型注解。
现有内容
| 模块 | 内容 |
|---|---|
ids.py |
雪花 ID 生成(§6.1 原则 2) |
errors.py |
纯领域异常,适配层负责翻译成 AppError |
billing/quota.py |
配额三段式的算术与周期键(§7.4) |
task/state.py |
tk_task 状态机(§6.2) |