fix(gateway): 同能力备链铁律——显式指定模型不可用时禁止跨能力静默顺延(2026-09-07 qwen-image-plus实测:指定t2i模型无key账号→旧逻辑补t2t备链→静默返回deepseek文字还报ok:true,要图得文当成功);_model_chain指定模型只补同能力备链+owner容错链同能力过滤,无同能力替代则报可行动错误(违反禁止静默回退铁律,且毒害invoke_model自动选型)

This commit is contained in:
yumoqing 2026-09-07 18:38:25 +08:00
parent 681b4c46b7
commit 5a1a567720

View File

@ -169,24 +169,47 @@ async def _org_policy(sor, org_id: str):
async def _model_chain(sor, org_id: str, model_name: str, purpose: str = ''):
"""解析模型链 [主, 备...],返回 llm_model 记录 dict 列表。
- 调用方指定 model_name llm_model 找到 [] + 策略备链补充找不到 走旧表兜底
- 调用方指定 model_name llm_model 找到 [] + **同能力**备链补充
找不到 走旧表兜底
- 未指定策略主模型 + 备链无策略
- purpose='utility'辅助任务分类/选择/摘要 = [辅助模型] + +
显式指定 model_name 时忽略 purpose显式优先未配辅助模型时等同普通链
辅助模型不可用账号/端点全挂 govern_resolve 的逐模型尝试顺延到主模型
与主备容错同一机制无新增降级路径
同能力备链铁律2026-09-07 qwen-image-plus 实测根因显式指定 t2i
模型不可用时旧逻辑把策略备链清一色 t2t 对话模型补进链里 静默
顺延到 deepseek 返回**文字**还报 ok:true要图得文当成功违反
治理真实失败禁止静默回退且直接毒害 invoke_model 自动选型
选了生图模型却拿到文本跨能力替换永远非法同能力替代
另一个文生图模型才是合法容错指定模型无同能力替代时链只剩
它自己挑不到候选 govern_resolve 报可行动错误消息带最后原因
"""
chain_ids = []
primary, utility, backups, pref = await _org_policy(sor, org_id)
if model_name:
recs = await sor.sqlExe(
"SELECT id FROM llm_model WHERE status='active' AND (name=${n}$ OR vendor_model_id=${n}$) LIMIT 1",
"SELECT id, capability FROM llm_model WHERE status='active' "
"AND (name=${n}$ OR vendor_model_id=${n}$) LIMIT 1",
{"n": model_name})
await sor.sqlExe("COMMIT", {})
if not recs:
return [], pref
chain_ids.append(getattr(recs[0], 'id', ''))
chain_ids.extend([b for b in backups if b not in chain_ids])
want_cap = (getattr(recs[0], 'capability', '') or 't2t').strip().lower()
if backups:
# sqlor IN 列表必须展开占位符(传 list 会崩)
ph = ','.join('${b%d}$' % i for i in range(len(backups)))
bparams = {'b%d' % i: b for i, b in enumerate(backups)}
bparams['cap'] = want_cap
brecs = await sor.sqlExe(
"SELECT id FROM llm_model WHERE status='active' AND id IN (%s) "
"AND COALESCE(NULLIF(capability,''),'t2t')=${cap}$" % ph, bparams)
await sor.sqlExe("COMMIT", {})
for br in (brecs or []):
bid = getattr(br, 'id', '')
if bid and bid not in chain_ids:
chain_ids.append(bid)
else:
if purpose == 'utility' and utility:
chain_ids.append(utility)
@ -514,9 +537,15 @@ async def govern_resolve(org_id: str, user_id: str = '', model_name: str = '',
logger.info("pipeline_llm: 模型 %s 候选不可用(%s),尝试链内下一模型", m.get('name'), cand)
# 本机构链全不可用 → 容错落 owner 链
if chosen is None and policy_org != '0':
models0, pref0 = await _model_chain(sor, '0', '', purpose)
models0, pref0 = await _model_chain(sor, '0', model_name or '', purpose)
# owner 容错链同样过归属校验(策略里可能配了他机构注册的模型)
models0 = [m for m in models0 if _owner_allowed(m.get('org_id'), org_id)]
# 同能力铁律2026-09-07显式指定模型时 owner 容错链也只许
# 同能力替代——要图给文的静默回退在任何一层都不合法。
if model_name and models0:
_want = (models0[0].get('capability') or 't2t').strip().lower()
models0 = [m for m in models0
if (m.get('capability') or 't2t').strip().lower() == _want]
for m in models0:
ok, cand = await _pick_candidate(sor, m, pref0)
if ok and isinstance(cand, tuple):