fix(llm): price by call time and stop paying for thinking tokens

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.
This commit is contained in:
xsl
2026-08-14 00:54:40 +08:00
parent 8a4720472b
commit f46ecc5eb0
34 changed files with 1793 additions and 988 deletions
+24 -8
View File
@@ -31,8 +31,13 @@ trap cleanup EXIT
mkdir -p "${LOG_DIR}"
[[ -f .env ]] || fail ".env not found. Copy .env.example to .env and set DEEPSEEK_API_KEY."
set -a; source .env; set +a
[[ -n "${DEEPSEEK_API_KEY:-}" ]] || fail "DEEPSEEK_API_KEY is empty in .env"
# 只取需要的这一个键,不 `source .env`:那些 *_JSON 值含双引号,
# 被 shell 解析后引号会消失,应用侧再读就成了非法 JSON。
DEEPSEEK_API_KEY=$(sed -n 's/^[[:space:]]*DEEPSEEK_API_KEY[[:space:]]*=[[:space:]]*//p' .env | tail -1)
DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY%\"}; DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY#\"}
DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY%\'}; DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY#\'}
[[ -n "${DEEPSEEK_API_KEY}" ]] || fail "DEEPSEEK_API_KEY is empty in .env"
[[ "${DEEPSEEK_API_KEY}" == sk-* ]] || fail "DEEPSEEK_API_KEY does not look like a real key"
bold "1/7 Starting PostgreSQL (pgvector) and Redis"
${COMPOSE} up -d postgres redis
@@ -78,23 +83,34 @@ CHAT_JSON=$(curl -fsS -X POST "${BASE}/v1/agent/chat" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${TOKEN}" \
-d '{"message":"三十岁男生,第一次去互联网公司面试,穿什么比较稳妥?"}')
printf '%s\n' "${CHAT_JSON}" | python3 -m json.tool
# 走临时文件而不是管道:heredoc 会占用 stdin,管道里的 JSON 读不到
printf '%s' "${CHAT_JSON}" >"${LOG_DIR}/chat.json"
python3 -m json.tool --no-ensure-ascii "${LOG_DIR}/chat.json"
python3 - "${LOG_DIR}/chat.json" <<'PYEOF'
import json, sys
python3 - "${LOG_DIR}/chat.json" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as fh:
body = json.load(fh)
assert body["code"] == 0, body
data = body["data"]
assert data["reply"].strip(), "empty model reply"
usage = data["usage"]
print("\n--- MODEL REPLY -------------------------------------------------")
print(data["reply"])
print("-----------------------------------------------------------------")
print("provider={provider} model={model} prompt={prompt_id}@{prompt_version}".format(**data))
print("usage={}".format(data["usage"]))
print("provider={} model={} prompt={}@{}".format(
data["provider"], data["model"], data["prompt_id"], data["prompt_version"]))
print("latency={}ms tokens_in={} tokens_out={} cached={} cost_cents={}".format(
usage["latency_ms"], usage["tokens_in"], usage["tokens_out"],
usage["tokens_cached"], usage["cost_cents"]))
print("quota_remaining={}".format(data["quota_remaining"]))
PYEOF
# 对话必须在 NFR-PERF-02b 的整轮 15s 之内。这里只量单次 LLM 调用,
# 留出检索与后处理的余量,因此阈值取 10s。
assert usage["latency_ms"] < 10_000, f"LLM 调用耗时 {usage['latency_ms']}ms,超出预期"
PY
bold "6/7 ob_ai_call row (成本归因的唯一数据源, 架构文档 §13)"
${COMPOSE} exec -T postgres psql -U meida -d meida -x -c \