feat(proxy): 架构与算法优化二轮——sense 运行时单例激活 T-X3、打码密钥泄漏修复、账本热路径 2.4x
- gateway/proxy/routes.py:sense 运行时(SenseStore+Grader+晋升表)按配置单例化, 原每请求重建导致:每请求跑全量 DDL、重读工件文件,且 T-X3 60s 决策缓存随 Grader 丢弃、命中率恒为 0(本轮最大收益,激活既有已测组件); T1 审计抽样修复原 NameError×3 被 except 吞掉(load_config/get_review/request_id 均未定义、§9.4 从未入队)——request_id 上提、review 队列经 review_getter 注入 (api.py 反向依赖解除)、sample_rate 走 load_config - gateway/model_pool.py:新增 usable_entries() 未打码内通道;routes 的降级链/ 预算降档/能力位重定向/档位映射四处改走该通道——修复打码 api_key 流入上游 Bearer 头导致带密钥条目重定向必 401 的隐性缺陷(HTTP 管理面仍用打码 list()) - gateway/proxy/ledger.py + billing.py:共享持久连接(原每操作新建,全链路 每请求 3-5 次 connect)+ 日重置 UPDATE 每实例每日一次短路(原每请求全表扫描 抢写锁);check_and_count A/B 0.75→0.31 ms/op(2.4x);新增 usage_stats() SQL 聚合,/admin/stats 从 list_usage(limit=50 万) Python 四遍扫描改为下推聚合 (微基准 ~28x,随流水线性扩大) - gateway/sense/grader.py:决策留痕 insert_decision 移入 asyncio.to_thread (原同步 sqlite 写直接跑在事件循环线程,高并发阻塞网关;与观察写批量队列同等保护) - 全量 474 项两轮复核全绿(基线 474)
This commit is contained in:
+79
-36
@@ -46,11 +46,14 @@ def install_error_handlers(app) -> None:
|
||||
headers={"WWW-Authenticate": "Bearer"} if exc.status_code == 401 else None)
|
||||
|
||||
|
||||
def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRouter:
|
||||
def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None,
|
||||
review_getter=None) -> APIRouter:
|
||||
"""组装代理面路由(唯一组装点)。
|
||||
|
||||
settings_provider:返回设置对象的回调(api.py 注入 settings_store)。
|
||||
未注入时语义分析器分流不启用(None 安全,既有测试不受影响)。
|
||||
review_getter:返回人工检验队列的回调(api.py 注入 get_review);
|
||||
未注入时 T1 审计抽样跳过(不报错)。
|
||||
"""
|
||||
router = APIRouter(prefix="/proxy")
|
||||
ledger = __import__("gateway.proxy.ledger", fromlist=["Ledger"]).Ledger.init_db(cfg.db_path)
|
||||
@@ -87,19 +90,16 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
|
||||
# 语义分析器 live 分流(D-G7:mode=live 才启用;任何异常不影响代理可用性)。
|
||||
# T-X4 修复:settings_provider 由 api.py 注入(原实现引用未导入的
|
||||
# settings_store,NameError 被 except 吞掉,分流从未实际生效)。
|
||||
# 2026-09 优化:sense 运行时按配置单例化(原每请求重建 SenseStore+Grader:
|
||||
# 每请求跑全量 DDL、重读工件文件,且 T-X3 决策缓存随 Grader 丢弃、命中率恒 0)。
|
||||
tier_used = None
|
||||
request_id = "px" + uuid.uuid4().hex[:12]
|
||||
if settings_provider is not None:
|
||||
try:
|
||||
from gateway.sense.config import build_sense_config
|
||||
from gateway.sense.grader import Grader
|
||||
from gateway.sense.observer import get_observer
|
||||
from gateway.sense.promotion import PromotionTable
|
||||
from gateway.sense.store import SenseStore
|
||||
scfg = build_sense_config(settings_provider().to_dict())
|
||||
if scfg.enabled and scfg.mode == "live":
|
||||
sstore = SenseStore.init_db(scfg.db_path)
|
||||
grader = Grader(scfg, sstore, get_observer(sstore),
|
||||
promotion=PromotionTable(sstore))
|
||||
_sstore, grader = _get_sense_runtime(scfg)
|
||||
qtext = "\n".join(str(m.get("content") or "")
|
||||
for m in (body.get("messages") or []))
|
||||
d = await grader.decide(qtext or str(body.get("model") or ""),
|
||||
@@ -113,17 +113,22 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
|
||||
except Exception:
|
||||
pass # D-G4:分级故障不影响代理可用性
|
||||
try:
|
||||
resp = await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool)
|
||||
resp = await _run_chat(body, dict(request.headers), ctx, cfg, ledger, pool,
|
||||
request_id=request_id)
|
||||
if tier_used:
|
||||
resp.headers["x-campus-tier"] = tier_used
|
||||
# T1 审计抽样(§9.4):按 review.sample_rate 入队人工核;
|
||||
# promo 标签供人工 verdict 回填晋升表(T-X5)
|
||||
# promo 标签供人工 verdict 回填晋升表(T-X5)。
|
||||
# 2026-09 修复:原块引用未定义的 load_config/get_review/request_id,
|
||||
# 三个 NameError 被 except 吞掉,抽样从未入队;review 队列现由
|
||||
# api.py 经 review_getter 注入(避免 proxy -> api 反向依赖)。
|
||||
try:
|
||||
import random as _random
|
||||
from router_system.config import load_config
|
||||
from gateway.sense.promotion import promotion_label
|
||||
rate = float(load_config().get("review", {}).get("sample_rate", 0.1))
|
||||
if _random.random() < rate:
|
||||
get_review().enqueue(
|
||||
if review_getter is not None and _random.random() < rate:
|
||||
review_getter().enqueue(
|
||||
request_id + "-sense", str(body.get("model") or "proxy"),
|
||||
"(sense T1 审计抽样)",
|
||||
tags=["sense_t1", "promo:" + promotion_label("proxy", "")],
|
||||
@@ -136,7 +141,7 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
|
||||
|
||||
def _entry_by_tier_hint(pool, hint: str) -> Optional[Dict[str, Any]]:
|
||||
"""按档位名选池条目(tier_hint 优先,缺省按 tier 字段映射)。"""
|
||||
entries = pool.list().get("entries", [])
|
||||
entries = _pool_entries(pool)
|
||||
for e in entries:
|
||||
if not e.get("enabled") or e.get("backend") == "mock":
|
||||
continue
|
||||
@@ -191,32 +196,27 @@ def build_proxy_router(cfg: ProxyConfig, pool, settings_provider=None) -> APIRou
|
||||
|
||||
@router.get("/admin/stats", tags=["proxy-admin"])
|
||||
async def admin_stats(request: Request, since: int = 0):
|
||||
"""命中率-毛利看板(§5.2 口径):h_g/h_p/revenue/cost/margin/by_bucket。"""
|
||||
"""命中率-毛利看板(§5.2 口径):h_g/h_p/revenue/cost/margin/by_bucket。
|
||||
|
||||
2026-09 优化:SQL 聚合下推(原 list_usage(limit=500000) 全量拉回
|
||||
Python 扫 4 遍,随流水线性劣化);响应字段与四舍五入口径不变。
|
||||
"""
|
||||
_guard_admin(request)
|
||||
from gateway.proxy.ledgerutil import _today
|
||||
import time as _t
|
||||
now = _t.time()
|
||||
today = _today(now)
|
||||
# list_usage 返回全列(含 stats 所需字段),复用既有参数化查询
|
||||
rows = ledger.list_usage(limit=500000)
|
||||
n = len(rows)
|
||||
cached = sum(1 for r in rows if r["gateway_cached"])
|
||||
s = ledger.usage_stats()
|
||||
n = s["requests"]
|
||||
cached = s["gateway_cached"]
|
||||
h_g = cached / n if n else 0.0
|
||||
in_hit = sum(r["in_hit_tok"] for r in rows)
|
||||
in_miss = sum(r["in_miss_tok"] for r in rows)
|
||||
in_hit = s["in_hit_tok"]
|
||||
in_miss = s["in_miss_tok"]
|
||||
h_p = in_hit / (in_hit + in_miss) if (in_hit + in_miss) else 0.0
|
||||
revenue = sum(r["charged_milli"] for r in rows)
|
||||
cost = sum(r["upstream_cost_milli"] for r in rows)
|
||||
by_bucket: Dict[str, Dict[str, int]] = {}
|
||||
for r in rows:
|
||||
b = by_bucket.setdefault(r["bucket"], {"requests": 0, "revenue_milli": 0,
|
||||
"cost_milli": 0})
|
||||
b["requests"] += 1
|
||||
b["revenue_milli"] += r["charged_milli"]
|
||||
b["cost_milli"] += r["upstream_cost_milli"]
|
||||
return {"requests": n, "h_g": round(h_g, 4), "h_p": round(h_p, 4),
|
||||
"revenue_milli": revenue, "cost_milli": cost,
|
||||
"margin_milli": revenue - cost, "by_bucket": by_bucket,
|
||||
"revenue_milli": s["revenue_milli"], "cost_milli": s["cost_milli"],
|
||||
"margin_milli": s["revenue_milli"] - s["cost_milli"],
|
||||
"by_bucket": s["by_bucket"],
|
||||
"today": today,
|
||||
"upstream_failover": upstream_failover_stats()}
|
||||
|
||||
@@ -271,6 +271,15 @@ def _limits():
|
||||
return _AUTH_SINGLETON_LIMITS
|
||||
|
||||
|
||||
def _pool_entries(pool) -> List[Dict[str, Any]]:
|
||||
"""池条目快照(转发链内部专用):优先未打码通道 usable_entries,
|
||||
回退打码 list()(兼容测试注入的简化 pool)。HTTP 响应一律用 list()。"""
|
||||
fn = getattr(pool, "usable_entries", None)
|
||||
if fn is not None:
|
||||
return fn()
|
||||
return pool.list().get("entries", [])
|
||||
|
||||
|
||||
def _estimate_hold_milli(body: dict, cfg: ProxyConfig, model: str) -> int:
|
||||
"""预扣估算(宁可高估,D-P11):in 按字符/3,out 按 min(max_tokens,4096)。
|
||||
|
||||
@@ -352,7 +361,7 @@ def _fallback_chain(pool, entry: Dict[str, Any], max_total: int = 3) -> List[Dic
|
||||
"""
|
||||
from gateway.model_pool import TIERS
|
||||
tier_rank = {t: i for i, t in enumerate(TIERS)}
|
||||
rest = [e for e in pool.list().get("entries", [])
|
||||
rest = [e for e in _pool_entries(pool)
|
||||
if (e.get("enabled") and e.get("id") != entry.get("id")
|
||||
and e.get("backend") not in ("mock",) and e.get("base_url")
|
||||
and e.get("model") != entry.get("model"))]
|
||||
@@ -383,7 +392,7 @@ def _downgrade_entry(pool, entry: Dict[str, Any], mode: str) -> Optional[Dict[st
|
||||
if target < 0:
|
||||
return None
|
||||
want = TIERS[target]
|
||||
for e in pool.list().get("entries", []):
|
||||
for e in _pool_entries(pool):
|
||||
if (e.get("enabled") and e.get("id") != entry.get("id")
|
||||
and e.get("tier") == want
|
||||
and e.get("backend") not in ("mock",) and e.get("base_url")):
|
||||
@@ -413,6 +422,38 @@ def reset_semcache_instances() -> None:
|
||||
_semcache_instances = {}
|
||||
|
||||
|
||||
_sense_runtimes: Dict[tuple, Any] = {}
|
||||
|
||||
|
||||
def _get_sense_runtime(scfg):
|
||||
"""sense 运行时按配置单例化(2026-09 优化,激活 T-X3 决策缓存)。
|
||||
|
||||
键取 SenseConfig 的完整 repr:任何设置变更都会派生新键并重建运行时,
|
||||
旧实例留在注册表中随设置维度有界增长(单进程部署,D-P9)。
|
||||
返回 (sstore, grader):Grader 持有的线性头/阈值/60s 决策缓存跨请求复用,
|
||||
免去每请求的 SenseStore DDL 与工件文件重读。
|
||||
"""
|
||||
key = ("sense", repr(scfg))
|
||||
rt = _sense_runtimes.get(key)
|
||||
if rt is None:
|
||||
from gateway.sense.grader import Grader
|
||||
from gateway.sense.observer import get_observer
|
||||
from gateway.sense.promotion import PromotionTable
|
||||
from gateway.sense.store import SenseStore
|
||||
sstore = SenseStore.init_db(scfg.db_path)
|
||||
grader = Grader(scfg, sstore, get_observer(sstore),
|
||||
promotion=PromotionTable(sstore))
|
||||
rt = (sstore, grader)
|
||||
_sense_runtimes[key] = rt
|
||||
return rt
|
||||
|
||||
|
||||
def reset_sense_runtimes() -> None:
|
||||
"""测试用:清空 sense 运行时单例。"""
|
||||
global _sense_runtimes
|
||||
_sense_runtimes = {}
|
||||
|
||||
|
||||
def _cached_charge(body: dict, cfg: ProxyConfig, model: str) -> Dict[str, int]:
|
||||
"""缓存命中计费:成本 0,按未命中口径对入/出估 token 收售价(§7)。"""
|
||||
from gateway.proxy.pricing import compute
|
||||
@@ -423,14 +464,16 @@ def _cached_charge(body: dict, cfg: ProxyConfig, model: str) -> Dict[str, int]:
|
||||
|
||||
|
||||
async def _run_chat(body: dict, headers: Dict[str, str], ctx: Dict[str, Any],
|
||||
cfg: ProxyConfig, ledger, pool):
|
||||
cfg: ProxyConfig, ledger, pool,
|
||||
request_id: Optional[str] = None):
|
||||
model = str(body.get("model") or "")
|
||||
entry = _resolve_entry(pool, model, cfg)
|
||||
if entry is None:
|
||||
return JSONResponse({"error": {"message": f"模型不可用: {model}",
|
||||
"type": "invalid_request_error"}},
|
||||
status_code=400)
|
||||
request_id = "px" + uuid.uuid4().hex[:12]
|
||||
if request_id is None:
|
||||
request_id = "px" + uuid.uuid4().hex[:12]
|
||||
ts = time.time()
|
||||
client_wants_usage = bool(body.get("stream_options", {}).get("include_usage")) \
|
||||
if isinstance(body.get("stream_options"), dict) else False
|
||||
@@ -441,7 +484,7 @@ async def _run_chat(body: dict, headers: Dict[str, str], ctx: Dict[str, Any],
|
||||
if not _entry_meets(entry, needs):
|
||||
from gateway.model_pool import filter_by_capabilities
|
||||
cap_entries = filter_by_capabilities(
|
||||
[e for e in pool.list().get("entries", [])
|
||||
[e for e in _pool_entries(pool)
|
||||
if e.get("enabled") and e.get("backend") not in ("mock",)
|
||||
and e.get("base_url")],
|
||||
need_vision=needs["vision"], need_tools=needs["tools"],
|
||||
|
||||
Reference in New Issue
Block a user