fix(platform): 定价多因子校验+openai_compat模板生成器双缺陷(2026-09-06豆包pro配置实抓)
① apply_model_pricing「同一模型仅单因子」铁律与生产事实矛盾——token模型天然
多因子(输入/输出/缓存同方案,生产实证qwen3.8-max正常出账),豆包pro定价三次
被拒。改为因子族校验:token族(prompt/completion/uncache/cached_tokens)合并
一个方案;token族与按量族(duration/flat)禁混;prompt_tokens与uncache/cached
互斥(重复计价);同因子同维度多价拒绝。提取提示词factor枚举补uncache_tokens/
cached_tokens+缓存拆分规则(此前枚举缺缓存因子,助手只能硬塞导致混用误判)。
_build_pricing_yaml支持多因子:derived因子(uncache/cached)自动带derived表达式
定义,真实定价引擎验证:豆包pro三因子6/1.2/30计算精确匹配(0.000738/缓存场景0.01908)。
② _gen_templates openai_compat分支模板恒干跑失败:json.dumps包占位符生成
"{{temperature}}"——运行时ns(model/messages/extra/params/api_key/org_id)无
temperature,StrictUndefined干跑必崩→profiles恒空,运行时侥幸走内置模板。
且"messages":"{{messages}}"带引号渲染成repr字符串。改为对齐pipeline-llm
内置模板形态:model/messages|tojson无引号+extra白名单透传循环(传了才发)。
_dry_render_check补extra入ns(运行时注入,干跑缺它误报)。验证:干跑通过,
无extra/带extra/中文引号注入/指纹稳定四场景渲染全合法JSON。
This commit is contained in:
parent
a49ef8ee4a
commit
549d671e99
@ -365,7 +365,7 @@ _EXTRACT_PROMPT = """你是大模型 API 配置专家。通读下面这份模型
|
|||||||
"pricing": {
|
"pricing": {
|
||||||
"currency": "CNY|USD(文档标注的币种)",
|
"currency": "CNY|USD(文档标注的币种)",
|
||||||
"items": [{"vendor_model_id": "对应模型ID",
|
"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_price": 0.45,
|
||||||
"unit": "秒|次|百万(文档标注的计价单位名)",
|
"unit": "秒|次|百万(文档标注的计价单位名)",
|
||||||
"dimensions": {"resolution": "480P(影响价格档位的维度,按文档原文值;禁止放 model)"},
|
"dimensions": {"resolution": "480P(影响价格档位的维度,按文档原文值;禁止放 model)"},
|
||||||
@ -1286,10 +1286,17 @@ def _gen_templates(protocol: str, capability: str, spec: dict) -> dict:
|
|||||||
headers = _headers_from_spec(spec)
|
headers = _headers_from_spec(spec)
|
||||||
notes = []
|
notes = []
|
||||||
if protocol == "openai_compat":
|
if protocol == "openai_compat":
|
||||||
req = json.dumps({
|
# 2026-09-06 修复(豆包 pro 配置实抓):旧模板用 json.dumps 包占位符——
|
||||||
"model": "{{model}}", "messages": "{{messages}}",
|
# 1. "messages": "{{messages}}" 带引号,渲染成 Python repr 字符串而非
|
||||||
"temperature": "{{temperature}}", "stream": False,
|
# JSON 数组,上游必拒;
|
||||||
}, ensure_ascii=False)
|
# 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({
|
resp = json.dumps({
|
||||||
"content": "choices[0].message.content",
|
"content": "choices[0].message.content",
|
||||||
"usage": {"prompt_tokens": "usage.prompt_tokens",
|
"usage": {"prompt_tokens": "usage.prompt_tokens",
|
||||||
@ -1610,6 +1617,9 @@ def _dry_render_check(req_tpl, capability, biz_params, media_params):
|
|||||||
# StrictUndefined 下缺键会误报,二者行为不一致。
|
# StrictUndefined 下缺键会误报,二者行为不一致。
|
||||||
('零媒体形态', dict(params_arr, **{mp: [] for mp in media_params or []}))):
|
('零媒体形态', dict(params_arr, **{mp: [] for mp in media_params or []}))):
|
||||||
ns = {'model': 'dry-run-model', 'prompt': '干跑校验', 'messages': [],
|
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',
|
'params': params, 'api_key': 'sk-dry-run', 'org_id': '0',
|
||||||
'request': None, 'json': json,
|
'request': None, 'json': json,
|
||||||
'b64media2url': _stub, 'downloadfile2url': _stub}
|
'b64media2url': _stub, 'downloadfile2url': _stub}
|
||||||
@ -1696,32 +1706,70 @@ async def _ensure_step_profile(sor, protocol, cap, vmid, step, spec):
|
|||||||
|
|
||||||
_FACTOR_LABELS = {
|
_FACTOR_LABELS = {
|
||||||
'duration': '时长', 'flat': '按次', 'prompt_tokens': '输入tokens',
|
'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):
|
# token 因子族:一个 token 模型的定价天然多因子(输入/输出/缓存同在一个方案)。
|
||||||
"""生成定价 YAML(2026-09-05 用户定夺的新模式):
|
# 旧校验「同模型仅允许单一因子」与生产事实矛盾(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 过滤:一个定价方案只服务一个模型;定价相同的多个模型共享
|
- 不放 model 过滤:一个定价方案只服务一个模型;定价相同的多个模型共享
|
||||||
同一 ppid(不是往定价里加 model 维度)
|
同一 ppid(不是往定价里加 model 维度)
|
||||||
- 不用 filters 子结构:维度直接平铺在定价项(引擎对非保留键做 AND 匹配)
|
- 不用 filters 子结构:维度直接平铺在定价项(引擎对非保留键做 AND 匹配)
|
||||||
- fields 需定义每个维度(role: filter),否则引擎报「在fields中没有定义」
|
- fields 需定义每个维度(role: filter),否则引擎报「在fields中没有定义」
|
||||||
|
- factors 为列表:token 模型天然多因子(输入/输出/缓存同在一个方案,
|
||||||
|
生产实证 qwen3.8-max),每条 pricing 用各自的 factor;derived 因子
|
||||||
|
(uncache_tokens/cached_tokens)必须在 fields 定义 derived 表达式,
|
||||||
|
引擎计算衍生字段依赖此定义(pricing-data-format 技能 §2)
|
||||||
"""
|
"""
|
||||||
fields = {
|
fields = {
|
||||||
'price_factors': {'type': 'string', 'role': 'factor', 'label': '计价因子'},
|
'price_factors': {'type': 'string', 'role': 'factor', 'label': '计价因子'},
|
||||||
'unit_prices': {'type': 'float', 'role': 'factor', 'label': '单位定价'},
|
'unit_prices': {'type': 'float', 'role': 'factor', 'label': '单位定价'},
|
||||||
'unit': {'type': 'string', 'role': 'factor', 'label': '计价单位'},
|
'unit': {'type': 'string', 'role': 'factor', 'label': '计价单位'},
|
||||||
}
|
}
|
||||||
if factor not in fields:
|
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',
|
fields[factor] = {'type': 'float', 'role': 'factor',
|
||||||
'label': _FACTOR_LABELS.get(factor, factor)}
|
'label': _FACTOR_LABELS.get(factor, factor)}
|
||||||
for dim in sorted(dimensions_all):
|
for dim in sorted(dimensions_all):
|
||||||
fields[dim] = {'type': 'string', 'role': 'filter', 'label': dim}
|
fields[dim] = {'type': 'string', 'role': 'filter', 'label': dim}
|
||||||
pricings = []
|
pricings = []
|
||||||
for it in currency_items:
|
for it in currency_items:
|
||||||
item = {'price_factors': factor, 'unit_prices': it['unit_price'],
|
item = {'price_factors': it.get('factor') or 'flat',
|
||||||
'unit': it['unit']}
|
'unit_prices': it['unit_price'], 'unit': it['unit']}
|
||||||
for dk, dv in sorted((it.get('dimensions') or {}).items()):
|
for dk, dv in sorted((it.get('dimensions') or {}).items()):
|
||||||
item[dk] = dv
|
item[dk] = dv
|
||||||
pricings.append(item)
|
pricings.append(item)
|
||||||
@ -1796,15 +1844,41 @@ async def _h_apply_model_pricing(sor, params, ctx):
|
|||||||
model_id = getattr(recs[0], "id", "")
|
model_id = getattr(recs[0], "id", "")
|
||||||
old_ppid = getattr(recs[0], "ppid", "") or ""
|
old_ppid = getattr(recs[0], "ppid", "") or ""
|
||||||
factors = sorted(set((it.get("factor") or "flat") for it in mitems))
|
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,
|
results.append({"model": vmid, "ok": False,
|
||||||
"error": "同一模型混用多个计价因子 %s——请拆分为多个定价方案" % factors})
|
"error": "token 因子 %s 与按量因子 %s 混在一个模型——"
|
||||||
|
"计费单位不同,请拆分为多个定价方案" % (token_hit, non_token)})
|
||||||
continue
|
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()
|
dims_all = set()
|
||||||
for it in mitems:
|
for it in mitems:
|
||||||
dims_all.update((it.get("dimensions") or {}).keys())
|
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 完整(不落库先验证)
|
# 引擎试算护栏:渲染一遍确认合法 YAML + fields 完整(不落库先验证)
|
||||||
try:
|
try:
|
||||||
parsed = yaml.safe_load(yaml_str)
|
parsed = yaml.safe_load(yaml_str)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user