From 1985f442d799bb70fab4d9dea549ce0b965ea294 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 9 Sep 2026 13:34:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(display):=20get=5Fpricing=5Fdisplay=20?= =?UTF-8?q?=E6=A1=A3=E4=BD=8D=E5=AE=9A=E4=BB=B7=E5=B1=95=E5=BC=80+?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=E9=94=AE=E4=B8=8D=E6=B3=84=E6=BC=8F+?= =?UTF-8?q?=E5=8C=BA=E9=97=B4=E5=80=BC=E7=BE=8E=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三个展示层缺陷一次根治(计费引擎 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 消费方结构兼容) --- README.md | 46 ++++++++++++++- pricing/pricing.py | 144 ++++++++++++++++++++++++++++++++------------- 2 files changed, 147 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 6c67de7..14bfe99 100644 --- a/README.md +++ b/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 使用指南 diff --git a/pricing/pricing.py b/pricing/pricing.py index cbc37ee..e0fb8c3 100644 --- a/pricing/pricing.py +++ b/pricing/pricing.py @@ -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,