From b2ecf54f4372022c923c07f0fe211739f825a3b0 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 6 Sep 2026 14:56:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(platform):=20=E8=83=BD=E5=8A=9B=E8=AF=AF?= =?UTF-8?q?=E5=88=A4=E6=A0=B9=E6=B2=BB=E4=B8=89=E5=B1=82=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E6=8F=90=E7=A4=BA=E8=AF=8D=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E9=93=81=E5=BE=8B(=E6=8C=89=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E6=A8=A1=E6=80=81=E5=A3=B0=E6=98=8E=E7=A6=81=E7=85=A7?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B);apply=E8=83=BD=E5=8A=9B=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E5=AE=88=E5=8D=AB(=E6=8F=8F=E8=BF=B0=E6=A8=A1?= =?UTF-8?q?=E6=80=81vs=20capability=E7=9F=9B=E7=9B=BE=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BA=A7=E7=BA=A0=E6=AD=A3t2v=E2=86=92r2v);overrides.model=5Fc?= =?UTF-8?q?apability=E7=99=BD=E5=90=8D=E5=8D=95(=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=8F=A3=E5=A4=B4=E7=BA=A0=E6=AD=A3=E8=83=BD=E8=90=BD=E8=BF=9B?= =?UTF-8?q?=E8=A7=84=E6=A0=BC,=E7=A6=81=E9=87=8D=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E6=AD=BB=E5=BE=AA=E7=8E=AF);check=5Fmodel=5Faccounting?= =?UTF-8?q?=E5=8C=BA=E5=88=86failed=E8=B0=83=E7=94=A8=E8=A1=8C/=E5=85=85?= =?UTF-8?q?=E5=80=BC=E8=A1=8C=E9=9D=9E=E5=BE=85=E8=AE=B0=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline_platform/platform_ability.py | 126 ++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index f89834e..b506dbe 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -136,12 +136,35 @@ def _spec_overlay(base, overrides): """覆盖项合并:仅允许少量标量覆盖(vendor_name 等),不做大 JSON 搬运。 overrides 只认白名单键,防止 LLM 借覆盖项塞回编造结构。 + model_capability(2026-09-06 wan3.0 教训):{vendor_model_id: capability} + 按模型纠正能力——用户提醒「这是参考生视频」后助手必须有工具路径把纠正 + 落进规格,否则只能重跑 extract,同样提示词同样误判(提醒无效的死循环)。 + 纠正值仍要过能力字典校验(下游 unknown 拒绝落库),不会被塞私货。 """ allowed = {'vendor_name', 'base_url', 'protocol', 'doc_url', 'doc_notes'} out = dict(base or {}) for k in allowed: if k in (overrides or {}) and str(overrides.get(k) or '').strip(): out[k] = str(overrides[k]).strip() + mc = (overrides or {}).get('model_capability') + if isinstance(mc, str): + try: + mc = json.loads(mc) + except Exception: + mc = None + if isinstance(mc, dict) and mc: + fixed = [] + models = list(out.get('models') or []) + for m in models: + vmid = m.get('vendor_model_id') or m.get('name') or '' + newcap = str(mc.get(vmid) or '').strip() + if vmid and newcap and newcap != m.get('capability'): + fixed.append('%s: %s→%s' % (vmid, m.get('capability'), newcap)) + m['capability'] = newcap + out['models'] = models + if fixed: + out['doc_notes'] = ((out.get('doc_notes') or '') + + ' [能力已按人工纠正: %s]' % ';'.join(fixed)).strip() return out @@ -335,7 +358,7 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型 "response_format": "响应格式说明(content 字段路径 + usage 字段路径)", "response_example": {"说明": "文档中的响应示例 JSON(或结果字段说明),逐字照抄;无则 null"}, "async_steps": [{"purpose": "query|download", "path": "该步骤接口路径", "method": "GET|POST", "request_fields": ["请求字段名列表"], "response_format": "该步骤响应格式说明"}], - "models": [{"vendor_model_id": "供应商侧模型ID", "capability": "t2t|t2i|i2t|t2v|i2v|r2v|i2i|embedding|rerank|tts|asr", + "models": [{"vendor_model_id": "供应商侧模型ID", "capability": "t2t|t2i|i2t|t2v|i2v|r2v|i2i|embedding|rerank|tts|asr|mm-embedding|mm-rerank", "sync_mode": "sync|async(提交后需轮询查询结果的填 async)", "description": "一句话说明"}], "pricing": { @@ -358,6 +381,16 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型 path 照文档逐字抄;同供应商多个模型会复用同一份查询模板,不要因模型而异。 - 同步模型 async_steps 填空数组 []。 +能力判定铁律(2026-09-06 wan3.0-video-prime 误判教训——概述明写"四模态全能参考"、 +输入=音频/图像/文本/视频,却因调用示例只有 prompt 被判成 t2v): +- capability 必须以文档**概述正文 + 输入/输出模态声明**为准,禁止以调用示例为准—— + 示例常只演示最简形态(纯文本 prompt),照示例判会把多媒体参考模型误判成纯文本模型。 +- 判定表(输入模态 → 输出模态 ⇒ 能力): + 多种媒体输入或正文含「参考/reference/全能参考/多图/多模态输入」⇒ r2v(即使示例只有文本); + 图像+文本 → 视频 ⇒ i2v;仅文本 → 视频 ⇒ t2v;仅文本 → 图像 ⇒ t2i;图像 → 图像 ⇒ i2i; + 视频 → 视频 ⇒ v2v 类按字典就近;文本 → 音频 ⇒ tts;音频 → 文本 ⇒ asr。 +- description 里必须保留正文的模态声明原文关键词(如"支持四模态全能参考"),供下游一致性校验。 + 定价规则: - pricing.items 每条必须带 doc_quote(文档原文定价句逐字照抄)——定价只允许来自文档原文 - 价格/单位按文档原文记录,禁止自行换算、推导或统一单位 @@ -411,9 +444,20 @@ async def _h_extract_llm_api_spec(sor, params, ctx): if anchored: out["__anchored__"] = ("规格已锚定到本会话缓存。下一步调用 apply_llm_config 与 " "apply_model_pricing 时传 {\"use_last_extract\": true}," - "需改动只传覆盖项(overrides,仅 vendor_name/base_url/" - "protocol/doc_url/doc_notes 白名单键)。" + "需改动只传覆盖项(overrides 白名单:vendor_name/base_url/" + "protocol/doc_url/doc_notes/model_capability)。" "禁止把本规格复制进 spec 参数——复制即编造。") + # 能力一致性告警(2026-09-06 wan3.0 教训):提取完当场校验描述模态声明 vs + # capability,矛盾信息直接回给助手(用户纠正时传 overrides.model_capability 重 apply) + cap_alerts = [] + for _m in (spec.get("models") or []): + _fix, _warn = _capability_guard(_m.get("capability", ""), _m.get("description", "")) + if _warn: + cap_alerts.append("%s: %s" % (_m.get("vendor_model_id", "?"), _warn)) + if cap_alerts: + out["__capability_alert__"] = ("能力分类与文档描述矛盾(按示例误判的典型信号)," + "apply 时会自动纠正,也可用 overrides.model_capability " + "人工指定:" + ";".join(cap_alerts)) return json.dumps(out, ensure_ascii=False) @@ -426,6 +470,52 @@ def _f(v, default=0.0): return default +# 描述里的媒体输入声明词(能力一致性守卫用) +_DESC_MEDIA_WORDS = { + 'image': ('图像', '图片', '多图'), + 'video': ('视频',), + 'audio': ('音频', '声音'), +} +_DESC_REF_WORDS = ('参考生', '全能参考', '参考图', '参考视频', '参考音频', '四模态', '多模态参考') + + +def _capability_guard(capability, description): + """描述模态声明 vs capability 一致性守卫(2026-09-06 wan3.0-video-prime 实测: + 描述自带"支持四模态全能参考"、capability 却是 t2v——提取 LLM 照调用示例判能力, + 示例只演示最简形态就误判;提示词加铁律后仍需代码级兜底,LLM 不保证遵守)。 + + 保守策略:只在「纯文本能力(t2v/t2i/t2t)+ 描述含媒体输入/参考声明」这种 + 铁证矛盾时纠正;其他不一致只警告不改(避免误伤描述措辞不规范的正确配置)。 + 返回 (corrected_cap 或 None, warning 或 None)。 + """ + desc = description or '' + if not desc or capability not in ('t2v', 't2i', 't2t'): + return None, None + has_ref = any(w in desc for w in _DESC_REF_WORDS) + kinds = [k for k, words in _DESC_MEDIA_WORDS.items() if any(w in desc for w in words)] + # 「输出 视频」的"视频"不算输入声明——t2v 描述几乎都带"视频"二字。 + # 输入声明判据:参考词,或图像/音频词(这两个不会出现在纯文本生成的描述里), + # 或"视频"以外多模态组合。 + input_kinds = [k for k in kinds if k != 'video'] + if has_ref or len(input_kinds) >= 1 or (has_ref is False and len(kinds) >= 2 and 'video' in kinds and input_kinds): + out_word = '视频' if capability == 't2v' else ('图像' if capability == 't2i' else '') + if capability == 't2t': + return None, ("描述含媒体输入声明(%s)但能力是 t2t——请人工核对能力分类" + % '/'.join(kinds)) + expected = 'r2v' if (has_ref or len(kinds) >= 2) else ( + 'i2v' if 'image' in input_kinds and out_word == '视频' else + ('i2i' if 'image' in input_kinds and out_word == '图像' else None)) + if expected and expected != capability: + return expected, ("能力自动纠正 %s→%s:描述声明媒体输入(%s%s)," + "纯文本能力与之矛盾(提取照示例误判,守卫按正文模态纠正)" + % (capability, expected, '/'.join(kinds), + ',含参考声明' if has_ref else '')) + return None, ("描述含媒体输入声明(%s)但能力是 %s——无法确定目标能力," + "请人工核对(可用 overrides.model_capability 纠正)" + % ('/'.join(kinds), capability)) + return None, None + + async def _h_apply_llm_config(sor, params, ctx): """按提取的规格写库:供应商(复用/新建)+ 适配模板 + 模型。 @@ -649,6 +739,15 @@ async def _h_apply_llm_config(sor, params, ctx): cap = m.get("capability") or "t2t" sync_mode = "async" if str(m.get("sync_mode") or "").strip() == "async" else "sync" desc = (m.get("description", "") or "").strip() + # 能力一致性守卫(2026-09-06 wan3.0 实测):描述模态声明 vs capability + # 铁证矛盾时代码级纠正——提取 LLM 照示例误判,提示词铁律不保证遵守 + cap_fix, cap_warn = _capability_guard(cap, desc) + if cap_fix: + tpl_notes.append("%s: %s" % (vmid, cap_warn)) + cap = cap_fix + m["capability"] = cap_fix + elif cap_warn: + tpl_notes.append("%s: %s" % (vmid, cap_warn)) # 出处标注(2026-09-05 用户规则):模型注册表描述字段必须保存文档 URL if doc_url and ("[出处:" + doc_url + "]") not in desc: desc = (desc + " " if desc else "") + "[出处:%s]" % doc_url @@ -1731,7 +1830,17 @@ async def _h_check_model_accounting(sor, params, ctx): "模型未映射产品→产品管理导入产线模型;" "定价无匹配档位→核对usages维度值与定价YAML是否一致") elif st == "created": - item["verdict"] = "待记账(出账循环60秒一轮,稍后复查;持续不变查记账worker进程)" + # m0011 设计:accounting_status 只对 status='ok' 行有意义—— + # failed 调用行/recharge 充值行挂 created 是被出账循环天然排除(不计费), + # 不是「待记账」(2026-09-06 实测:助手把 21 条 failed 行误报成待记账) + call_st = row.get("status", "") + if call_st == "recharge": + item["verdict"] = "充值对账流水(非计费用量,不参与出账)" + elif call_st != "ok": + item["verdict"] = "调用失败行(不计费,出账循环天然排除)" + item["reason"] = (row.get("note") or "")[:200] + else: + item["verdict"] = "待记账(出账循环60秒一轮,稍后复查;持续不变查记账worker进程)" elif st == "accounted": # 独立复算:用 usages 因子过定价引擎,对比 charge item["verdict"] = "已记账" @@ -1813,10 +1922,10 @@ PLATFORM_TOOLS = [ ), ToolDefinition( name="apply_llm_config", - description="按提取的规格写入模型治理配置:供应商/端点/适配模板/模型。幂等。推荐 use_last_extract=true 取本会话锚定规格(禁止把规格复制进 spec——复制即编造);需改动加 overrides(仅 vendor_name/base_url/protocol/doc_url/doc_notes)。仅owner组织角色可用。", + description="按提取的规格写入模型治理配置:供应商/端点/适配模板/模型。幂等。推荐 use_last_extract=true 取本会话锚定规格(禁止把规格复制进 spec——复制即编造);需改动加 overrides(vendor_name/base_url/protocol/doc_url/doc_notes/model_capability——用户纠正能力分类时传 model_capability 而非重提取)。仅owner组织角色可用。", parameters={ "use_last_extract": "true=用本会话 extract_llm_api_spec 锚定的规格(推荐,会话级隔离)", - "overrides": "覆盖项 JSON(白名单:vendor_name/base_url/protocol/doc_url/doc_notes,如用户要求改供应商名)", + "overrides": "覆盖项 JSON(白名单:vendor_name/base_url/protocol/doc_url/doc_notes/model_capability。model_capability={模型ID:能力} 如 {\\\"wan3.0-video-prime\\\": \\\"r2v\\\"}——用户纠正能力分类时传它+use_last_extract 重新 apply,不要重提取;模板会按新能力重建)", "spec": "(仅无锚定时的兜底)extract 返回的 JSON 规格原文", }, category="platform", @@ -1904,6 +2013,11 @@ PLATFORM_PROMPT = """ - **定价只允许来自文档原文**(doc_quote 强制),禁止编造/换算/推导 - **出处必留**:模型注册表描述字段与定价方案描述字段都必须保存文档 URL - 能力分类不存在 → 先加分类(字典/种子/提示词/端点注释四处同步)再配模型,禁止塞近似分类 +- 能力分类铁律:按文档正文的输入/输出模态声明判定,禁止照调用示例判(示例常只演示 + 最简形态——wan3.0 实测「四模态全能参考」被示例判成 t2v)。extract 返回 + __capability_alert__ 说明描述与能力矛盾,apply 会自动纠正;用户口头纠正能力时, + 传 overrides={"model_capability": {"模型ID": "能力"}} + use_last_extract 重新 apply, + 禁止重提取(同样的提示词只会同样误判) - 生成类模型媒体铁律:上行统一三数组契约 image_files/audio_files/video_files (字符串或数组均可)经 b64media2url 转公网 URL;下行产物用 downloadfile2url(request, url) 落地;apply 返回的 media_audit 警告必须如实转告