fix(display): get_pricing_display 档位定价展开+机制键不泄漏+区间值美化

三个展示层缺陷一次根治(计费引擎 get_pricing_from_ymalstr 独立路径零触碰):

1. 机制键泄漏:filters 列表中带 unit_prices 的价格档位(tier)被当扁平 filter
   提取,value_mode/单位定价混进 filter_labels→商店表格把它们当表头显示。
   现在 tier 不再进 item 级 filters,机制键显式排除。

2. 档位价丢失(词匹配误杀):旧 tiered 提取按「not k.endswith('_tokens')」
   剥维度键——prompt_tokens 既是真维度又以 _tokens 结尾被误杀,MiniMax-M3/
   qwen3.7-flash 分档价整条丢失只剩顶层兜底价(2.1 从未展示过)。
   词匹配不当裁决者的又一实例。现在每个档位展开为独立 item(条件+价各成一行),
   tiered 嵌套结构删除(全仓 grep 实证无消费方);顶层兜底价不展示
   (避免与某档同价的无条件行让客户困惑)。

3. 区间原始语法不可读:'0 =~= 512000'/'8 =~ 22' 直接透给客户。
   新增 beautify_filter_value(纯展示):between→数学区间记号,括号语义与
   check_value 一一对应(=~→[lo,hi) ~=→(lo,hi] =~=→[lo,hi] ~→(lo,hi)),
   in→'a / b / c',数字原样不换算。filter_labels 存美化值,filters 存原始值。

实测(离线+测试机数据形态):MiniMax-M3 六档全出含丢失的 2.1 ✓、
deepseek 忙闲时 [8,22)/[22,24)/[0,8) ✓、qwen-image-3.0-pro 平铺维度回归不变 ✓、
model 过滤回退兼容 ✓(llmage/sage/storage/account 消费方结构兼容)
This commit is contained in:
yumoqing 2026-09-09 13:34:18 +08:00
parent 38390ae01f
commit 1985f442d7
2 changed files with 147 additions and 43 deletions

View File

@ -419,17 +419,57 @@ Authorization: Bearer {token}
- `name`: 定价项目名称
- `pricing_type`: 定价类型(`per_use` 按次计费)
- `items`: 定价项列表
- `filters`: 过滤条件(如适用模型、分辨率等)
- `filter_labels`: 过滤条件的中文标签
- `filters`: 过滤条件(如适用模型、分辨率等),值为 YAML 原始值(供匹配用)
- `filter_labels`: 过滤条件的中文标签**美化后的展示值**(区间语法已转数学记号,见下)
- `price_factors`: 计价因子列表
- `factor`: 因子字段名
- `label`: 因子中文名称
- `unit_price`: 单位价格(已转换为展示价,无需再乘)
- `unit`: 单位(百万、秒、次等)
- `unit_label`: 单位标签(元/百万、元/秒等)
- `tiered`: 阶梯定价(仅当价格不同时展示)
- `display_text`: 人类可读的价格表文本
**返回示例(分档/忙闲时定价2026-09-09 起每档展开为独立 item**
YAML 里 `filters` 列表中带 `unit_prices` 的条目是价格档位tier如 MiniMax-M3 按输入长度分档、
deepseek 按忙闲时分档。旧版把档位塞进 `price_factors[0].tiered` 嵌套且用
`not k.endswith('_tokens')`」词匹配剥维度键,`prompt_tokens` 档位被误杀整条丢失
(只剩顶层兜底价,档位价从未展示)。现在每个档位展开为一个独立 item
```json
{
"items": [
{
"filters": {"prompt_tokens": "0 =~= 512000"},
"filter_labels": {"prompt_tokens": "[0, 512000]"},
"price_factors": [
{"factor": "uncache_tokens", "label": "非缓存输入Token",
"unit_price": 2.1, "unit": "百万", "unit_label": "元/百万"}
]
},
{
"filters": {"prompt_tokens": "512000 ~= 1000000"},
"filter_labels": {"prompt_tokens": "(512000, 1000000]"},
"price_factors": [
{"factor": "uncache_tokens", "label": "非缓存输入Token",
"unit_price": 4.2, "unit": "百万", "unit_label": "元/百万"}
]
}
]
}
```
要点:
- **区间值美化**`beautify_filter_value`,纯展示层):`filter_labels` 中 between 值转数学区间记号,
括号语义与计费引擎 `check_value` 完全一致——`=~``[lo, hi)``~``(lo, hi)`
`~=``(lo, hi]``=~=``[lo, hi]``filters` 保留原始值不动(内部匹配用)。
`value_mode: in``'a b c'``'a / b / c'`。数字原样不换算。
- **机制键不泄漏**`value_mode`、档位 `unit_prices` 是引擎机制键,绝不进 `filter_labels`
(旧版泄漏后被商店表格当表头显示)。
- **顶层兜底价不展示**:有档位的定价项,顶层 `unit_prices` 只是引擎在档位全不命中时的兜底
(计费路径 `get_pricing_from_ymalstr` 独立,不受展示层影响),展示它会出现一条无条件的
同价行让客户困惑,故只展开档位行。
---
## pricing_data 使用指南

