diff --git a/pipeline_platform/platform_ability.py b/pipeline_platform/platform_ability.py index 3ce7ef7..8313b80 100644 --- a/pipeline_platform/platform_ability.py +++ b/pipeline_platform/platform_ability.py @@ -365,7 +365,7 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型 "pricing": { "currency": "CNY|USD(文档标注的币种)", "items": [{"vendor_model_id": "对应模型ID", - "factor": "duration|flat|prompt_tokens|completion_tokens(计价因子:视频按秒=duration,按次=flat,token计价=对应token因子)", + "factor": "duration|flat|prompt_tokens|completion_tokens|uncache_tokens|cached_tokens(计价因子:视频按秒=duration,按次=flat;token计价:文档只给一个输入价=prompt_tokens;文档区分缓存命中价=非缓存输入用uncache_tokens+缓存命中用cached_tokens+输出completion_tokens——禁止用prompt_tokens配缓存价,会与uncache/cached重复计价)", "unit_price": 0.45, "unit": "秒|次|百万(文档标注的计价单位名)", "dimensions": {"resolution": "480P(影响价格档位的维度,按文档原文值;禁止放 model)"}, @@ -1286,10 +1286,17 @@ def _gen_templates(protocol: str, capability: str, spec: dict) -> dict: headers = _headers_from_spec(spec) notes = [] if protocol == "openai_compat": - req = json.dumps({ - "model": "{{model}}", "messages": "{{messages}}", - "temperature": "{{temperature}}", "stream": False, - }, ensure_ascii=False) + # 2026-09-06 修复(豆包 pro 配置实抓):旧模板用 json.dumps 包占位符—— + # 1. "messages": "{{messages}}" 带引号,渲染成 Python repr 字符串而非 + # JSON 数组,上游必拒; + # 2. "{{temperature}}" 运行时命名空间无此变量(inference._build_upstream_body + # 的 ns 是 model/messages/extra/params/api_key/org_id),StrictUndefined + # 干跑必崩 → 落库被拦,profiles 恒空,运行时侥幸走 _BUILTIN_REQUEST_TMPL。 + # 正确形态对齐 pipeline-llm 内置模板:tojson 无引号 + extra 白名单透传循环 + # (temperature/top_p/max_tokens 等调用方传了才发,不硬编码业务参数)。 + req = ('{"model": {{ model|tojson }}, "messages": {{ messages|tojson }}' + '{% for k, v in extra.items() %}, "{{ k }}": {{ v|tojson }}' + '{% endfor %}, "stream": false}') resp = json.dumps({ "content": "choices[0].message.content", "usage": {"prompt_tokens": "usage.prompt_tokens", @@ -1610,6 +1617,9 @@ def _dry_render_check(req_tpl, capability, biz_params, media_params): # StrictUndefined 下缺键会误报,二者行为不一致。 ('零媒体形态', dict(params_arr, **{mp: [] for mp in media_params or []}))): ns = {'model': 'dry-run-model', 'prompt': '干跑校验', 'messages': [], + # extra:运行时白名单透传参数(inference._build_upstream_body 注入, + # openai_compat 模板的 for 循环消费)——干跑缺它会误报 UndefinedError + 'extra': {'temperature': 0.7}, 'params': params, 'api_key': 'sk-dry-run', 'org_id': '0', 'request': None, 'json': json, 'b64media2url': _stub, 'downloadfile2url': _stub} @@ -1696,32 +1706,70 @@ async def _ensure_step_profile(sor, protocol, cap, vmid, step, spec): _FACTOR_LABELS = { 'duration': '时长', 'flat': '按次', 'prompt_tokens': '输入tokens', - 'completion_tokens': '输出tokens', + 'completion_tokens': '输出tokens', 'uncache_tokens': '非缓存输入tokens', + 'cached_tokens': '缓存tokens', } +# 缓存拆分因子(derived 自 usage 原始字段,定价引擎自动计算,见 pricing-data-format 技能) +_DERIVED_FACTOR_SPECS = { + 'uncache_tokens': { + 'type': 'int', 'role': 'factor', 'label': '非缓存输入Token', + 'derived': 'prompt_tokens - prompt_tokens_details.cached_tokens'}, + 'cached_tokens': { + 'type': 'int', 'role': 'factor', 'label': '缓存Token', + 'derived': 'prompt_tokens_details.cached_tokens'}, +} -def _build_pricing_yaml(currency_items, dimensions_all, factor): - """生成定价 YAML(2026-09-05 用户定夺的新模式): +# token 因子族:一个 token 模型的定价天然多因子(输入/输出/缓存同在一个方案)。 +# 旧校验「同模型仅允许单一因子」与生产事实矛盾(qwen3.8-max 三因子同方案正常出账), +# token 模型按此族合并为一个定价方案(2026-09-06 修复)。 +_TOKEN_FACTORS = ('prompt_tokens', 'completion_tokens', 'uncache_tokens', 'cached_tokens') + +# 同一因子多条价格且无维度区分 → 引擎会重复计价,必须拆方案(真混用的判据) +def _check_factor_dup(mitems, factors): + seen = set() + for it in mitems: + f = it.get("factor") or "flat" + dims = json.dumps(it.get("dimensions") or {}, ensure_ascii=False, sort_keys=True) + k = (f, dims) + if k in seen: + return ("因子 %s 出现多条相同维度的价格(引擎会重复计价)——请核对文档," + "同因子多价必须用维度区分档位" % f) + seen.add(k) + return None + + +def _build_pricing_yaml(currency_items, dimensions_all, factors): + """生成定价 YAML(2026-09-05 用户定夺的新模式;2026-09-06 支持多因子): - 不放 model 过滤:一个定价方案只服务一个模型;定价相同的多个模型共享 同一 ppid(不是往定价里加 model 维度) - 不用 filters 子结构:维度直接平铺在定价项(引擎对非保留键做 AND 匹配) - fields 需定义每个维度(role: filter),否则引擎报「在fields中没有定义」 + - factors 为列表:token 模型天然多因子(输入/输出/缓存同在一个方案, + 生产实证 qwen3.8-max),每条 pricing 用各自的 factor;derived 因子 + (uncache_tokens/cached_tokens)必须在 fields 定义 derived 表达式, + 引擎计算衍生字段依赖此定义(pricing-data-format 技能 §2) """ fields = { 'price_factors': {'type': 'string', 'role': 'factor', 'label': '计价因子'}, 'unit_prices': {'type': 'float', 'role': 'factor', 'label': '单位定价'}, 'unit': {'type': 'string', 'role': 'factor', 'label': '计价单位'}, } - if factor not in fields: - fields[factor] = {'type': 'float', 'role': 'factor', - 'label': _FACTOR_LABELS.get(factor, factor)} + for factor in factors: + if factor in fields: + continue + if factor in _DERIVED_FACTOR_SPECS: + fields[factor] = dict(_DERIVED_FACTOR_SPECS[factor]) + else: + fields[factor] = {'type': 'float', 'role': 'factor', + 'label': _FACTOR_LABELS.get(factor, factor)} for dim in sorted(dimensions_all): fields[dim] = {'type': 'string', 'role': 'filter', 'label': dim} pricings = [] for it in currency_items: - item = {'price_factors': factor, 'unit_prices': it['unit_price'], - 'unit': it['unit']} + item = {'price_factors': it.get('factor') or 'flat', + 'unit_prices': it['unit_price'], 'unit': it['unit']} for dk, dv in sorted((it.get('dimensions') or {}).items()): item[dk] = dv pricings.append(item) @@ -1796,15 +1844,41 @@ async def _h_apply_model_pricing(sor, params, ctx): model_id = getattr(recs[0], "id", "") old_ppid = getattr(recs[0], "ppid", "") or "" factors = sorted(set((it.get("factor") or "flat") for it in mitems)) - if len(factors) != 1: + # 因子族校验(2026-09-06 修复):token 模型天然多因子(输入/输出/缓存 + # 同在一个方案,生产实证 qwen3.8-max ppid=TNJnx_Nw1HGDOtDIFji-w 三因子 + # 正常出账)——旧「单因子」铁律把所有 token 定价判死,工具校验与生产 + # 事实矛盾。真混用的判据改为两条: + # 1. token 族与非 token 族(duration/flat 等)不可混在一个方案 + # 2. 同一因子多条价格且维度相同 → 引擎重复计价 + non_token = sorted(set(factors) - set(_TOKEN_FACTORS)) + token_hit = sorted(set(factors) & set(_TOKEN_FACTORS)) + if non_token and token_hit: results.append({"model": vmid, "ok": False, - "error": "同一模型混用多个计价因子 %s——请拆分为多个定价方案" % factors}) + "error": "token 因子 %s 与按量因子 %s 混在一个模型——" + "计费单位不同,请拆分为多个定价方案" % (token_hit, non_token)}) continue - factor = factors[0] + if len(non_token) > 1: + results.append({"model": vmid, "ok": False, + "error": "同一模型混用多个按量计价因子 %s——请拆分为多个定价方案" % non_token}) + continue + # 缓存拆分与整体输入价互斥:prompt_tokens 计全部输入,uncache_tokens 只计 + # 非缓存部分——同方案并存会对缓存命中 tokens 重复计价(2026-09-06) + if 'prompt_tokens' in factors and ('uncache_tokens' in factors or 'cached_tokens' in factors): + results.append({"model": vmid, "ok": False, + "error": "prompt_tokens 与 uncache_tokens/cached_tokens 同方案——" + "缓存命中部分会被重复计价。文档区分缓存价时只用 " + "uncache_tokens+cached_tokens+completion_tokens;" + "不区分时只用 prompt_tokens+completion_tokens"}) + continue + dup_err = _check_factor_dup(mitems, factors) + if dup_err: + results.append({"model": vmid, "ok": False, "error": dup_err}) + continue + factor = ",".join(factors) # 方案命名/回报展示用 dims_all = set() for it in mitems: dims_all.update((it.get("dimensions") or {}).keys()) - yaml_str = _build_pricing_yaml(mitems, dims_all, factor) + yaml_str = _build_pricing_yaml(mitems, dims_all, factors) # 引擎试算护栏:渲染一遍确认合法 YAML + fields 完整(不落库先验证) try: parsed = yaml.safe_load(yaml_str)