feat(proxy): T-X6 模型池能力位过滤——vision/tools/context 硬过滤(采纳 cortiq capabilities)
- model_pool:条目新增 capabilities{vision,tools,context_window},
归一化缺省全兼容(老条目行为零变化;非法形态回落缺省);
filter_by_capabilities 硬过滤(context_window=0 视为不限)
- proxy routes:_request_needs 从请求体推断需求(多模态 image_url → vision、
tools 非空 → tools、上下文需求 = 字符/3 + min(max_tokens,4096) 与预扣同口径);
条目不满足时在池内重定向到首个合格条目
- 与 T-X1/T-X2 组合语义:能力重定向 → 预算降档 → 降级链,逐级独立不互扰
pytest 461 passed(T-X3 后 455 + 6)
This commit is contained in:
@@ -274,6 +274,41 @@ def _budget_headers(budget_mode: str) -> Dict[str, str]:
|
||||
return {"X-Budget-Mode": budget_mode} if budget_mode and budget_mode != "normal" else {}
|
||||
|
||||
|
||||
def _request_needs(body: dict) -> Dict[str, Any]:
|
||||
"""从请求体推断能力需求(T-X6):多模态图片 -> vision;带 tools -> tools;
|
||||
上下文需求 = 提示字符/3 + min(max_tokens, 4096)(与预扣估算同口径)。"""
|
||||
vision = False
|
||||
for m in body.get("messages") or []:
|
||||
content = m.get("content")
|
||||
if isinstance(content, list):
|
||||
for part in content:
|
||||
if isinstance(part, dict) and ("image_url" in part
|
||||
or part.get("type") == "image_url"):
|
||||
vision = True
|
||||
prompt_chars = sum(len(str(m.get("content") or ""))
|
||||
if not isinstance(m.get("content"), list)
|
||||
else sum(len(str(p.get("text") or "")) for p in m["content"]
|
||||
if isinstance(p, dict))
|
||||
for m in (body.get("messages") or []))
|
||||
est_out = min(int(body.get("max_tokens") or 1024), 4096)
|
||||
return {"vision": vision,
|
||||
"tools": bool(body.get("tools")),
|
||||
"min_context_tokens": prompt_chars // 3 + est_out}
|
||||
|
||||
|
||||
def _entry_meets(entry: Dict[str, Any], needs: Dict[str, Any]) -> bool:
|
||||
"""条目能力位是否满足请求需求(capabilities 缺省全兼容)。"""
|
||||
cap = entry.get("capabilities") or {}
|
||||
if needs["vision"] and not cap.get("vision", True):
|
||||
return False
|
||||
if needs["tools"] and not cap.get("tools", True):
|
||||
return False
|
||||
ctx = int(cap.get("context_window") or 0)
|
||||
if needs["min_context_tokens"] > 0 and 0 < ctx < needs["min_context_tokens"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _fallback_chain(pool, entry: Dict[str, Any], max_total: int = 3) -> List[Dict[str, Any]]:
|
||||
"""上游有序降级链(T-X2,采纳 cortiq tier 链思路)。
|
||||
|
||||
@@ -367,6 +402,21 @@ async def _run_chat(body: dict, headers: Dict[str, str], ctx: Dict[str, Any],
|
||||
if isinstance(body.get("stream_options"), dict) else False
|
||||
is_stream = bool(body.get("stream"))
|
||||
|
||||
# ---- 能力位硬过滤(T-X6):请求需要 vision/tools/上下文而条目不满足时重定向 ----
|
||||
needs = _request_needs(body)
|
||||
if not _entry_meets(entry, needs):
|
||||
from gateway.model_pool import filter_by_capabilities
|
||||
cap_entries = filter_by_capabilities(
|
||||
[e for e in pool.list().get("entries", [])
|
||||
if e.get("enabled") and e.get("backend") not in ("mock",)
|
||||
and e.get("base_url")],
|
||||
need_vision=needs["vision"], need_tools=needs["tools"],
|
||||
min_context_tokens=needs["min_context_tokens"])
|
||||
if cap_entries:
|
||||
entry = cap_entries[0]
|
||||
model = str(entry.get("model") or model)
|
||||
body = {**body, "model": model}
|
||||
|
||||
# ---- 预算四档(T-X1):接近日上限渐进降档;>100% 仍由 try_hold 硬拒 ----
|
||||
budget_mode = "normal"
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user