From 51d1e4ccdd0909a7cab577b0ab53abfa466e5ec6 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sat, 5 Sep 2026 23:08:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(llm):=20=5Fpick=5Fcandidate=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E6=84=9F=E7=9F=A5=E9=80=89=E7=AB=AF=E7=82=B9(404?= =?UTF-8?q?=E6=A0=B9=E5=9B=A0)=E2=80=94=E2=80=94=E6=8C=89=E6=A8=A1?= =?UTF-8?q?=E5=9E=8Bprofile.protocol=E8=BF=87=E6=BB=A4=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E6=A0=87=E7=AD=BE,=E4=B8=8D=E5=8C=B9=E9=85=8D=E6=8A=A5?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E5=8F=AF=E8=A1=8C=E5=8A=A8=E9=94=99=E8=AF=AF?= =?UTF-8?q?;=E6=97=A0=E6=A0=87=E7=AD=BE=E7=AB=AF=E7=82=B9=E8=A7=86?= =?UTF-8?q?=E4=B8=BA=E9=80=9A=E7=94=A8(=E5=90=91=E5=90=8E=E5=85=BC?= =?UTF-8?q?=E5=AE=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_llm/gateway.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/pipeline_llm/gateway.py b/pipeline_llm/gateway.py index c1a6d2c..b0114d5 100644 --- a/pipeline_llm/gateway.py +++ b/pipeline_llm/gateway.py @@ -260,8 +260,19 @@ def _filter_by_pref(candidates_with_ep, pref: str): return matched + rest +async def _model_protocol(sor, model: dict) -> str: + """模型适配模板的协议(2026-09-05 端点协议感知选择用)。查不到返回空串。""" + pid = (model.get('profile_id') or '').strip() + if not pid: + return '' + recs = await sor.sqlExe( + "SELECT protocol FROM llm_api_profile WHERE id=${i}$ LIMIT 1", {"i": pid}) + await sor.sqlExe("COMMIT", {}) + return (getattr(recs[0], 'protocol', '') or '').strip() if recs else '' + + async def _pick_candidate(sor, model: dict, pref: str): - """构建候选并选一个:区域偏好过滤 → 冷却避让 → 余额加权。 + """构建候选并选一个:协议过滤 → 区域偏好过滤 → 冷却避让 → 余额加权。 返回 (ok, result):ok 时 result=(account_dict, endpoint_dict);否则 result=错误信息。 """ @@ -274,6 +285,27 @@ async def _pick_candidate(sor, model: dict, pref: str): accounts = await _account_candidates(sor, model) if not accounts: return False, '模型「%s」的供应商下无启用账号(或账号余额状态异常)' % model.get('name', '') + # 协议感知(2026-09-05 404 教训):模型模板协议与端点标签匹配才可用。 + # 端点无标签(历史数据)视为通用——同供应商混布 compatible-mode/api/v1 + # 两类端点时,无标签过滤会把原生异步模型拼到 openai_compat 前缀下 404。 + mproto = await _model_protocol(sor, model) + if mproto: + tagged = [(i, e) for i, e in enumerate(endpoints) if (e.get('protocol') or '').strip()] + if tagged: + matched = {i for i, e in tagged if (e.get('protocol') or '').strip() == mproto} + untagged = {i for i, e in enumerate(endpoints) + if not (e.get('protocol') or '').strip()} + usable = matched | untagged + if not matched: + return False, ('供应商端点均为其他协议(模型模板协议 %s,端点协议 %s)——' + '原生异步(openai_compat 之外)与兼容模式端点 base_url 不同,' + '混用必 404。请在供应商端点目录添加协议 %s 的端点' + % (mproto, sorted({(e.get('protocol') or '') for _, e in tagged}), + mproto)) + else: + usable = set(range(len(endpoints))) + else: + usable = set(range(len(endpoints))) # 展开 (账号, 端点) 对:账号选用的端点下标 pairs = [] for acc in accounts: @@ -285,10 +317,11 @@ async def _pick_candidate(sor, model: dict, pref: str): i = int(i) except (TypeError, ValueError): continue - if 0 <= i < len(endpoints): + if i in usable: pairs.append((acc, endpoints[i], i)) if not pairs: - return False, '所有账号都未选用端点(模型治理→供应商账号→选用端点),请配置' + return False, ('所有账号都未选用可用端点(模型治理→供应商账号→选用端点)。' + '模型模板协议 %s——请把该协议端点加入账号的选用端点' % (mproto or '未知')) pairs = _filter_by_pref(pairs, pref) if not pairs: return False, ('端点偏好为「仅 %s」,但没有可用的该区域端点。按合规要求不跨端点降级——'