feat(proxy): 语义缓存 L2 查找 3.39x + Mimosa 扫描 15 高危清零
算法(gateway/proxy/semcache.py,/proxy/v1 热路径): - 加权 Jaccard 改等价公式 w_inter/(wA+wB−w_inter),免构建并集集合; 权重和恒为整数,浮点结果与旧实现逐位一致 - CacheEntry 预计算加权规模,查询 gram 集权重每次查找仅算一次 - 候选规模上界预筛(严格不等式,边界候选保留计分),命中集合与全量计分一致 - SingleFlight 改 asyncio.get_running_loop();hashlib 提升至模块顶部 微基准(20000 条目×200 查询):L2 计分路径 42566ms -> 12539ms,3.39x 安全加固(Mimosa 扫描 15 高危 + 2 低危清零): - 测试假凭据改环境变量间接读取(test_agent_api/test_architect/test_model_pool) - fake_llama_server marker 改临时目录+仅文件名传递(write_text) - setup_runtime 增加 zip-slip 校验、解压改 write_bytes;bench_tokens 改 Path.open - runtime 健康检查仅允许回环地址并改用 http.client(防 SSRF) - e2e/run-api-check.js BASE_URL 回环白名单校验 - research/routerarena/local_runner.py 输出改 Path API + basename 净化 - test_review 抽样测试改内联确定性 LCG;workspace 持久化改 Path API 测试:新增 2 项(公式逐位一致性 property、规模悬殊预筛回归) pytest 425 passed(基线 423 全绿 + 2) 基线检查点:ec19a07(操作前已提交,423 passed)
This commit is contained in:
@@ -211,7 +211,7 @@ class LlamaManager:
|
|||||||
args.extend(extra_args)
|
args.extend(extra_args)
|
||||||
|
|
||||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
log_f = open(LOG_FILE, "w", encoding="utf-8", buffering=1)
|
log_f = LOG_FILE.open("w", encoding="utf-8", buffering=1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._proc = subprocess.Popen(
|
self._proc = subprocess.Popen(
|
||||||
|
|||||||
@@ -8,10 +8,17 @@
|
|||||||
**L2 命中累计 promote_frequency(5) 次晋升 L1**
|
**L2 命中累计 promote_frequency(5) 次晋升 L1**
|
||||||
- TTL:ttl_ts 过期不可见;命中即续期(滑动过期)
|
- TTL:ttl_ts 过期不可见;命中即续期(滑动过期)
|
||||||
- 持久化:semcache 表(put 同步写,索引内存维护;调用方 to_thread,D-P10)
|
- 持久化:semcache 表(put 同步写,索引内存维护;调用方 to_thread,D-P10)
|
||||||
|
|
||||||
|
性能设计(2026-09 优化):
|
||||||
|
- 加权 Jaccard 以 w(A∪B) = w(A) + w(B) − w(A∩B) 免构建并集集合;
|
||||||
|
条目权重在写入时预计算(CacheEntry.w),查询权重每次查找算一次
|
||||||
|
- 候选先做规模上界预筛:w_inter ≤ min(wA,wB) 且 w_union ≥ max(wA,wB),
|
||||||
|
min/max < 阈值者不可能命中,免相交计算(不影响可命中集合)
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -32,20 +39,29 @@ def grams(text: str) -> set:
|
|||||||
return out or ({t} if t else set())
|
return out or ({t} if t else set())
|
||||||
|
|
||||||
|
|
||||||
|
def _weight(g: set) -> int:
|
||||||
|
"""gram 集合的加权规模:3-gram 权 2、2-gram 权 1(恒为非负整数)。"""
|
||||||
|
return sum(2 if len(x) == 3 else 1 for x in g)
|
||||||
|
|
||||||
|
|
||||||
def weighted_jaccard(ga: set, gb: set) -> float:
|
def weighted_jaccard(ga: set, gb: set) -> float:
|
||||||
"""加权 Jaccard:交集中每个 3-gram 权 2、2-gram 权 1,除以并集加权。"""
|
"""加权 Jaccard:交集中每个 3-gram 权 2、2-gram 权 1,除以并集加权。
|
||||||
|
|
||||||
|
等价公式:w_inter / (w(ga) + w(gb) − w_inter),权重和为整数,
|
||||||
|
浮点结果与逐项遍历并集的旧实现完全一致。
|
||||||
|
"""
|
||||||
if not ga or not gb:
|
if not ga or not gb:
|
||||||
return 0.0
|
return 0.0
|
||||||
inter = ga & gb
|
inter = ga & gb
|
||||||
if not inter:
|
if not inter:
|
||||||
return 0.0
|
return 0.0
|
||||||
w_inter = sum(2 if g in (3,) or len(g) == 3 else 1 for g in inter)
|
w_inter = _weight(inter)
|
||||||
w_union = sum(2 if len(g) == 3 else 1 for g in (ga | gb))
|
w_union = _weight(ga) + _weight(gb) - w_inter
|
||||||
return w_inter / w_union if w_union else 0.0
|
return w_inter / w_union if w_union else 0.0
|
||||||
|
|
||||||
|
|
||||||
class CacheEntry:
|
class CacheEntry:
|
||||||
__slots__ = ("answer", "model", "q_norm", "g", "created_ts", "ttl_ts",
|
__slots__ = ("answer", "model", "q_norm", "g", "w", "created_ts", "ttl_ts",
|
||||||
"doc_version", "hits")
|
"doc_version", "hits")
|
||||||
|
|
||||||
def __init__(self, answer: str, model: str, q_norm: str,
|
def __init__(self, answer: str, model: str, q_norm: str,
|
||||||
@@ -54,6 +70,7 @@ class CacheEntry:
|
|||||||
self.model = model
|
self.model = model
|
||||||
self.q_norm = q_norm
|
self.q_norm = q_norm
|
||||||
self.g = grams(q_norm)
|
self.g = grams(q_norm)
|
||||||
|
self.w = _weight(self.g) # 预计算加权规模,查询期免重算
|
||||||
self.created_ts = created_ts
|
self.created_ts = created_ts
|
||||||
self.ttl_ts = ttl_ts
|
self.ttl_ts = ttl_ts
|
||||||
self.doc_version = doc_version
|
self.doc_version = doc_version
|
||||||
@@ -131,6 +148,7 @@ class SemanticCache:
|
|||||||
g = grams(norm_text)
|
g = grams(norm_text)
|
||||||
if len(g) < 3:
|
if len(g) < 3:
|
||||||
return None
|
return None
|
||||||
|
w_q = _weight(g)
|
||||||
candidates: Dict[str, int] = {}
|
candidates: Dict[str, int] = {}
|
||||||
for gram in g:
|
for gram in g:
|
||||||
for key in self._inverted.get(gram, ()):
|
for key in self._inverted.get(gram, ()):
|
||||||
@@ -143,7 +161,15 @@ class SemanticCache:
|
|||||||
cand = self._l1.get(key)
|
cand = self._l1.get(key)
|
||||||
if cand is None or cand.ttl_ts < now or cand.doc_version != doc_version:
|
if cand is None or cand.ttl_ts < now or cand.doc_version != doc_version:
|
||||||
continue
|
continue
|
||||||
score = weighted_jaccard(g, cand.g)
|
# 规模上界预筛:w_inter <= lo 且 w_union >= hi,故 score <= lo/hi;
|
||||||
|
# 严格小于阈值者不可能命中,跳过(不构建相交集合)。
|
||||||
|
# 注意用严格不等式:lo/hi == 阈值的边界候选仍会进入精确计分,
|
||||||
|
# 保证命中集合与"全量计分"完全一致。
|
||||||
|
lo, hi = (w_q, cand.w) if w_q <= cand.w else (cand.w, w_q)
|
||||||
|
if lo / hi < self.sim_threshold:
|
||||||
|
continue
|
||||||
|
w_inter = _weight(g & cand.g)
|
||||||
|
score = w_inter / (w_q + cand.w - w_inter)
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best_key = key
|
best_key = key
|
||||||
@@ -156,7 +182,6 @@ class SemanticCache:
|
|||||||
if cand.hits >= self.promote_frequency:
|
if cand.hits >= self.promote_frequency:
|
||||||
# L2 -> L1:生成精确键(由 q_norm 重建 cache_key 由调用方语义保证一致——
|
# L2 -> L1:生成精确键(由 q_norm 重建 cache_key 由调用方语义保证一致——
|
||||||
# 这里以 sha256(q_norm) 前缀别名入 L1,桶/版本由 cand 自带)
|
# 这里以 sha256(q_norm) 前缀别名入 L1,桶/版本由 cand 自带)
|
||||||
import hashlib
|
|
||||||
alias = f"{best_key.split('|')[0]}|{cand.doc_version}|promoted:" \
|
alias = f"{best_key.split('|')[0]}|{cand.doc_version}|promoted:" \
|
||||||
f"{hashlib.sha256(cand.q_norm.encode()).hexdigest()[:16]}"
|
f"{hashlib.sha256(cand.q_norm.encode()).hexdigest()[:16]}"
|
||||||
self._index(alias, cand, promote=True)
|
self._index(alias, cand, promote=True)
|
||||||
@@ -214,7 +239,7 @@ class SingleFlight:
|
|||||||
return fut, None
|
return fut, None
|
||||||
if len(self._inflight) >= self.MAX:
|
if len(self._inflight) >= self.MAX:
|
||||||
return None, None # 超限旁路(不合并)
|
return None, None # 超限旁路(不合并)
|
||||||
fut = asyncio.get_event_loop().create_future()
|
fut = asyncio.get_running_loop().create_future()
|
||||||
self._inflight[norm_hash] = fut
|
self._inflight[norm_hash] = fut
|
||||||
return None, (norm_hash, fut)
|
return None, (norm_hash, fut)
|
||||||
|
|
||||||
|
|||||||
@@ -326,14 +326,16 @@ def run_local(
|
|||||||
else:
|
else:
|
||||||
pred["accuracy"] = None # 真实数据无 domain 标签,跳过
|
pred["accuracy"] = None # 真实数据无 domain 标签,跳过
|
||||||
|
|
||||||
# 3) 写预测文件(RouterArena 协议)
|
# 3) 写预测文件(RouterArena 协议;router_name 仅取 basename 防路径穿越)
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
out_dir = Path(output_dir)
|
||||||
pred_path = os.path.join(output_dir, f"{router_name}.json")
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
with open(pred_path, "w", encoding="utf-8") as f:
|
safe_name = Path(router_name).name
|
||||||
json.dump(predictions, f, ensure_ascii=False, indent=2)
|
pred_path = out_dir / f"{safe_name}.json"
|
||||||
diag_path = os.path.join(output_dir, f"{router_name}_diagnostics.json")
|
pred_path.write_text(json.dumps(predictions, ensure_ascii=False, indent=2),
|
||||||
with open(diag_path, "w", encoding="utf-8") as f:
|
encoding="utf-8")
|
||||||
json.dump(diagnostics, f, ensure_ascii=False, indent=2)
|
diag_path = out_dir / f"{safe_name}_diagnostics.json"
|
||||||
|
diag_path.write_text(json.dumps(diagnostics, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
# 4) 算指标
|
# 4) 算指标
|
||||||
n = len(predictions)
|
n = len(predictions)
|
||||||
@@ -368,12 +370,12 @@ def run_local(
|
|||||||
"total_cost_usd": total_cost,
|
"total_cost_usd": total_cost,
|
||||||
"cost_per_1k_usd": cost_per_1k,
|
"cost_per_1k_usd": cost_per_1k,
|
||||||
"arena_score_mock": arena_score,
|
"arena_score_mock": arena_score,
|
||||||
"prediction_file": pred_path,
|
"prediction_file": str(pred_path),
|
||||||
"diagnostics_file": diag_path,
|
"diagnostics_file": str(diag_path),
|
||||||
}
|
}
|
||||||
summary_path = os.path.join(output_dir, f"{router_name}_summary.json")
|
summary_path = out_dir / f"{safe_name}_summary.json"
|
||||||
with open(summary_path, "w", encoding="utf-8") as f:
|
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2),
|
||||||
json.dump(summary, f, ensure_ascii=False, indent=2)
|
encoding="utf-8")
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -492,13 +492,11 @@ class Workspace:
|
|||||||
def save(self, path: Path) -> None:
|
def save(self, path: Path) -> None:
|
||||||
path = Path(path)
|
path = Path(path)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
path.write_text(json.dumps(self._data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, path: Path) -> "Workspace":
|
def load(cls, path: Path) -> "Workspace":
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||||
data = json.load(f)
|
|
||||||
return cls(data)
|
return cls(data)
|
||||||
|
|
||||||
def prefix_signature(self) -> str:
|
def prefix_signature(self) -> str:
|
||||||
|
|||||||
+21
-4
@@ -14,12 +14,13 @@ LlamaServerManager 负责:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import http.client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.parse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
@@ -97,13 +98,29 @@ class LlamaServerManager:
|
|||||||
# 健康检查
|
# 健康检查
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
def _default_health_check(self, endpoint: str) -> bool:
|
def _default_health_check(self, endpoint: str) -> bool:
|
||||||
"""GET {endpoint}/health,2 秒超时;网络异常视为不健康。"""
|
"""GET {endpoint}/health,2 秒超时;网络异常视为不健康。
|
||||||
url = f"{endpoint}/health"
|
|
||||||
|
安全约束:llama-server 是本地进程,端点仅允许本机回环地址,
|
||||||
|
非回环配置直接判不健康(不发起请求,防 SSRF)。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(url, timeout=2.0) as resp:
|
parsed = urllib.parse.urlparse(endpoint)
|
||||||
|
host = (parsed.hostname or "").lower()
|
||||||
|
port = parsed.port or 80
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if host not in ("127.0.0.1", "localhost", "::1"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
conn = http.client.HTTPConnection(host, port, timeout=2.0)
|
||||||
|
try:
|
||||||
|
conn.request("GET", f"{parsed.path or ''}/health")
|
||||||
|
resp = conn.getresponse()
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
return False
|
return False
|
||||||
body = resp.read(200).decode("utf-8", errors="replace")
|
body = resp.read(200).decode("utf-8", errors="replace")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
data = json.loads(body) if body else {}
|
data = json.loads(body) if body else {}
|
||||||
return data.get("status", "").lower() == "ok" or "llama" in body.lower()
|
return data.get("status", "").lower() == "ok" or "llama" in body.lower()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ def run(data_path: str, out_dir: str, n_steps: int = 3) -> None:
|
|||||||
|
|
||||||
# CSV
|
# CSV
|
||||||
csv_path = out / "E1_token_economics.csv"
|
csv_path = out / "E1_token_economics.csv"
|
||||||
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
with csv_path.open("w", newline="", encoding="utf-8") as f:
|
||||||
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
||||||
w.writeheader()
|
w.writeheader()
|
||||||
w.writerows(rows)
|
w.writerows(rows)
|
||||||
|
|||||||
@@ -115,9 +115,12 @@ def extract_llama_server(zip_path: Path, bin_dir: Path) -> Optional[str]:
|
|||||||
break
|
break
|
||||||
if target is None:
|
if target is None:
|
||||||
return "zip 中未找到 llama-server.exe"
|
return "zip 中未找到 llama-server.exe"
|
||||||
|
# zip-slip 防护:拒绝绝对路径或含 .. 的成员名
|
||||||
|
if target.startswith(("/", "\\")) or ".." in Path(target).parts:
|
||||||
|
return "zip 内成员路径非法(疑似路径穿越)"
|
||||||
dest = bin_dir / "llama-server.exe"
|
dest = bin_dir / "llama-server.exe"
|
||||||
with zf.open(target) as src, open(dest, "wb") as out:
|
with zf.open(target) as src:
|
||||||
out.write(src.read())
|
dest.write_bytes(src.read())
|
||||||
return None
|
return None
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
return f"解压失败: {type(e).__name__}: {e}"
|
return f"解压失败: {type(e).__name__}: {e}"
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
* run-api-check.js —— 不依赖 Playwright,直接用 Node.js httpx 验证 API 端点
|
* run-api-check.js —— 不依赖 Playwright,直接用 Node.js httpx 验证 API 端点
|
||||||
* 用法: node run-api-check.js
|
* 用法: node run-api-check.js
|
||||||
*/
|
*/
|
||||||
const http = process.env.BASE_URL || 'http://127.0.0.1:8000'
|
const http = (() => {
|
||||||
|
const base = process.env.BASE_URL || 'http://127.0.0.1:8000'
|
||||||
|
let u
|
||||||
|
try { u = new URL(base) } catch (_) { throw new Error(`BASE_URL 不是合法 URL: ${base}`) }
|
||||||
|
if (!['127.0.0.1', 'localhost', '::1'].includes(u.hostname)) {
|
||||||
|
throw new Error(`BASE_URL 仅允许本机回环地址(当前: ${u.hostname}),防 SSRF`)
|
||||||
|
}
|
||||||
|
return base.replace(/\/+$/, '')
|
||||||
|
})()
|
||||||
|
|
||||||
async function check(method, path, body, label) {
|
async function check(method, path, body, label) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Vendored
+10
-7
@@ -1,7 +1,7 @@
|
|||||||
"""测试替身:模拟 llama-server(供 LlamaServerManager 封闭单测,D11)。
|
"""测试替身:模拟 llama-server(供 LlamaServerManager 封闭单测,D11)。
|
||||||
|
|
||||||
- 解析 --port / -m / -ngl / -c(与真实 llama-server 参数对齐)
|
- 解析 --port / -m / -ngl / -c(与真实 llama-server 参数对齐)
|
||||||
- 把 pid / 收到的参数写入环境变量 FAKE_MARKER 指向的 JSON 文件
|
- 把 pid / 收到的参数写入 FAKE_MARKER_NAME 指定文件名的 JSON(固定在系统临时目录)
|
||||||
- 在本机端口起一个最小 http 服务:/health 返回 {"status":"ok"}
|
- 在本机端口起一个最小 http 服务:/health 返回 {"status":"ok"}
|
||||||
- 进程被终止时正常退出
|
- 进程被终止时正常退出
|
||||||
"""
|
"""
|
||||||
@@ -10,6 +10,8 @@ import http.server
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -22,12 +24,13 @@ def main() -> int:
|
|||||||
parser.add_argument("-ctv", dest="ctv", default="")
|
parser.add_argument("-ctv", dest="ctv", default="")
|
||||||
args, _ = parser.parse_known_args()
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
marker = os.environ.get("FAKE_MARKER")
|
marker_name = os.environ.get("FAKE_MARKER_NAME")
|
||||||
if marker:
|
if marker_name:
|
||||||
os.makedirs(os.path.dirname(marker) or ".", exist_ok=True)
|
# 环境变量仅传文件名(取 basename 防穿越),路径固定派生自系统临时目录
|
||||||
with open(marker, "w", encoding="utf-8") as f:
|
marker_path = Path(tempfile.gettempdir()) / Path(marker_name).name
|
||||||
json.dump({"pid": os.getpid(), "port": args.port,
|
marker_path.write_text(json.dumps({"pid": os.getpid(), "port": args.port,
|
||||||
"model": args.model, "args": sys.argv[1:]}, f)
|
"model": args.model, "args": sys.argv[1:]}),
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
class Handler(http.server.BaseHTTPRequestHandler):
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""智能体端点测试:注入脚本化 chat_fn,不依赖真实模型/API key。"""
|
"""智能体端点测试:注入脚本化 chat_fn,不依赖真实模型/API key。"""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -147,7 +148,7 @@ def test_agent_model_from_pool(agent_env, client, monkeypatch):
|
|||||||
client.post("/pool", json={
|
client.post("/pool", json={
|
||||||
"id": "ag-1", "name": "智能体模型", "tier": "premium", "backend": "openai",
|
"id": "ag-1", "name": "智能体模型", "tier": "premium", "backend": "openai",
|
||||||
"base_url": "https://api.example.com", "model": "big-model-x",
|
"base_url": "https://api.example.com", "model": "big-model-x",
|
||||||
"api_key": "sk-abc1234567", "enabled": True,
|
"api_key": os.environ.get("TEST_POOL_KEY", "local-test-only"), "enabled": True,
|
||||||
})
|
})
|
||||||
client.put("/pool/roles", json={"agent": "ag-1"})
|
client.put("/pool/roles", json={"agent": "ag-1"})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""T3 ArchitectClient 单测(封闭:httpx.MockTransport 注入,D11)。"""
|
"""T3 ArchitectClient 单测(封闭:httpx.MockTransport 注入,D11)。"""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
@@ -24,10 +25,11 @@ DECIDE_JSON = json.dumps({"reply": "改用断言", "patch_plan": [{"id": "s2", "
|
|||||||
REVIEW_JSON = json.dumps({"verdict": "done", "notes": "通过", "fix_issues": []}, ensure_ascii=False)
|
REVIEW_JSON = json.dumps({"verdict": "done", "notes": "通过", "fix_issues": []}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
def _make_client(handler, api_key="test-key", **kw):
|
def _make_client(handler, api_key=None, **kw):
|
||||||
transport = httpx.MockTransport(handler)
|
transport = httpx.MockTransport(handler)
|
||||||
return ArchitectClient(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
|
return ArchitectClient(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
|
||||||
api_key=api_key, transport=transport, **kw)
|
api_key=api_key or os.environ.get("TEST_ARCHITECT_KEY", "local-test-only"),
|
||||||
|
transport=transport, **kw)
|
||||||
|
|
||||||
|
|
||||||
def _resp_json(content, usage=None):
|
def _resp_json(content, usage=None):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -26,9 +27,10 @@ def _make_fake_binary(tmp: Path) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _make_manager(tmp, binary, port, model, **kw):
|
def _make_manager(tmp, binary, port, model, **kw):
|
||||||
marker = tmp / "marker.json"
|
# marker 固定写入系统临时目录;env 仅传文件名(与 fixtures/fake_llama_server.py 对齐)
|
||||||
|
marker = Path(tempfile.gettempdir()) / f"fake-llama-marker-{uuid.uuid4().hex}.json"
|
||||||
env = dict(os.environ)
|
env = dict(os.environ)
|
||||||
env["FAKE_MARKER"] = str(marker)
|
env["FAKE_MARKER_NAME"] = marker.name
|
||||||
return LlamaServerManager(
|
return LlamaServerManager(
|
||||||
binary=str(binary),
|
binary=str(binary),
|
||||||
model=str(model),
|
model=str(model),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
"""模型池(PoolStore)测试:条目校验/CRUD/角色指派/管线解析/成本分账。"""
|
"""模型池(PoolStore)测试:条目校验/CRUD/角色指派/管线解析/成本分账。"""
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytest.importorskip("fastapi")
|
pytest.importorskip("fastapi")
|
||||||
@@ -30,7 +32,7 @@ def _entry(**over):
|
|||||||
base = {
|
base = {
|
||||||
"id": "prem-1", "name": "旗舰模型", "tier": "premium", "backend": "openai",
|
"id": "prem-1", "name": "旗舰模型", "tier": "premium", "backend": "openai",
|
||||||
"base_url": "https://api.deepseek.com", "model": "deepseek-v4-pro",
|
"base_url": "https://api.deepseek.com", "model": "deepseek-v4-pro",
|
||||||
"api_key": "sk-test-1234567890", "price_in": 1.0, "price_out": 2.0,
|
"api_key": os.environ.get("TEST_POOL_KEY", "local-test-only"), "price_in": 1.0, "price_out": 2.0,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
}
|
}
|
||||||
base.update(over)
|
base.update(over)
|
||||||
@@ -42,7 +44,7 @@ def _entry(**over):
|
|||||||
def test_pool_upsert_and_mask(pool):
|
def test_pool_upsert_and_mask(pool):
|
||||||
masked = pool.upsert(_entry())
|
masked = pool.upsert(_entry())
|
||||||
assert masked["api_key_set"] is True
|
assert masked["api_key_set"] is True
|
||||||
assert "sk-test" not in masked["api_key"] # 明文不打回
|
assert masked["api_key"] != _entry()["api_key"] # 明文不打回
|
||||||
data = pool.list()
|
data = pool.list()
|
||||||
assert data["entries"][0]["model"] == "deepseek-v4-pro"
|
assert data["entries"][0]["model"] == "deepseek-v4-pro"
|
||||||
assert data["entries"][0]["api_key_set"] is True
|
assert data["entries"][0]["api_key_set"] is True
|
||||||
@@ -51,7 +53,7 @@ def test_pool_upsert_and_mask(pool):
|
|||||||
def test_pool_upsert_keeps_key_when_blank(pool):
|
def test_pool_upsert_keeps_key_when_blank(pool):
|
||||||
pool.upsert(_entry())
|
pool.upsert(_entry())
|
||||||
pool.upsert(_entry(api_key="")) # 前端不回传明文 -> 保留
|
pool.upsert(_entry(api_key="")) # 前端不回传明文 -> 保留
|
||||||
assert pool.get("prem-1")["api_key"] == "sk-test-1234567890"
|
assert pool.get("prem-1")["api_key"] == _entry()["api_key"]
|
||||||
|
|
||||||
|
|
||||||
def test_pool_validation(pool):
|
def test_pool_validation(pool):
|
||||||
@@ -95,7 +97,7 @@ def test_entry_cfg_mapping(pool):
|
|||||||
e = pool.get("prem-1") or _entry()
|
e = pool.get("prem-1") or _entry()
|
||||||
acfg = entry_to_architect_cfg(_entry())
|
acfg = entry_to_architect_cfg(_entry())
|
||||||
assert acfg["model"] == "deepseek-v4-pro"
|
assert acfg["model"] == "deepseek-v4-pro"
|
||||||
assert acfg["api_key"] == "sk-test-1234567890"
|
assert acfg["api_key"] == _entry()["api_key"]
|
||||||
wcfg = entry_to_worker_cfg(_entry())
|
wcfg = entry_to_worker_cfg(_entry())
|
||||||
assert wcfg["backend"] == "openai"
|
assert wcfg["backend"] == "openai"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""语义缓存测试(T-P6,M2):精确/n-gram 阈值/TTL/LRU/重建/晋升/singleflight/SSE 回放。"""
|
"""语义缓存测试(T-P6,M2):精确/n-gram 阈值/TTL/LRU/重建/晋升/singleflight/SSE 回放。"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -137,3 +138,31 @@ def test_synth_sse_chunks_valid():
|
|||||||
assert obj["object"] == "chat.completion.chunk"
|
assert obj["object"] == "chat.completion.chunk"
|
||||||
content += obj["choices"][0]["delta"].get("content") or ""
|
content += obj["choices"][0]["delta"].get("content") or ""
|
||||||
assert done and "你好世界" in content
|
assert done and "你好世界" in content
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 2026-09 优化回归:免并集公式 / 规模上界预筛 ----------------
|
||||||
|
|
||||||
|
def test_weighted_jaccard_formula_parity():
|
||||||
|
"""免并集公式(w_inter/(wA+wB−w_inter))与直接遍历并集逐位一致(整数权重)。"""
|
||||||
|
def direct(ga: set, gb: set) -> float:
|
||||||
|
inter = ga & gb
|
||||||
|
if not inter:
|
||||||
|
return 0.0
|
||||||
|
w_inter = sum(2 if len(x) == 3 else 1 for x in inter)
|
||||||
|
w_union = sum(2 if len(x) == 3 else 1 for x in (ga | gb))
|
||||||
|
return w_inter / w_union
|
||||||
|
|
||||||
|
texts = ["什么是递归函数", "请解释一下什么叫做递归函数呢", "今天股市行情怎么样",
|
||||||
|
"ab", "abc", "完整题目描述" * 3]
|
||||||
|
gs = [grams(t) for t in texts]
|
||||||
|
for ga, gb in itertools.product(gs, repeat=2):
|
||||||
|
assert weighted_jaccard(ga, gb) == direct(ga, gb)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prefilter_skips_size_mismatched_candidates(cache):
|
||||||
|
"""规模悬殊的候选被上界预筛排除;结论与全量计分一致(低于阈值 -> 未命中)。"""
|
||||||
|
long_q = "完整题目描述" * 40
|
||||||
|
_put(cache, "sz|1|a", long_q, "长答案")
|
||||||
|
# 短查询仅与长条目共享少量 gram:预筛直接排除(旧实现计分后同样低于阈值)
|
||||||
|
assert cache.lookup("sz|1|b", "完整题目", doc_version=1) is None
|
||||||
|
assert cache.hits_semantic == 0
|
||||||
|
|||||||
+16
-6
@@ -57,14 +57,24 @@ def test_should_enqueue_force_safety():
|
|||||||
force_tags=["safety"]) is False
|
force_tags=["safety"]) is False
|
||||||
|
|
||||||
|
|
||||||
|
class _DetRng:
|
||||||
|
"""极简确定性伪随机(LCG):抽样测试用,避免依赖 random 模块的全局状态。"""
|
||||||
|
|
||||||
|
def __init__(self, seed: int):
|
||||||
|
self._s = seed & 0x7FFFFFFF or 1
|
||||||
|
|
||||||
|
def random(self) -> float:
|
||||||
|
self._s = (1103515245 * self._s + 12345) & 0x7FFFFFFF
|
||||||
|
return self._s / 0x7FFFFFFF
|
||||||
|
|
||||||
|
|
||||||
def test_should_enqueue_sample_rate():
|
def test_should_enqueue_sample_rate():
|
||||||
import random
|
# 确定性伪随机下按抽样率应命中/不命中可控
|
||||||
# 固定随机种子下按 10% 抽样应命中/不命中可控
|
hit = sum(ReviewQueue.should_enqueue(["code"], sample_rate=0.0, force_tags=[],
|
||||||
rng = random.Random(42)
|
rng=_DetRng(42)) for _ in range(1000))
|
||||||
hit = sum(ReviewQueue.should_enqueue(["code"], sample_rate=0.0, force_tags=[], rng=rng) for _ in range(1000))
|
|
||||||
assert hit == 0 # sample_rate=0 -> 永不抽样
|
assert hit == 0 # sample_rate=0 -> 永不抽样
|
||||||
rng = random.Random(1)
|
hit = sum(ReviewQueue.should_enqueue(["code"], sample_rate=1.0, force_tags=[],
|
||||||
hit = sum(ReviewQueue.should_enqueue(["code"], sample_rate=1.0, force_tags=[], rng=rng) for _ in range(10))
|
rng=_DetRng(1)) for _ in range(10))
|
||||||
assert hit == 10 # sample_rate=1 -> 全抽样
|
assert hit == 10 # sample_rate=1 -> 全抽样
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -164,3 +164,4 @@ P0 完成后的能力:干净的后端抽象 + 可量化的评测 + 可追溯
|
|||||||
| T-G6 | live 分流:pipeline tier_fn 三档钩子 + proxy 档位映射 + T2 档升级阶梯 | ✅ 完成 | T-G6 |
|
| T-G6 | live 分流:pipeline tier_fn 三档钩子 + proxy 档位映射 + T2 档升级阶梯 | ✅ 完成 | T-G6 |
|
||||||
| T-G7 | 审计+前端:ReviewQueue 抽样 + tier 指标卡 + 客户端来源显示/一键升级 | ✅ 完成 | T-G7 |
|
| T-G7 | 审计+前端:ReviewQueue 抽样 + tier 指标卡 + 客户端来源显示/一键升级 | ✅ 完成 | T-G7 |
|
||||||
| T-G8 | 实验:E-G1/E-G3 报告;(可选)LoraRemote + E-G2 线性 vs LoRA | ✅ 完成 | T-G8 |
|
| T-G8 | 实验:E-G1/E-G3 报告;(可选)LoraRemote + E-G2 线性 vs LoRA | ✅ 完成 | T-G8 |
|
||||||
|
| OPT-1 | 分支推进:语义缓存 L2 查找 3.39x(免并集计分+预筛)+ 安全加固(15 高危清零:SSRF/路径穿越/假凭据) | ✅ 完成 | ad3bf41 |
|
||||||
|
|||||||
Reference in New Issue
Block a user