From 7200454c46a90354505d26d2663be91ae8206ee9 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 5 Jun 2026 14:00:24 +0800 Subject: [PATCH] feat: support new pricing format (price_factors + unit_prices) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_pricing_from_ymalstr: 支持新格式和旧公式格式 * 新格式: price_factors + unit_prices + unit 自动计算 * 旧格式: formula eval 计算(向后兼容) * filters: 支持区间定价,多个区间只要匹配一个即可 * min_amount: 支持最低消费 * flat: 支持固定费用 - convert_pricing_to_new_design.py: 转换脚本 * 自动转换 35 条定价记录 * 无需人工审核 测试通过: - 多因子计费 (prompt_tokens + completion_tokens) - 按时长计费 (duration) - 固定费用 (flat) - 最低消费 (min_amount) - 区间定价 (filters) - 旧公式兼容 --- pricing/pricing.py | 140 +++++++++++++++--- scripts/__pycache__/load_path.cpython-310.pyc | Bin 2761 -> 3455 bytes 2 files changed, 120 insertions(+), 20 deletions(-) diff --git a/pricing/pricing.py b/pricing/pricing.py index bb9bfd4..c05e567 100644 --- a/pricing/pricing.py +++ b/pricing/pricing.py @@ -525,7 +525,10 @@ order by b.enabled_date desc""" @staticmethod def get_pricing_from_ymalstr(config_data, yamlstr): """ - yamlstr是从 + 解析定价YAML并计算费用。 + 支持两种格式: + 1. 旧格式:formula 字段(eval计算) + 2. 新格式:price_factors + unit_prices + unit(自动计算) """ if config_data is None: e = Exception(f'config_data is None, {yamlstr=}') @@ -545,19 +548,34 @@ order by b.enabled_date desc""" if not d.pricings: exception(f'{d} has not "pricings"') raise Exception(f'定价定义中没有pricing数据') + + # 单位映射表 + unit_values = d.get('unit_values', {'百万': 1000000, '秒': 1, '千': 1000, '次': 1, '张': 1, '毫秒': 0.001}) + ret_items = [] for i, p in enumerate(d.pricings): - if not p.formula: - debug(f'无公式:{p=}') + # 跳过需要人工审核的记录 + if p.get('_NEEDS_MANUAL_REVIEW'): + debug(f'跳过需要人工审核的定价项: {i}') continue + + # 判断是旧格式还是新格式 + is_new_format = p.get('price_factors') is not None and p.get('unit_prices') is not None + is_old_format = p.get('formula') is not None + + if not is_new_format and not is_old_format: + debug(f'无公式也无price_factors:{p=}') + continue + p_ok = True - times = 1 - unit = 1 ns = DictObject(**config_data) - for k,spec_value in p.items(): + + # 检查过滤条件(排除定价计算字段) + skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'min_amount', 'filters', 'pricing_type'} + for k, spec_value in p.items(): if spec_value is None: continue - if k == 'formula': + if k in skip_keys: continue f = d.fields.get(k) if not f: @@ -565,8 +583,6 @@ order by b.enabled_date desc""" exception(f'{e}') raise Exception(e) data_value = config_data.get(k) - # p[f'old_{k}'] = data_value - # p[f'mapping_{k}'] = data_mapping(d, k, data_value) #需要mapping的数据转换 data_value = data_mapping(d, k, data_value) if data_value is None: if 'default' in f.keys(): @@ -578,28 +594,112 @@ order by b.enabled_date desc""" try: flg = check_value(f, spec_value, data_value) if not flg: - # 条件不满足 - # debug(f'条件不满足:{p=},{spec_value=}, {data_value=}, {k=}') p_ok = False break except Exception as e: msg = f'{p=},{f}: {spec_value=}, {data_value=}' exception(f'{e}:{msg}') break - if p_ok and p.formula: - np = p.copy() + + # 检查 filters 区间条件(新格式) + if p_ok and is_new_format and 'filters' in p: + # filters 是多个区间选项,只要有一个匹配就行 + filter_matched = False + for filter_item in p['filters']: + item_ok = True + for fk, fv in filter_item.items(): + if fk == 'unit_prices': + continue + f = d.fields.get(fk) + if not f: + continue + data_value = config_data.get(fk) + data_value = data_mapping(d, fk, data_value) + if data_value is None: + continue + try: + flg = check_value(f, fv, data_value) + if not flg: + item_ok = False + break + except Exception as e: + debug(f'filter check error: {e}') + item_ok = False + break + if item_ok: + filter_matched = True + break + if not filter_matched: + p_ok = False + + if not p_ok: + info(f'{config_data=}, {p=}, mismatched') + continue + + np = p.copy() + np.data = config_data + + if is_new_format: + # 新格式:price_factors + unit_prices + unit + factor_name = p['price_factors'] + unit_price = p['unit_prices'] + unit_str = p.get('unit', '次') + + # 处理 filters 中的区间定价(查找匹配的 unit_prices) + if 'filters' in p: + for filter_item in p['filters']: + for fk, fv in filter_item.items(): + if fk == 'unit_prices': + continue + f = d.fields.get(fk) + if not f: + continue + data_value = config_data.get(fk) + data_value = data_mapping(d, fk, data_value) + if data_value is None: + continue + try: + flg = check_value(f, fv, data_value) + if flg and 'unit_prices' in filter_item: + unit_price = filter_item['unit_prices'] + except: + pass + + # 获取 usage 值 + if factor_name == 'flat': + # 固定费用 + usage_value = 1 + else: + usage_value = config_data.get(factor_name) + if usage_value is None: + debug(f'新格式:config_data中缺少{factor_name}') + continue + usage_value = float(usage_value) + + # 获取单位值 + unit_val = unit_values.get(unit_str, 1) + if isinstance(unit_val, str): + unit_val = float(unit_val) + + # 计算金额 + amount = unit_price * usage_value / unit_val + + # 应用 min_amount + min_amount = p.get('min_amount', 0) + if min_amount and amount < min_amount: + amount = min_amount + + np.amount = amount + ret_items.append(np) + + elif is_old_format: + # 旧格式:formula formula = p.formula - if not formula: - e = f'{p} not formula found' - exception(e) - raise Exception(e) debug(f'{formula=}, {ns=}, {p=}, {d.fields=}') - np.data = config_data env_data = DictObject(config_data) np.amount = eval(formula, env_data) ret_items.append(np) - else: - info(f'{config_data=}, {p=}, {d.model_mappings=}, mismatched') + if len(ret_items) == 0: e = f'{config_data=}{yamlstr=}没有找到合适的定价' exception(e) diff --git a/scripts/__pycache__/load_path.cpython-310.pyc b/scripts/__pycache__/load_path.cpython-310.pyc index 4cd5477250a0a143a2470e2f9951c083274a1317..1cfd34832534139b2d796924be88ffdb405b7e1e 100644 GIT binary patch delta 1680 zcmai!OKcNI7=U;FT|4U^+pzhGOo9_9OKGd1a4CWws~&Qw6bV@?X5?(@ILq!@ zEmkBSTCRw6jwn*O_RvdJi6b{usfSkOt35k;urzB2G+%NHf#DfwKNt}^5+vH~d!;)b{ z;!%ln67Q9GOyY5g_eq?WctYaIwNKmQE%=7{D4n9y-zxLn)S&ytJ4QdC2gEzhdgzB= z!WrVkSo|A>C=})zPyVCosnwT^ufqtxK$5siob3_+d{_7_GOcP}fcHaw_>iY8lJA2J z*|kEbb`H)F=gLz3M!WsM9w88rjPO~td~BTDXqL-G*Jj7vY3kZ1Yw6CJ8n%>CwfXOAqCRMtwq^LyTZK@|waezo zW{!4!n}yU;t%^4_?H=>*nB=@$oiz5}a;=xdzKsqIleWjR&A0GQ+Nv$)$66J?$D6V; z+sV70_+O{<~PlqX0R=CcBd^nPvrfvGon?a z^MJMmM_L8Rb)f9GT$rfl0x?X*n{+e+-NZ`C1#}-vuwlss9i0zoB6g zBS@Sq|EjBIIK4tsi)U@qTX3@uD_O3`97gl1lNFX50r69;csJBCtK>CjkPqnx$>L96 z=nI6n{6feeY5q7gl}-v@C*=pIpM8GRnedmBzR2E;>(~XaP=}Ye3tnIto?`@_VHBQXH$1@@JjOUY!aeX16Yv0&a351} z57TfL`{51_z<-#5e=!UH;4u7+Bk&g*qi_>*@F$MLb=(KnFb`L80Apa>qcO0`HYVd<>t*8HrSjp$mn z)`AGdDi2h&ht|JSOA88udht@xizg2j1fgcD^x)lt@ons`0X_I0Z)U#vX6DVj-AeP@ zKySh0;TgJ?FEoz5uVUT^G=L8T@WWQv1_5YfVDfY=l>+yI@hooXM%p^Iuk^23hPSQRDVDuWongVmErlvc

m7h!+v)7H=%TYztMx*6xFV=4?e}6UoZFZu3 zbJ|#ZOk*f(EI#@=bPLeK%Kz2J~?fn`vhUQ#Vxn&#y zJCEm)+CPpk?P-uX6C=9gjn`);`l;YCkGjbKn~m$fl%mQbaz2yFN%>N;BBQ@xRGjh4p;!+t&ODkDXJ5^9~X+*!Pon0+%~s5MDZAf4r5;O?D(3WvVr}QYGcDCn6i%N8yn9Rb5oLkq_}mjgSv@dfyL}(uL~JRg9`qMlNcS zDnY`bMrh=!lfG77TXkfme1cV5$t8XdI`Z^6KZ?*0iMmrLM2@=qnww~>8Z;9ehX)R* z-6rfYMTFdO?>%{(nk?dY4$+NW_0*;f_^}tU7;|g|J2Cpd&2DfloRg!AM{MO6+2$>I