"""Sense 路由(T-G1):/sense 前缀组 + /v1 无前缀组(embeddings 透传)。 /v1/route(T-G5 grader)后续加入无前缀组;/sense/admin/*(T-G3+)加入前缀组。 """ from __future__ import annotations 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_routers(cfg: SenseConfig) -> List[APIRouter]: """组装 sense 路由组:[/sense 前缀组, /v1 无前缀组]。""" main = APIRouter(prefix="/sense") v1 = APIRouter() @main.get("/health", tags=["sense"]) async def health(): """sense 面健康检查(含灰度状态,供看板/运维)。""" return {"enabled": cfg.enabled, "mode": cfg.mode, "embedder": cfg.embedder.base_url} @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 不可用 -> 503(D-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]