feat(sense): T-G1 Embedder(int8 量化/降级 fail-closed + /v1/embeddings 透传)

- embedder.py:embed() 调 llama-server /v1/embeddings -> 对称 per-vector
  int8 量化(scale=127/max|v|,1024 维余弦扰动 ~1e-4 << 1e-2 验收);
  超时/连接/非200/空形状 -> EmbedderDown(D-G4 fail-closed);
  quantize_int8/cosine_int8 辅助(int8 直接点积,scale 正标量不改方向)
- routes.py 拆双路由组:/sense 前缀组 + /v1 无前缀组(/v1/embeddings
  OpenAI 兼容透传,embedder 不可用 503);build_sense_routers 返回列表,
  api.py 逐个 include;build_sense_router 保留向后兼容
- 测试 +6:量化余弦误差 200 组 <1e-2/范围/零向量/int8 余弦/
  正常量化/三种 fail-closed/透传端点(503+形状+400),全量 381 passed
- 待真机项:llama-server embedder 端点冒烟(需用户配置 embedder.base_url)
This commit is contained in:
tzt
2026-09-05 13:33:58 +08:00
parent 7830f1936a
commit f783878c02
5 changed files with 286 additions and 17 deletions
+40 -9
View File
@@ -1,21 +1,52 @@
"""Sense 路由(T-G0 骨架:健康检查;/v1/route 与 /v1/embeddings 在 T-G1/G5 落地)。"""
"""Sense 路由(T-G1):/sense 前缀组 + /v1 无前缀组(embeddings 透传)。
/v1/routeT-G5 grader)后续加入无前缀组;/sense/admin/*T-G3+)加入前缀组。
"""
from __future__ import annotations
from fastapi import APIRouter
from typing import List
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from gateway.sense.config import SenseConfig
from gateway.sense.embedder import embed
from gateway.sense.errors import EmbedderDown
def build_sense_router(cfg: SenseConfig) -> APIRouter:
"""组装 sense 路由(唯一组装点)"""
router = APIRouter(prefix="/sense")
def build_sense_routers(cfg: SenseConfig) -> List[APIRouter]:
"""组装 sense 路由组:[/sense 前缀组, /v1 无前缀组]"""
main = APIRouter(prefix="/sense")
v1 = APIRouter()
@router.get("/health", tags=["sense"])
@main.get("/health", tags=["sense"])
async def health():
"""sense 面健康检查(含灰度状态,供看板/运维)。"""
return {"enabled": cfg.enabled, "mode": cfg.mode,
"embedder": cfg.embedder.base_url}
# /v1/routeT-G5 grader)、/v1/embeddingsT-G1)、/sense/admin/*T-G3+)后续追加。
# 注意 /v1/route 不带 /sense 前缀——由 api.py 单独 include 一个无前缀子路由(T-G5)。
return router
@v1.post("/v1/embeddings", tags=["sense"])
async def embeddings(request: Request):
"""OpenAI 兼容透传 embedder(客户端/代理共用)。
请求:{"model"?, "input": str};响应:{"object":"list","data":[{"object":
"embedding","index":0,"embedding":[int8...]}],"model":...}。
embedder 不可用 -> 503D-G4 fail-closed,客户端可感知降级)。
"""
try:
body = await request.json()
except Exception:
body = {}
text = str((body or {}).get("input") or "").strip()
if not text:
return JSONResponse({"error": {"message": "input 必填"}}, status_code=400)
model = str((body or {}).get("model") or cfg.embedder.model)
try:
vec = await embed(text, cfg.embedder)
except EmbedderDown as e:
return JSONResponse(
{"error": {"message": str(e), "type": "embedder_down"}}, status_code=503)
return {"object": "list", "model": model,
"data": [{"object": "embedding", "index": 0, "embedding": vec}]}
return [main, v1]