View File

@ -130,6 +130,38 @@ def typevalue(v, t):
return v
return f(v)
# ── 展示层filter/tier 维度值可读化2026-09-09仅展示不参与计费比较──
# 区间操作符 → 数学区间记号,语义与 check_value 的 between 分支一一对应
_INTERVAL_BRACKETS = {
'=~': ('[', ')'), # lo <= v < hi
'~': ('(', ')'), # lo < v < hi
'~=': ('(', ']'), # lo < v <= hi
'=~=': ('[', ']'), # lo <= v <= hi
}
def beautify_filter_value(v, fdef=None, value_mode=None):
"""把 filter/tier 维度值渲染成可读文本(纯展示,绝不参与计费比较)。
- value_mode=between原始 '0 =~= 512000' '[0, 512000]'
'8 =~ 22' '[8, 22)'操作符语义对齐 check_value无歧义
- value_mode=in'a b c' 'a / b / c'
- 其余原样返回 resolution=1K
数字保持原值不转不加千分位与实际比较值一字不差
"""
vm = value_mode
if vm is None and isinstance(fdef, dict):
vm = fdef.get('value_mode')
s = str(v).strip()
if vm == 'between':
arr = s.split()
if len(arr) == 3 and arr[1] in _INTERVAL_BRACKETS:
lb, rb = _INTERVAL_BRACKETS[arr[1]]
return '%s%s, %s%s' % (lb, arr[0], arr[2], rb)
return s
if vm == 'in':
return ' / '.join(s.split())
return v
def check_value(field, spec_value, data_value, value_mode=None):
if value_mode is None:
value_mode = field.value_mode
@ -829,31 +861,45 @@ async def get_pricing_display(ppid, model=None):
continue
# 提取过滤条件role=filter 的字段)
# filters[k]=原始值(供 model 过滤等内部匹配filter_labels[label]=美化后的展示值
filters = {}
filter_labels = {}
skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'filters', 'min_amount', 'pricing_type', 'name'}
# filter/tier 内的机制键unit_prices=档位单价、value_mode=区间操作符提示,均非展示维度
_mechanism_keys = {'unit_prices', 'value_mode'}
# 先从 p.items() 中提取(直接字段)
# 先从 p.items() 中提取(item 级平铺维度,如视频 SR/resolution
for k, v in p.items():
if k in skip_keys:
continue
fdef = fields.get(k, {})
role = fdef.get('role', 'filter') if isinstance(fdef, dict) else 'filter'
if role == 'filter':
if not isinstance(fdef, dict):
fdef = {}
if fdef.get('role', 'filter') == 'filter':
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
filter_labels[fdef.get('label', k)] = beautify_filter_value(v, fdef)
# 再从 p['filters'] 列表中提取(如果存在)
# 再从 p['filters'] 列表中提取 item 级条件(如果存在)
# ⚠️ 列表中带 unit_prices 的条目是价格档位(tier),不是 item 级条件——
# 旧代码把它们当扁平 filter 提取,导致 value_mode/unit_prices 机制键泄漏进
# filter_labels被表格当成表头且多档被压扁只剩最后一档。档位由下方
# tier_rows 块单独展开成行2026-09-09 根治)。
raw_filters = p.get('filters')
if isinstance(raw_filters, list):
for fi in raw_filters:
if isinstance(fi, dict):
for k, v in fi.items():
fdef = fields.get(k, {})
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
if not isinstance(fi, dict) or 'unit_prices' in fi:
continue
vm = fi.get('value_mode')
for k, v in fi.items():
if k in _mechanism_keys:
continue
fdef = fields.get(k, {})
if not isinstance(fdef, dict):
fdef = {}
if fdef.get('role', 'filter') != 'filter':
continue
filters[k] = v
filter_labels[fdef.get('label', k)] = beautify_filter_value(v, fdef, vm)
# 新格式price_factors(scalar) + unit_prices(scalar) + unit
# 注意:生产数据中 price_factors 可能是 list、unit_prices 可能是 dict混合格式
@ -878,6 +924,52 @@ async def get_pricing_display(ppid, model=None):
# 构建 unit_label (元/单位)
unit_label = f"元/{unit_str}" if unit_str else ""
min_amount = p.get('min_amount', 0)
# 价格档位(tier)filters 列表中带 unit_prices 的条目(分档 token 定价、
# 忙闲时定价)。每档展开为独立 item条件+价各成一行)——旧实现把档位塞进
# price_factors[0]['tiered'] 嵌套结构且维度键按「not k.endswith('_tokens')」
# 词匹配误杀prompt_tokens 分档整条丢失MiniMax-M3 只剩兜底价 4.2
# 档位价 2.1 从未展示过。tiered 嵌套全仓无消费方2026-09-09 grep 实证),
# 展开为 item 对 llmage 卡片/商店表格反而更可读。
tier_filters = []
if isinstance(raw_filters, list):
for fi in raw_filters:
if isinstance(fi, dict) and fi.get('unit_prices') is not None:
tier_filters.append(fi)
if tier_filters:
# 只展示档位行:顶层 unit_prices 是引擎兜底价(无档位命中时用,计费
# 引擎 get_pricing_from_ymalstr 独立路径不受影响),不是文档定价档位——
# 展示它会在表格里出现与某档同价的无条件行,客户无法判断何时用哪个价
for fi in tier_filters:
tier_labels = dict(filter_labels) # item级条件如 model对每档都适用
tier_raw = dict(filters)
vm = fi.get('value_mode')
for k, v in fi.items():
if k in _mechanism_keys:
continue
kfdef = fields.get(k, {})
if not isinstance(kfdef, dict):
kfdef = {}
tier_raw[k] = v
tier_labels[kfdef.get('label', k)] = beautify_filter_value(v, kfdef, vm)
tier_item = {}
tier_item['price_factors'] = [{
'factor': factor_name,
'label': factor_label,
'unit_price': fi['unit_prices'],
'unit': unit_str,
'unit_label': unit_label
}]
if tier_raw:
tier_item['filters'] = tier_raw
tier_item['filter_labels'] = tier_labels
if min_amount:
tier_item['min_amount'] = min_amount
items.append(tier_item)
continue
price_factors_display = [{
'factor': factor_name,
'label': factor_label,
@ -886,35 +978,12 @@ async def get_pricing_display(ppid, model=None):
'unit_label': unit_label
}]
# 处理 filters: 提取适用条件(model)到item级区间条件放tiered
# 注意filters 已在上方提取(支持 dict 和 list 两种格式)
# tiered 仅用于价格不同的阶梯定价
tiered_pricing = []
if isinstance(p.get('filters'), list):
for fi in p['filters']:
if not isinstance(fi, dict):
continue
raw_tier_price = fi.get('unit_prices')
if raw_tier_price is None or raw_tier_price == unit_price:
continue
fi_copy = {k: v for k, v in fi.items()
if k not in ('unit_prices', 'value_mode')
and not k.endswith('_tokens')}
if fi_copy:
tiered_pricing.append({
'filters': fi_copy,
'unit_prices': raw_tier_price
})
item = {'price_factors': price_factors_display}
if filters:
item['filters'] = filters
item['filter_labels'] = filter_labels
min_amount = p.get('min_amount', 0)
if min_amount:
item['min_amount'] = min_amount
if tiered_pricing:
item['price_factors'][0]['tiered'] = tiered_pricing
items.append(item)
else:
# 旧格式formula
@ -980,11 +1049,6 @@ async def get_pricing_display(ppid, model=None):
ul = ul.replace('', currency_label)
if up is not None:
display_lines.append(f" - {label}: {up} {ul}{filter_text}")
if pf.get('tiered'):
for t in pf['tiered']:
t_filters = ', '.join(f"{k}={v}" for k, v in t.get('filters', {}).items())
t_price = t.get('unit_prices', '')
display_lines.append(f" · {t_filters}: {t_price} {ul}")
return {
'ppid': ppid,