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:
@@ -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 / 30(D-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()
|
||||
Reference in New Issue
Block a user