fix(platform): 端到端实测揪出3个bug——①模型更新分支不刷新profile_id(旧模板被弃用重建后模型仍指deprecated→运行时崩)②legacy查询模板复用未做骨架自愈③查询path归一化(单花括号{task_id}对齐运行时str.replace契约+剥base_url重复前缀+缺占位符补/tasks/{task_id})
This commit is contained in:
parent
e9d0bfc62d
commit
69884fd2ec
Binary file not shown.
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user