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.
88 lines
2.7 KiB
Python
Executable File
88 lines
2.7 KiB
Python
Executable File
"""Snowflake IDs (架构文档 §6.1 原则 2).
|
|
|
|
Auto-increment keys invite enumeration attacks and collide when the database is
|
|
later sharded; UUID primary keys have poor index locality. A 64-bit snowflake
|
|
avoids both.
|
|
|
|
Layout: 41 bits ms since epoch | 10 bits node id | 12 bits sequence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from collections.abc import Callable
|
|
|
|
#: 2025-01-01T00:00:00Z in milliseconds. Do not change once IDs are issued.
|
|
DEFAULT_EPOCH_MS = 1_735_689_600_000
|
|
|
|
_NODE_BITS = 10
|
|
_SEQUENCE_BITS = 12
|
|
_MAX_NODE_ID = (1 << _NODE_BITS) - 1
|
|
_SEQUENCE_MASK = (1 << _SEQUENCE_BITS) - 1
|
|
|
|
|
|
def _system_clock_ms() -> int:
|
|
return int(time.time() * 1000)
|
|
|
|
|
|
class SnowflakeGenerator:
|
|
"""Thread-safe monotonic 64-bit id generator."""
|
|
|
|
__slots__ = ("_clock", "_epoch_ms", "_last_ms", "_lock", "_node_id", "_sequence")
|
|
|
|
def __init__(
|
|
self,
|
|
node_id: int = 1,
|
|
epoch_ms: int = DEFAULT_EPOCH_MS,
|
|
clock: Callable[[], int] | None = None,
|
|
) -> None:
|
|
if not 0 <= node_id <= _MAX_NODE_ID:
|
|
msg = f"node_id must be within 0..{_MAX_NODE_ID}"
|
|
raise ValueError(msg)
|
|
self._node_id = node_id
|
|
self._epoch_ms = epoch_ms
|
|
self._sequence = 0
|
|
self._last_ms = -1
|
|
self._lock = threading.Lock()
|
|
# 时钟可注入:序号回绕与时钟回拨这两条分支只能靠假时钟覆盖,
|
|
# 真等一毫秒的测试既慢又不确定。
|
|
self._clock = clock or _system_clock_ms
|
|
|
|
def _now_ms(self) -> int:
|
|
return self._clock()
|
|
|
|
def next_id(self) -> int:
|
|
with self._lock:
|
|
now = self._now_ms()
|
|
if now < self._last_ms:
|
|
# Clock moved backwards: wait it out rather than issue duplicates.
|
|
now = self._last_ms
|
|
if now == self._last_ms:
|
|
self._sequence = (self._sequence + 1) & _SEQUENCE_MASK
|
|
if self._sequence == 0:
|
|
while now <= self._last_ms:
|
|
now = self._now_ms()
|
|
else:
|
|
self._sequence = 0
|
|
self._last_ms = now
|
|
elapsed = now - self._epoch_ms
|
|
return (
|
|
(elapsed << (_NODE_BITS + _SEQUENCE_BITS))
|
|
| (self._node_id << _SEQUENCE_BITS)
|
|
| self._sequence
|
|
)
|
|
|
|
|
|
_default_generator = SnowflakeGenerator()
|
|
|
|
|
|
def next_id() -> int:
|
|
"""Process-wide default generator."""
|
|
return _default_generator.next_id()
|
|
|
|
|
|
def timestamp_ms_of(snowflake_id: int, epoch_ms: int = DEFAULT_EPOCH_MS) -> int:
|
|
"""Extract the creation timestamp from an id (useful for partition pruning)."""
|
|
return (snowflake_id >> (_NODE_BITS + _SEQUENCE_BITS)) + epoch_ms
|