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:
+47
-51
@@ -1,85 +1,81 @@
|
||||
# =============================================================================
|
||||
# 美搭 backend — environment template
|
||||
# =============================================================================
|
||||
# Copy to `.env` and fill in real values. `.env` is git-ignored and MUST NEVER
|
||||
# be committed (架构文档 §11.2: 密钥全部走 SSM/KMS,禁止进代码与镜像).
|
||||
# In dev/staging/prod these variables are injected by SSM/KMS, not by a file.
|
||||
# Every variable below is documented; NO real values appear in this file.
|
||||
# =============================================================================
|
||||
# 复制为 .env 后填入真实值。.env 已在 .gitignore 中,不要提交。
|
||||
# dev/staging/prod 由部署平台注入环境变量,不使用 .env(架构文档 §11.2)。
|
||||
|
||||
# --- Application ------------------------------------------------------------
|
||||
APP_ENV=local # local | dev | staging | prod (架构文档 §14.2)
|
||||
APP_ENV=local
|
||||
APP_NAME=meida-backend
|
||||
API_PREFIX=/api/v1 # 架构文档 §9.1 基础路径;/v1/* 作为别名同样可用
|
||||
API_PREFIX=/api/v1
|
||||
DEBUG=true
|
||||
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR
|
||||
LOG_JSON=true # false => human-readable console renderer
|
||||
LOG_LEVEL=INFO
|
||||
LOG_JSON=true
|
||||
# 需要额外脱敏的字段名,逗号分隔;默认清单见 packages/shared/logging.py
|
||||
LOG_EXTRA_REDACT_KEYS=
|
||||
|
||||
# --- Database (PostgreSQL 16 + pgvector) ------------------------------------
|
||||
# Async driver is required (SQLAlchemy 2.0 asyncio + asyncpg).
|
||||
# ── 数据库 / Redis ─────────────────────────────────────────────────────────
|
||||
# 端口刻意用 55432 / 56379,避免与本机已有的 PostgreSQL、Redis 抢端口
|
||||
DATABASE_URL=postgresql+asyncpg://meida:meida@localhost:55432/meida
|
||||
DB_POOL_SIZE=10
|
||||
DB_MAX_OVERFLOW=20
|
||||
DB_ECHO=false
|
||||
|
||||
# --- Redis 7 (cache / quota counters / celery broker) -----------------------
|
||||
REDIS_URL=redis://localhost:56379/0
|
||||
CELERY_BROKER_URL=redis://localhost:56379/1
|
||||
CELERY_RESULT_BACKEND=redis://localhost:56379/2
|
||||
|
||||
# --- Auth (架构文档 §9.1: JWT access 15min + 可撤销 refresh) ----------------
|
||||
JWT_SECRET= # REQUIRED. >=32 random chars. Generate: openssl rand -hex 32
|
||||
# ── 鉴权 ──────────────────────────────────────────────────────────────────
|
||||
# 生成:openssl rand -hex 32 。staging/prod 若沿用占位值会启动失败
|
||||
JWT_SECRET=change-me-local-only-do-not-use-outside-local
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TTL_SECONDS=900 # 15 minutes
|
||||
JWT_ACCESS_TTL_SECONDS=900
|
||||
JWT_ISSUER=meida
|
||||
DEV_AUTH_ENABLED=true # MUST be false outside local/dev: enables POST /auth/dev-token
|
||||
# 免登录换 token 的调试入口,仅本地/联调可开
|
||||
DEV_AUTH_ENABLED=true
|
||||
|
||||
# --- LLM gateway: routing (架构文档 §8.1) -----------------------------------
|
||||
LLM_PRIMARY_PROVIDER=deepseek # DeepSeek 为主 (TD-04)
|
||||
LLM_BACKUP_PROVIDER=zhipu # 智谱 GLM 为备(V1 未实现,仅结构占位)
|
||||
LLM_STRUCTURED_MAX_RETRIES=2 # 结构化输出校验失败的重试次数
|
||||
LLM_CIRCUIT_FAILURE_THRESHOLD=3 # 连续失败达到该值即熔断
|
||||
LLM_CIRCUIT_COOLDOWN_SECONDS=30 # 熔断冷却时长,之后半开探测
|
||||
# JSON 值一律用单引号包住:scripts/smoke.sh 会 `source .env`,
|
||||
# 裸的双引号会被 bash 剥掉,JSON 就此损坏。
|
||||
LLM_SCENE_OVERRIDES_JSON='{}' # 场景路由覆盖,JSON: {"agent_chat":{"max_tokens":900}}
|
||||
# ── LLM 路由 ──────────────────────────────────────────────────────────────
|
||||
LLM_PRIMARY_PROVIDER=deepseek
|
||||
LLM_BACKUP_PROVIDER=zhipu
|
||||
LLM_STRUCTURED_MAX_RETRIES=2
|
||||
LLM_CIRCUIT_FAILURE_THRESHOLD=3
|
||||
LLM_CIRCUIT_COOLDOWN_SECONDS=30
|
||||
# 按场景覆盖模型与采样参数,如 {"agent_chat":{"temperature":0.5}}
|
||||
LLM_SCENE_OVERRIDES_JSON={}
|
||||
|
||||
# --- LLM gateway: DeepSeek (OpenAI-compatible) ------------------------------
|
||||
DEEPSEEK_API_KEY= # REQUIRED. sk-... Never commit.
|
||||
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
DEEPSEEK_MODEL=deepseek-v4-flash # 默认模型,可用: deepseek-v4-flash / deepseek-v4-pro
|
||||
DEEPSEEK_MODEL=deepseek-v4-flash
|
||||
DEEPSEEK_TIMEOUT_SECONDS=30
|
||||
|
||||
# --- LLM gateway: 智谱 GLM (backup, not implemented in V1) ------------------
|
||||
ZHIPU_API_KEY=
|
||||
ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4
|
||||
ZHIPU_MODEL=glm-4-flash
|
||||
ZHIPU_TIMEOUT_SECONDS=30
|
||||
|
||||
# --- LLM gateway: prompt caching discipline (架构文档 §7.2 缓存前缀纪律) ----
|
||||
# strict => 固定前缀含时间戳/随机ID/昵称时直接拒绝调用
|
||||
# warn => 只记录告警日志;off => 关闭(不建议)
|
||||
# ── Prompt 缓存纪律 ───────────────────────────────────────────────────────
|
||||
# strict:固定前缀里出现时间戳/随机值等易变 token 直接启动失败。
|
||||
# 命中缓存的输入价格只有未命中的 1/30,前缀被污染会让输入成本翻数十倍。
|
||||
LLM_CACHE_PREFIX_MODE=strict
|
||||
PROMPTS_DIR=prompts
|
||||
|
||||
# --- LLM gateway: 成本归因与费率 (架构文档 §13, FR-BILL-08) -----------------
|
||||
# 单位:分/百万 token(cents per 1M tokens)。上线前必须以供应商实际价目表校准。
|
||||
LLM_COST_RATES_JSON='{"deepseek-v4-flash":{"input_cents_per_mtok":"14","cached_input_cents_per_mtok":"2","output_cents_per_mtok":"28"},"deepseek-v4-pro":{"input_cents_per_mtok":"200","cached_input_cents_per_mtok":"20","output_cents_per_mtok":"800"}}'
|
||||
LLM_DEFAULT_COST_RATE_JSON='{"input_cents_per_mtok":"100","cached_input_cents_per_mtok":"20","output_cents_per_mtok":"200"}'
|
||||
# ── 成本单价(单位:分 / 百万 token)──────────────────────────────────────
|
||||
# 取自 DeepSeek 官方价目表。2026-08-17 00:00 起改为峰谷定价:
|
||||
# 高峰(北京时间 09:00-12:00、14:00-18:00)单价是空闲时段的 2 倍。
|
||||
# 因此这里要配两套,由 packages/llm/cost.py 按调用时刻选择。
|
||||
# flash 空闲:缓存命中 ¥0.05 / 输入 ¥1.5 / 输出 ¥4.5 每百万 token
|
||||
LLM_COST_RATES_JSON={"deepseek-v4-flash":{"input_cents_per_mtok":"150","cached_input_cents_per_mtok":"5","output_cents_per_mtok":"450"},"deepseek-v4-pro":{"input_cents_per_mtok":"450","cached_input_cents_per_mtok":"15","output_cents_per_mtok":"1350"},"glm-4-flash":{"input_cents_per_mtok":"10","cached_input_cents_per_mtok":"10","output_cents_per_mtok":"10"}}
|
||||
# flash 高峰:缓存命中 ¥0.10 / 输入 ¥3 / 输出 ¥9 每百万 token
|
||||
LLM_COST_RATES_PEAK_JSON={"deepseek-v4-flash":{"input_cents_per_mtok":"300","cached_input_cents_per_mtok":"10","output_cents_per_mtok":"900"},"deepseek-v4-pro":{"input_cents_per_mtok":"900","cached_input_cents_per_mtok":"30","output_cents_per_mtok":"2700"},"glm-4-flash":{"input_cents_per_mtok":"10","cached_input_cents_per_mtok":"10","output_cents_per_mtok":"10"}}
|
||||
# 未登记的模型按这个价算。刻意取高档:宁可高估触发熔断,也不要低估成本形成盲区
|
||||
LLM_DEFAULT_COST_RATE_JSON={"input_cents_per_mtok":"300","cached_input_cents_per_mtok":"10","output_cents_per_mtok":"900"}
|
||||
LLM_PEAK_PRICING_ENABLED=true
|
||||
|
||||
# --- LLM gateway: 全局日成本熔断 (架构文档 §13 三道成本闸门) ----------------
|
||||
# 达到阈值后,非关键场景的 AI 调用被直接拒绝/降级,而不仅仅是告警。
|
||||
# ── 全局日成本熔断(NFR-COST-02)─────────────────────────────────────────
|
||||
# 按用户配额封不住总成本(总成本 = 用户数 × 人均,用户数无上限),
|
||||
# 真正封顶的只有这个全局开关。1000_00 分 = ¥1000/天
|
||||
LLM_DAILY_BUDGET_ENABLED=true
|
||||
LLM_DAILY_BUDGET_CENTS=100000 # 1000 元/天
|
||||
LLM_BUDGET_TIMEZONE_OFFSET_HOURS=8 # 结算日按 UTC+8 (FR-BILL-01)
|
||||
LLM_DAILY_BUDGET_CENTS=100000
|
||||
LLM_BUDGET_TIMEZONE_OFFSET_HOURS=8
|
||||
|
||||
# --- Observability (架构文档 §15) -------------------------------------------
|
||||
# ── 可观测性 ──────────────────────────────────────────────────────────────
|
||||
OTEL_ENABLED=false
|
||||
OTEL_SERVICE_NAME=meida-api
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT= # e.g. http://localhost:4318
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
PROMETHEUS_ENABLED=true
|
||||
|
||||
# --- Logging redaction (NFR-SEC-03) -----------------------------------------
|
||||
# Extra log field names to redact, comma separated. Built-in list already covers
|
||||
# phone / token / union_id / open_id / avatar_key / face image keys / diagnosis.
|
||||
LOG_EXTRA_REDACT_KEYS=
|
||||
|
||||
Reference in New Issue
Block a user