fix(gateway): 同能力备链铁律——显式指定模型不可用时禁止跨能力静默顺延(2026-09-07 qwen-image-plus实测:指定t2i模型无key账号→旧逻辑补t2t备链→静默返回deepseek文字还报ok:true,要图得文当成功);_model_chain指定模型只补同能力备链+owner容错链同能力过滤,无同能力替代则报可行动错误(违反禁止静默回退铁律,且毒害invoke_model自动选型)
This commit is contained in:
parent
681b4c46b7
commit
5a1a567720
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user