Files
meida_backend/apps/api/schemas.py
T
xsl f46ecc5eb0 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.
2026-08-14 00:54:40 +08:00

99 lines
2.2 KiB
Python
Executable File

"""Request/response schemas.
NFR-MAINT-01 契约先行: schema 先定义,客户端类型由 OpenAPI 生成。
Wire responses are wrapped by the unified envelope (架构文档 §9.2); these models
describe the ``data`` payload only.
"""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, Field
class DevTokenRequest(BaseModel):
device_key: str = Field(
default="local-dev",
min_length=1,
max_length=64,
description="任意稳定字符串;同一 device_key 复用同一测试用户",
)
class DevTokenResponse(BaseModel):
access_token: str
token_type: str = "Bearer"
expires_in: int
user_id: str = Field(description="雪花 ID 以字符串返回,避免 JS 精度丢失")
class ChatRequest(BaseModel):
message: str = Field(min_length=1, max_length=2000)
request_key: str | None = Field(
default=None,
max_length=128,
description="幂等键;相同键不会重复扣配额 (架构文档 §9.1)",
)
class ChatUsage(BaseModel):
tokens_in: int
tokens_out: int
tokens_cached: int
latency_ms: int
cost_cents: str
class ChatResponse(BaseModel):
reply: str
provider: str
model: str
prompt_id: str | None
prompt_version: str | None
call_id: str
usage: ChatUsage
quota_remaining: int
class QuotaResponse(BaseModel):
quota_key: str
period_key: str
used: int
limit_value: int
bonus_value: int
remaining: int
reset_at: datetime | None
class LedgerEntry(BaseModel):
tx_id: str
delta: int
tx_type: str
status: str
reason: str | None
quota_key: str | None
period_key: str | None
balance_after: int
created_at: datetime
class TaskSubmitRequest(BaseModel):
echo: str = Field(default="ping", max_length=200)
idempotency_key: str | None = Field(default=None, max_length=128)
class TaskResponse(BaseModel):
task_id: str
task_type: str
status: str
progress: int
stage_text: str | None
result: dict[str, object] | None
error_code: int | None
error_message: str | None
retry_count: int
submitted_at: datetime
started_at: datetime | None
finished_at: datetime | None