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:
parent
38390ae01f
commit
1985f442d7
46
README.md
46
README.md
@ -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 使用指南
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user