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.
99 lines
2.2 KiB
Python
Executable File
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
|