laya-ad / code /laya-server /backend /tests /test_ad_classifier.py
x6nux's picture
laya-ad v2.2: sync laya-server code (default version v2.2, build_push.sh, .env.example)
0cd599e verified
Raw History Blame Contribute Delete
7.38 kB
"""ad-clf 后端端到端测试:微型随机权重模型走完整 API 流程(不依赖真实权重/数据)。"""
from pathlib import Path
import pytest
from argon2 import PasswordHasher
from fastapi.testclient import TestClient
from server.config import Settings
from server.main import create_app
PASSWORD = "correct horse battery staple"
ORIGIN = {"Origin": "http://testserver"}
@pytest.fixture(scope="module")
def tiny_model_dir(tmp_path_factory):
"""微型 ModernBERT(2 层 × 32 hidden)+ 真实 tokenizer,落成 fp32 safetensors 目录。"""
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer, ModernBertConfig
d = tmp_path_factory.mktemp("tiny-model")
tok = AutoTokenizer.from_pretrained("bert-base-cased")
tok.save_pretrained(d)
cfg = ModernBertConfig(vocab_size=len(tok), hidden_size=32, intermediate_size=64,
num_hidden_layers=2, num_attention_heads=2,
max_position_embeddings=128,
pad_token_id=tok.pad_token_id or 0,
bos_token_id=tok.cls_token_id or 0,
eos_token_id=tok.sep_token_id or 0,
cls_token_id=tok.cls_token_id or 0,
mask_token_id=None)
model = AutoModelForSequenceClassification.from_config(cfg)
model.config.id2label = {0: "ad", 1: "normal"}
model.config.label2id = {"ad": 0, "normal": 1}
model.save_pretrained(d, safe_serialization=True)
(d / "PRECISION.txt").write_text("fp8", encoding="utf-8")
(d / "MODEL_ID.txt").write_text("laya-test-fp8", encoding="utf-8")
return d
def _settings(model_dir: Path, tmp_path: Path) -> Settings:
frontend = tmp_path / "frontend-dist"
frontend.mkdir(exist_ok=True)
return Settings("admin", PasswordHasher().hash(PASSWORD), tmp_path / "db.sqlite3",
1, Path(model_dir), None, frontend)
@pytest.fixture()
def client(client_factory, tiny_model_dir, tmp_path):
c = client_factory(create_app(_settings(tiny_model_dir, tmp_path)))
assert c.get("/health/ready").status_code == 200
assert c.post("/internal/auth/login", json={"username": "admin", "password": PASSWORD},
headers=ORIGIN).status_code == 200
csrf = c.get("/internal/auth/session").json()["csrf_token"]
key = c.post("/internal/api-keys", json={"name": "smoke"},
headers={**ORIGIN, "X-CSRF-Token": csrf}).json()["key"]
c.headers.update({"Authorization": f"Bearer {key}"})
return c
def test_models_list(client):
assert client.get("/internal/models").json() == {
"models": ["auto", "laya", "laya-test", "laya-test-fp8"]}
def test_laya_alias_accepted(client):
r = client.post("/v1/systemone", json={
"state": "hello",
"questions": {"q": {"type": "noul", "instructions": "这是 advertisement 广告吗?"}},
"model": "laya",
})
assert r.status_code == 200
assert r.json()["model"] == "laya-test-fp8"
def test_rejects_unbundled_models(client):
r = client.post("/v1/systemone", json={
"state": "hello",
"questions": {"q": {"type": "noul", "instructions": "urgent?"}},
"model": "multilingual",
})
assert r.status_code == 422
assert r.json()["detail"]["code"] == "MODEL_NOT_AVAILABLE"
def test_systemone_answers(client):
payload = {
"state": {"message": {"text": "加微信免费领取涨粉秘籍,名额有限!"}},
"questions": {
"is_ad": {"type": "choice", "instructions": "判断",
"criteria": {"clean": "正常内容", "ad": "广告"}},
"ad_kind": {"type": "choice", "instructions": "类别",
"criteria": {"promo": "推广", "scam": "诈骗"}},
"refund": {"type": "noul", "instructions": "Does the sender ask for a refund?"},
"is_ad_noul": {"type": "noul", "instructions": "这条消息是 advertisement 广告吗?"},
},
}
r = client.post("/v1/systemone", json=payload)
assert r.status_code == 200, r.text
body = r.json()
ans = body["answers"]
assert ans["is_ad"]["unsupported"] is False
assert ans["is_ad"]["choice"] in ("ad", "clean")
assert abs(sum(ans["is_ad"]["probabilities"].values()) - 1.0) < 0.01
# is_ad 的键序保持请求原序(clean 在前)
assert set(ans["is_ad"]["probabilities"]) == {"clean", "ad"}
assert ans["ad_kind"]["unsupported"] is True
assert ans["refund"]["unsupported"] is True and ans["refund"]["noul"] == 0.5
assert ans["is_ad_noul"]["unsupported"] is False
usage = body["usage"]
assert isinstance(usage["input_tokens"], int) and usage["input_tokens"] > 0
assert usage["output_tokens"] == len(payload["questions"])
assert body["model"] == "laya-test-fp8"
def test_state_string_and_variants(client):
r = client.post("/v1/systemone", json={
"state": "限时特惠 原价299今日99元 点击抢购",
"questions": {"q": {"type": "choice", "instructions": "判断",
"criteria": ["ad", "clean"]}},
})
assert r.status_code == 200
assert r.json()["answers"]["q"]["choice"] in ("ad", "clean")
def test_multi_precision_root(client_factory, tiny_model_dir, tmp_path):
"""multi 模式:根目录下多个精度子目录,按 model id 懒加载切换。"""
import shutil
root = tmp_path / "multi-root"
root.mkdir()
for mid, prec in (("laya-test-fp8", "fp8"), ("laya-test-int4", "int4")):
d = root / f"variant-{prec}"
d.mkdir()
for f in ("model.safetensors", "config.json", "tokenizer.json", "tokenizer_config.json"):
shutil.copy(tiny_model_dir / f, d / f)
(d / "PRECISION.txt").write_text(prec, encoding="utf-8")
(d / "MODEL_ID.txt").write_text(mid, encoding="utf-8")
c = client_factory(create_app(_settings(root, tmp_path)))
assert c.post("/internal/auth/login", json={"username": "admin", "password": PASSWORD},
headers=ORIGIN).status_code == 200
csrf = c.get("/internal/auth/session").json()["csrf_token"]
key = c.post("/internal/api-keys", json={"name": "multi"},
headers={**ORIGIN, "X-CSRF-Token": csrf}).json()["key"]
c.headers.update({"Authorization": f"Bearer {key}"})
assert c.get("/internal/models").json() == {
"models": ["auto", "laya", "laya-test", "laya-test-fp8", "laya-test-int4"]}
payload = {"state": "加微信免费领取涨粉秘籍,名额有限!",
"questions": {"is_ad": {"type": "choice", "instructions": "判断",
"criteria": {"ad": "广告", "clean": "正常"}}}}
r = c.post("/v1/systemone", json={**payload, "model": "laya-test-int4"})
assert r.status_code == 200
assert r.json()["model"] == "laya-test-int4"
r = c.post("/v1/systemone", json={**payload, "model": "laya"})
assert r.status_code == 200
assert r.json()["model"] == "laya-test-fp8" # laya 别名 → fp8 默认变体
r = c.post("/v1/systemone", json={**payload, "model": "laya-nonexist"})
assert r.status_code == 422