diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index f01d94d..f126a28 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -499,7 +499,13 @@ async def _h_apply_llm_config(sor, params, ctx): from appPublic.uniqueID import getID - # 1. 供应商:按名称复用 + # 1. 供应商:按名称复用。端点带协议标签(2026-09-05 404 实测教训): + # 同一供应商可同时有 openai_compat(compatible-mode/v1) 与原生异步(api/v1) + # 两类端点,账号绑定/运行时选择必须按协议匹配,否则原生模型被拼到 + # compatible-mode 前缀下 → 404。另拦截「裸域退化」:提取 LLM 偶发把 + # base_url 提成 https://host(丢了 /api/v1),与已有带路径端点同 host + # 时该裸域无意义,跳过不入目录。 + from urllib.parse import urlparse as _urlparse recs = await sor.sqlExe( "SELECT id, endpoints FROM llm_vendor WHERE name=${n}$", {"n": vendor_name}) await sor.sqlExe("COMMIT", {}) @@ -512,29 +518,55 @@ async def _h_apply_llm_config(sor, params, ctx): eps_old = [] merged = list(eps_old) added_eps = [] + skipped_eps = [] for ep in endpoints: bu = (ep.get("base_url") or "").rstrip("/") - if bu and bu not in [(e.get("base_url") or "").rstrip("/") for e in merged]: - merged.append({"base_url": bu, "region": ep.get("region") or "domestic", - "timeout": int(_f(ep.get("timeout"), 60))}) - added_eps.append(bu) - # 2026-09-05:供应商表不再有 protocol——请求形态由模型挂的适配模板决定 + if not bu: + continue + # 同 base_url 已存在:只补协议标签(旧端点无 protocol 时回填) + hit = None + for e in merged: + if (e.get("base_url") or "").rstrip("/") == bu: + hit = e + break + if hit is not None: + if protocol and not hit.get("protocol"): + hit["protocol"] = protocol + continue + # 裸域退化拦截:同 host 已有带路径端点 → 裸域跳过 + pu = _urlparse(bu) + if not pu.path.strip("/"): + same_host = any(_urlparse((e.get("base_url") or "")).netloc == pu.netloc + for e in merged) + if same_host: + skipped_eps.append("%s(裸域退化:同 host 已有带路径端点)" % bu) + continue + merged.append({"base_url": bu, "region": ep.get("region") or "domestic", + "timeout": int(_f(ep.get("timeout"), 60)), + "protocol": protocol}) + added_eps.append(bu) await sor.sqlExe( "UPDATE llm_vendor SET endpoints=${e}$, updated_at=NOW() WHERE id=${i}$", {"e": json.dumps(merged, ensure_ascii=False), "i": vendor_id}) await sor.sqlExe("COMMIT", {}) - vendor_action = "复用供应商 %s(新增端点 %d 个)" % (vendor_id, len(added_eps)) + vendor_action = "复用供应商 %s(新增端点 %d 个%s)" % ( + vendor_id, len(added_eps), + (",跳过 %s" % ";".join(skipped_eps)) if skipped_eps else "") + vendor_endpoints = merged else: vendor_id = getID() eps_norm = [{"base_url": (ep.get("base_url") or "").rstrip("/"), "region": ep.get("region") or "domestic", - "timeout": int(_f(ep.get("timeout"), 60))} for ep in endpoints] + "timeout": int(_f(ep.get("timeout"), 60)), + "protocol": protocol} for ep in endpoints + if (ep.get("base_url") or "").strip()] await sor.C("llm_vendor", { "id": vendor_id, "name": vendor_name, "endpoints": json.dumps(eps_norm, ensure_ascii=False), "description": spec.get("doc_notes", "") or "", "status": "active", "org_id": ctx.get("org_id", "") or "0"}) vendor_action = "新建供应商 %s(%s)" % (vendor_id, vendor_name) + vendor_endpoints = eps_norm # 2. 适配模板:按 (协议×能力) 复用;旧骨架模板(硬编码 xxx_file/__from_doc__, # 2026-09-05 实测 t2v 因它渲染崩)视为不可用,按文档示例自愈重建 @@ -649,6 +681,32 @@ async def _h_apply_llm_config(sor, params, ctx): if protocol not in RUNTIME_PROTOCOLS: rt_note = ("⚠️ 协议「%s」的适配模板已存档,但当前运行时调用链仅支持 %s——" "该供应商模型暂不可被产线直接调用" % (protocol, "/".join(RUNTIME_PROTOCOLS))) + # 4. 账号端点绑定自检(2026-09-05 404 教训):账号只绑 compatible-mode + # 端点时,原生协议模型运行时会被选到错误端点 → 404。自动补绑本协议 + # 端点到该供应商全部活跃账号(不删已有绑定),如实报告。 + account_notes = [] + proto_idx = [i for i, e in enumerate(vendor_endpoints or []) + if (e.get("protocol") or "") == protocol] + if proto_idx: + recs = await sor.sqlExe( + "SELECT id, name, endpoint_ids FROM llm_account " + "WHERE vendor_id=${v}$ AND status='active'", {"v": vendor_id}) + await sor.sqlExe("COMMIT", {}) + for acc in recs or []: + try: + bound = [int(b) for b in json.loads(getattr(acc, "endpoint_ids", "") or "[]")] + except Exception: + bound = [] + if any(i in proto_idx for i in bound): + continue # 已绑本协议端点,可用 + newb = sorted(set(bound) | set(proto_idx)) + await sor.sqlExe( + "UPDATE llm_account SET endpoint_ids=${e}$, updated_at=NOW() WHERE id=${i}$", + {"e": json.dumps(newb), "i": getattr(acc, "id", "")}) + await sor.sqlExe("COMMIT", {}) + account_notes.append("账号「%s」原绑定端点 %s 不含协议 %s 的端点(运行时会 404)," + "已自动补绑为 %s" % (getattr(acc, "name", ""), bound, + protocol, newb)) audit_note = await _audit_media_convention(sor, list(profile_ids.values())) return json.dumps({ "vendor": vendor_action, "profiles": profile_ids, @@ -656,6 +714,7 @@ async def _h_apply_llm_config(sor, params, ctx): "vendor_conflicts": conflicts, "template_errors": tpl_errors, "template_notes": tpl_notes, + "account_notes": account_notes, "runtime_note": rt_note, "media_audit": audit_note, }, ensure_ascii=False)