feat(proxy): T-P0 代理层骨架(包结构/DDL/门控挂载/单进程校验)

- gateway/proxy/ 十文件:config(ProxyConfig:元->毫元加载期换算 D-P1、差异化售价、
  桶/峰谷/限流/semcache 配置)/ errors(8 类 HTTP 语义异常)/ ledger(四表两索引 DDL,
  WAL,ReviewQueue 连接纪律)/ auth·pricing·normalizer·semcache·upstream(§6 签名占位)
  / routes(/proxy/v1/models OpenAI 形状)/ __init__(build_proxy_router 组装点)
- settings DEFAULTS 增 proxy 段(enabled 默认 False,D-P7)
- api.py 首次 include_router(enabled 门控 + 装配失败不拖垮主应用)
- serve.py workers>1 拒绝启动(D-P9:WEB_CONCURRENCY/UVICORN_WORKERS 校验)
- 测试 +4(门控 404/DDL 幂等/毫元换算/池模型列表),全量 322 passed
This commit is contained in:
tzt
2026-09-05 08:35:31 +08:00
parent 3cc9851623
commit b109576707
15 changed files with 580 additions and 1 deletions
+87
View File
@@ -0,0 +1,87 @@
"""代理层骨架测试(T-P0):enabled 门控 / DDL / 独立挂载的 models 端点。"""
import pytest
pytest.importorskip("fastapi")
from fastapi import FastAPI
from fastapi.testclient import TestClient
import gateway.api as ga
from gateway.proxy.config import ProxyConfig, build_proxy_config
from gateway.proxy.ledger import Ledger
def test_proxy_disabled_by_default_404():
"""D-P7:默认 enabled=False -> 全局 app 不注册任何 /proxy 路由。"""
client = TestClient(ga.app)
assert client.get("/proxy/v1/models").status_code == 404
assert client.post("/proxy/v1/chat/completions", json={}).status_code == 404
assert client.get("/proxy/admin/stats").status_code == 404
def test_ddl_creates_tables_and_indexes(tmp_path):
"""四表 + 两索引幂等创建(WAL)。"""
led = Ledger.init_db(tmp_path / "proxy.sqlite3")
tables = set(led.table_names())
for t in ("students", "proxy_keys", "usage_ledger", "semcache"):
assert t in tables
idx = set(led.index_names())
assert "idx_semcache_bucket" in idx
assert "idx_ledger_ts" in idx
# 幂等重建
led2 = Ledger.init_db(tmp_path / "proxy.sqlite3")
assert "semcache" in set(led2.table_names())
def test_build_proxy_config_defaults_and_milli_conversion():
"""缺省值兜底 + 元/1M -> 毫元/1M 加载期换算(D-P1)。"""
from gateway.settings import DEFAULTS
cfg = build_proxy_config(DEFAULTS) # DEFAULTS 自带 deepseek-chat 价格示例
assert cfg.enabled is False
assert "default" in cfg.buckets
assert cfg.sale_in == 0.5 and cfg.sale_out == 0.8 # 差异化定价
# 自带 DEFAULTS 的 deepseek-chat 价格:3.0 元 -> 3000 毫元
p = cfg.price("deepseek-chat")
assert p is not None
assert p.in_miss == 3000
assert p.in_hit == 100 # 0.1 元 -> 100 毫元
assert p.out == 9000
# in_hit 缺省 = in_miss / 30D-P3
cfg2 = build_proxy_config({"proxy": {
"pricing": {"some-model": {"in_miss": 3.0, "out": 9.0}}}})
p2 = cfg2.price("some-model")
assert p2.in_hit == 100 # 3000 // 30
# 桶解析回落 default
assert cfg.bucket("ghost").name == "default"
def test_enabled_router_lists_pool_models(tmp_path, monkeypatch):
"""enabled=True 独立挂载:/proxy/v1/models 返回池内非 mock 模型(OpenAI 形状)。"""
import gateway.model_pool as mp
from gateway.model_pool import PoolStore
mp.reset_pool()
monkeypatch.setattr(mp, "_store", PoolStore(path=tmp_path / "pool.json"))
mp.get_pool().upsert({
"id": "up1", "name": "云端", "tier": "budget", "backend": "openai",
"base_url": "https://api.example.com", "model": "deepseek-chat",
"price_in": 0.1, "price_out": 0.1, "enabled": True})
mp.get_pool().upsert({
"id": "mk", "name": "模拟", "tier": "local", "backend": "mock",
"model": "mock", "enabled": True}) # mock 不应出现
cfg = build_proxy_config({"proxy": {"enabled": True,
"db_path": str(tmp_path / "p.sqlite3")}})
app = FastAPI()
app.include_router(__import__("gateway.proxy", fromlist=["x"]).build_proxy_router(
cfg, mp.get_pool()))
client = TestClient(app)
r = client.get("/proxy/v1/models")
assert r.status_code == 200
data = r.json()
assert data["object"] == "list"
ids = [m["id"] for m in data["data"]]
assert ids == ["deepseek-chat"]
assert data["data"][0]["owned_by"] == "campus-proxy"
mp.reset_pool()