Files
projectAIpopular/gateway/proxy/config.py
T
tzt e5470a3715 feat(proxy): T-X10 采纳 cortiq 语义缓存路由签名分桶——不同路由意图不互串答案
- normalizer.canonical_hash 增加可选 route_sig 段:键 = bucket|doc_version|sig|sha256
  (旧三段格式向后兼容,旧条目随 TTL 自然淘汰)
- ProxyConfig 新增 semcache.route_sig_scope:capabilities(默认,vision/tools 需求
  签名)/ model(按模型隔离)/ none(旧行为);非法值回落 capabilities
- semcache:签名升级为条目属性并分区 L2 语义扫描(仅键分桶不够——语义层仍会
  跨签名命中);签名从缓存键第四段解析,重启重建零 schema 变更;
  晋升别名键携带签名段;route_sig=None 的旧调用零过滤完全兼容
- routes:lookup/put 共用同一 norm_hash(消除 put 侧重复哈希),签名贯穿两层
- 新增 tests/test_route_sig.py 6 项(键格式/键空间分割/scope 三态/两层隔离/
  重建存活/旧调用兼容)
2026-09-19 09:59:15 +08:00

172 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ProxyConfig:从 settings 的 proxy 段构建代理层配置(T-P0)。
锁定决策落地:
- D-P1 货币:账本毫元整数。配置里价格是浮点"元/1M tokens"(人类可读),
**加载期一次性转为整数毫元/1M tokens**,运行期纯整数运算(毫元漂移只可能
发生在这一次换算,黄金用例锁死)。
- 售价折扣采用差异化 {in: 0.5, out: 0.8}(设计文档 §2.5 结论:统一 5 折
盈亏平衡 h0≈32% 结构性危险;执行版 §4 示例与之冲突,按设计文档取差异化,
纯配置可热改)。
- D-P7enabled=False 时不注册任何 /proxy 路由。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
# 元 -> 毫元换算系数(D-P11 元 = 1000 毫元)
YUAN_TO_MILLI = 1000
@dataclass
class ModelPrice:
"""单一模型的整数价格表(毫元 / 1M tokens)。"""
in_miss: int
in_hit: int
out: int
@dataclass
class BucketCfg:
"""一个课程桶的配置。"""
name: str
system_template: str = "你是校园学习助手。"
doc_prefix_file: Optional[str] = None
doc_version: int = 1
ttl_hours: int = 72
@dataclass
class ProxyConfig:
"""代理层运行配置(全部为加载期定型值)。"""
enabled: bool = False
admin_key: str = ""
db_path: str = "data/proxy.sqlite3"
buckets: Dict[str, BucketCfg] = field(default_factory=dict)
model_prices: Dict[str, ModelPrice] = field(default_factory=dict)
peak_start: str = "08:30"
peak_end: str = "23:59"
offpeak_factor: float = 0.5
sale_in: float = 0.5
sale_out: float = 0.8
rpm_per_key: int = 10
day_req_cap: int = 200
concurrent_per_key: int = 2
max_body_chars: int = 60000
semcache_enabled: bool = True
sim_threshold: float = 0.92
max_entries: int = 300000
promote_frequency: int = 5
# T-X10(采纳 cortiq 路由签名分桶):语义缓存键的路由签名粒度
# none | capabilities(默认:vision/tools 需求不同不互串) | model(按模型隔离)
route_sig_scope: str = "capabilities"
def bucket(self, name: str) -> BucketCfg:
"""取桶配置;未知名回落 defaultD-P2 缺省桶)。"""
if name in self.buckets:
return self.buckets[name]
return self.buckets.get("default") or BucketCfg(name="default")
def price(self, model: str) -> Optional[ModelPrice]:
"""取模型价格表;未配置返回 None(调用方应拒绝或用 default 档)。"""
return self.model_prices.get(model)
def _to_milli_per_m(yuan_per_m: float) -> int:
"""元/1M -> 毫元/1M(四舍五入取整,加载期唯一换算点)。"""
return int(round(float(yuan_per_m) * YUAN_TO_MILLI))
def _parse_hhmm(text: str, fallback: str) -> str:
"""校验 HH:MM 形态,非法回落默认。"""
t = str(text or fallback).strip()
parts = t.split(":")
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
h, m = int(parts[0]), int(parts[1])
if 0 <= h <= 23 and 0 <= m <= 59:
return f"{h:02d}:{m:02d}"
return fallback
def build_proxy_config(settings_dict: Dict[str, Any]) -> ProxyConfig:
"""从 settings.to_dict() 的 proxy 段构建 ProxyConfig(缺省值兜底)。
settings_dict 为整个设置 dict(含 worker/architect/... 与 proxy 段)。
"""
raw = settings_dict.get("proxy") or {}
if not isinstance(raw, dict):
raw = {}
# ---- 桶 ----
buckets: Dict[str, BucketCfg] = {}
raw_buckets = raw.get("buckets") or {}
if isinstance(raw_buckets, dict):
for name, b in raw_buckets.items():
if not isinstance(b, dict):
continue
buckets[str(name)] = BucketCfg(
name=str(name),
system_template=str(b.get("system_template") or "你是校园学习助手。"),
doc_prefix_file=(str(b["doc_prefix_file"])
if b.get("doc_prefix_file") else None),
doc_version=int(b.get("doc_version", 1) or 1),
ttl_hours=int(b.get("ttl_hours", 72) or 72),
)
if "default" not in buckets:
buckets["default"] = BucketCfg(name="default")
# ---- 价格表:元/1M 浮点 -> 毫元/1M 整数(D-P1 加载期换算) ----
model_prices: Dict[str, ModelPrice] = {}
raw_pricing = raw.get("pricing") or {}
if isinstance(raw_pricing, dict):
for model, p in raw_pricing.items():
if not isinstance(p, dict) or "in_miss" not in p:
continue # peak_window/offpeak_factor/sale_discount 非模型键
in_miss = _to_milli_per_m(p.get("in_miss", 0))
in_hit = _to_milli_per_m(p.get("in_hit", 0))
out = _to_milli_per_m(p.get("out", 0))
if in_miss <= 0:
continue
if in_hit <= 0:
in_hit = max(1, in_miss // 30) # 缺省 = in_miss × 1/30D-P3
model_prices[str(model)] = ModelPrice(
in_miss=in_miss, in_hit=in_hit, out=out)
# ---- 峰谷窗口 / 系数 ----
pw = raw_pricing.get("peak_window") or {} if isinstance(raw_pricing, dict) else {}
off = float(raw_pricing.get("offpeak_factor", 0.5)) if isinstance(raw_pricing, dict) else 0.5
sale = raw_pricing.get("sale_discount") or {} if isinstance(raw_pricing, dict) else {}
# ---- 限流 / 缓存 ----
limits = raw.get("limits") or {}
if not isinstance(limits, dict):
limits = {}
sem = raw.get("semcache") or {}
if not isinstance(sem, dict):
sem = {}
return ProxyConfig(
enabled=bool(raw.get("enabled", False)),
admin_key=str(raw.get("admin_key") or ""),
db_path=str(raw.get("db_path") or "data/proxy.sqlite3"),
buckets=buckets,
model_prices=model_prices,
peak_start=_parse_hhmm(pw.get("start"), "08:30") if isinstance(pw, dict) else "08:30",
peak_end=_parse_hhmm(pw.get("end"), "23:59") if isinstance(pw, dict) else "23:59",
offpeak_factor=max(0.0, min(1.0, off)),
sale_in=max(0.0, min(1.0, float(sale.get("in", 0.5)))),
sale_out=max(0.0, min(1.0, float(sale.get("out", 0.8)))),
rpm_per_key=int(limits.get("rpm_per_key", 10) or 10),
day_req_cap=int(limits.get("day_req_cap", 200) or 200),
concurrent_per_key=int(limits.get("concurrent_per_key", 2) or 2),
max_body_chars=int(limits.get("max_body_chars", 60000) or 60000),
semcache_enabled=bool(sem.get("enabled", True)),
sim_threshold=float(sem.get("sim_threshold", 0.92) or 0.92),
max_entries=int(sem.get("max_entries", 300000) or 300000),
promote_frequency=int(sem.get("promote_frequency", 5) or 5),
route_sig_scope=(str(sem.get("route_sig_scope"))
if str(sem.get("route_sig_scope")) in
("none", "capabilities", "model")
else "capabilities"),
)