diff --git a/pipeline_platform/__pycache__/platform_ability.cpython-310.pyc b/pipeline_platform/__pycache__/platform_ability.cpython-310.pyc index 1146bc4..704177c 100644 Binary files a/pipeline_platform/__pycache__/platform_ability.cpython-310.pyc and b/pipeline_platform/__pycache__/platform_ability.cpython-310.pyc differ diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index 499fde5..f01d94d 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -621,11 +621,16 @@ async def _h_apply_llm_config(sor, params, ctx): "%s: 模型已存在且挂供应商 id=%s,与本次规格供应商(%s)不一致——" "未改供应商归属,仅更新描述/同步模式;如需迁移请明确指示" % (vmid, cur_vendor, vendor_name)) + # profile_id 必须一并刷新(2026-09-05 实测 bug):模型原挂模板可能已被 + # 弃用重建(旧骨架自愈),不刷新则运行时仍指 deprecated 模板→必崩 + new_pid = profile_ids.get(cap, "") await sor.sqlExe( "UPDATE llm_model SET description=${d}$, " - "sync_mode=${sm}$, query_profile_ids=${q}$, updated_at=NOW() " + "sync_mode=${sm}$, query_profile_ids=${q}$, " + "profile_id=IF(${p}$='', profile_id, ${p}$), updated_at=NOW() " "WHERE id=${i}$", - {"d": desc, "sm": sync_mode, "q": query_ids_json, "i": mid}) + {"d": desc, "sm": sync_mode, "q": query_ids_json, + "p": new_pid, "i": mid}) await sor.sqlExe("COMMIT", {}) updated.append(vmid) continue @@ -989,42 +994,56 @@ async def _ensure_step_profile(sor, protocol, cap, vmid, step, spec): """异步后续步骤的适配模板(query/download)。 2026-09-05 改造(用户:同类任务查询接口供应商级相同,可复用): - 幂等键=名称「{protocol}-query-{path}」(去掉模型维度)——同供应商同协议 - 下多个模型共享同一份查询模板;旧的按 {vmid}-{purpose} 命名继续兼容复用。 - 模板按文档真实生成:运行时查询只用 path/headers/method,task_id 经 - path 占位(如 /tasks/{{task_id}});不再产 __from_doc__ 骨架。 + 幂等键=名称「{protocol}-{purpose}-{path}」(去掉模型维度)——同供应商同协议 + 下多个模型共享同一份查询模板;旧的 {vmid}-{purpose} 命名兼容复用, + 但若旧模板仍是骨架(__from_doc__)同样弃用重建。 + path 归一化(对齐运行时契约,实测踩过三个坑): + - 任务号占位符用**单花括号** {task_id}(运行时 str.replace 消费, + Jinja 双花括号不会被替换);文档给 {{task_id}} 也归一为单花括号 + - 剥掉 base_url 已有的路径前缀(提取给 /api/v1/tasks/{task_id}, + 运行时 url=base_url+path,base_url 已含 /api/v1 → 不归一会双前缀 404) + - 缺占位符时按 DashScope 惯例补 /tasks/{task_id} 返回 profile id。 """ purpose = (step.get("purpose") or "query").strip() or "query" path = (step.get("path") or "").strip() method = (step.get("method") or ("GET" if purpose == "query" else "POST")).strip().upper() + # path 归一化 + from urllib.parse import urlparse + bu_path = urlparse((spec.get("base_url") or "").strip()).path.rstrip("/") + if bu_path and path.startswith(bu_path + "/"): + path = path[len(bu_path):] + path = path.replace("{{task_id}}", "{task_id}") + if "{task_id}" not in path and purpose == "query": + path = (path.rstrip("/") + "/{task_id}") if path else "/tasks/{task_id}" shared_name = "%s-%s-%s" % (protocol, purpose, path or "default") legacy_name = "%s-%s" % (vmid, purpose) - recs = await sor.sqlExe( - "SELECT id FROM llm_api_profile WHERE name=${n}$ AND status='active' LIMIT 1", - {"n": shared_name}) - await sor.sqlExe("COMMIT", {}) - if recs: - return getattr(recs[0], "id", "") - recs = await sor.sqlExe( - "SELECT id FROM llm_api_profile WHERE name=${n}$ AND status='active' LIMIT 1", - {"n": legacy_name}) - await sor.sqlExe("COMMIT", {}) - if recs: - return getattr(recs[0], "id", "") + for cand in (shared_name, legacy_name): + recs = await sor.sqlExe( + "SELECT id, request_template, response_template FROM llm_api_profile " + "WHERE name=${n}$ AND status='active' LIMIT 1", {"n": cand}) + await sor.sqlExe("COMMIT", {}) + if not recs: + continue + old_tpl = (getattr(recs[0], "request_template", "") or "") + \ + (getattr(recs[0], "response_template", "") or "") + if not any(mk in old_tpl for mk in _SKELETON_MARKERS): + return getattr(recs[0], "id", "") + pid_old = getattr(recs[0], "id", "") + await sor.sqlExe("UPDATE llm_api_profile SET status='deprecated' " + "WHERE id=${i}$", {"i": pid_old}) + await sor.sqlExe("COMMIT", {}) + # 旧骨架弃用——继续往下重建(不 return) headers = _headers_from_spec(spec) if method == "GET": req = json.dumps({"method": method}, ensure_ascii=False) - if "{{task_id}}" not in path: - # path 没带任务号占位时按 DashScope 惯例拼 /tasks/{id};notes 如实标注 - path = (path.rstrip("/") + "/{{task_id}}") if path else "/tasks/{{task_id}}" else: req = json.dumps({"method": method, - "data": {"task_id": "{{task_id}}"}}, ensure_ascii=False) - # 查询步骤的响应渲染在运行时由提交模板的 response_template 接管 - # (inference 轮询直接调 _render_response(submit_profile,...)),这里存档即可 + "data": {"task_id": "{task_id}"}}, ensure_ascii=False) + # 查询步骤的响应解析在运行时由提交模板的 response_template 接管 + # (inference._async_inference 轮询后直接用提交模板渲染),这里存档即可 resp = json.dumps({ - "__note__": "查询步骤模板:运行时响应解析走模型提交模板的 response_template", + "note": "查询步骤模板:运行时响应解析走模型提交模板的 response_template", "response_format": step.get("response_format", "") or "", }, ensure_ascii=False) pid = getID()