Commit 0ca86060 by renyizhao

模型价格精度调整

parent 0d2f60f7
...@@ -234,9 +234,17 @@ public class AppAiModelController { ...@@ -234,9 +234,17 @@ public class AppAiModelController {
String billingExpr = (String) pricing.get("billing_expr"); String billingExpr = (String) pricing.get("billing_expr");
vo.setBillingMode(billingMode); vo.setBillingMode(billingMode);
vo.setBillingExpr(billingExpr); vo.setBillingExpr(billingExpr);
// 提前提取 billing_usage_schema,用于 tier 计费字段单位推断(happyhorse 等按秒/次计费模型)
Map<String, Object> billingUsageSchemaForTiers = null;
Object schemaObjForTiers = pricing.get("billing_usage_schema");
if (schemaObjForTiers instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> schemaMap = (Map<String, Object>) schemaObjForTiers;
billingUsageSchemaForTiers = schemaMap;
}
// 解析分档表达式 // 解析分档表达式
if ("tiered_expr".equals(billingMode) && billingExpr != null) { if ("tiered_expr".equals(billingMode) && billingExpr != null) {
List<AppModelPricingVO.TierVO> tiers = parseTieredExpr(billingExpr, groupRatio); List<AppModelPricingVO.TierVO> tiers = parseTieredExpr(billingExpr, groupRatio, billingUsageSchemaForTiers);
vo.setTiers(tiers); vo.setTiers(tiers);
// 解析条件乘数 // 解析条件乘数
List<AppModelPricingVO.ConditionMultiplierVO> conditionMultipliers = parseConditionMultipliers(billingExpr); List<AppModelPricingVO.ConditionMultiplierVO> conditionMultipliers = parseConditionMultipliers(billingExpr);
...@@ -253,28 +261,30 @@ public class AppAiModelController { ...@@ -253,28 +261,30 @@ public class AppAiModelController {
if (vo.getQuotaType() != null && vo.getQuotaType() == 1) { if (vo.getQuotaType() != null && vo.getQuotaType() == 1) {
// 按次计费(quota_type=1):使用原始 group_ratio.default,不叠加 puhui 业务系数 // 按次计费(quota_type=1):使用原始 group_ratio.default,不叠加 puhui 业务系数
BigDecimal perCallPrice = modelPrice.multiply(plainGroupRatio) BigDecimal perCallPrice = modelPrice.multiply(plainGroupRatio)
.setScale(2, RoundingMode.HALF_UP); .setScale(4, RoundingMode.HALF_UP);
vo.setInputPrice(perCallPrice); vo.setInputPrice(perCallPrice);
vo.setOutputPrice(perCallPrice); vo.setOutputPrice(perCallPrice);
} else { } else {
// 按量计费(quota_type=0):应用 groupRatio(含 BUSINESS_RATIO) // 按量计费(quota_type=0):应用 groupRatio(含 BUSINESS_RATIO)。
BigDecimal inputPrice = modelPrice.add(modelRatio).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP); // 精度保留 4 位小数,避免 modelRatio × groupRatio(如 0.1435 × 2 = 0.287)
// 被四舍五入到 2 位后丢失精度;前端 formatPrice 用 toFixed(4) 去末尾 0 显示。
BigDecimal inputPrice = modelPrice.add(modelRatio).multiply(groupRatio).setScale(4, RoundingMode.HALF_UP);
vo.setInputPrice(inputPrice); vo.setInputPrice(inputPrice);
vo.setOutputPrice(modelPrice.add(modelRatio.multiply(completionRatio)).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP)); vo.setOutputPrice(modelPrice.add(modelRatio.multiply(completionRatio)).multiply(groupRatio).setScale(4, RoundingMode.HALF_UP));
// 缓存价格 = cache_ratio × 输入价格(输入价格已包含 groupRatio) // 缓存价格 = cache_ratio × 输入价格(输入价格已包含 groupRatio)
vo.setCacheReadPrice(cacheRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP)); vo.setCacheReadPrice(cacheRatio.multiply(inputPrice).setScale(4, RoundingMode.HALF_UP));
vo.setCacheCreatePrice(createCacheRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP)); vo.setCacheCreatePrice(createCacheRatio.multiply(inputPrice).setScale(4, RoundingMode.HALF_UP));
// 图片输入价格 = image_ratio × 输入价格 // 图片输入价格 = image_ratio × 输入价格
if (imageRatio != null && imageRatio.compareTo(BigDecimal.ZERO) > 0) { if (imageRatio != null && imageRatio.compareTo(BigDecimal.ZERO) > 0) {
vo.setImageInputPrice(imageRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP)); vo.setImageInputPrice(imageRatio.multiply(inputPrice).setScale(4, RoundingMode.HALF_UP));
} }
// 音频输入价格 = audio_ratio × 输入价格 // 音频输入价格 = audio_ratio × 输入价格
if (audioRatio != null && audioRatio.compareTo(BigDecimal.ZERO) > 0) { if (audioRatio != null && audioRatio.compareTo(BigDecimal.ZERO) > 0) {
vo.setAudioInputPrice(audioRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP)); vo.setAudioInputPrice(audioRatio.multiply(inputPrice).setScale(4, RoundingMode.HALF_UP));
} }
// 音频补全价格 = audio_completion_ratio × 音频输入价格 // 音频补全价格 = audio_completion_ratio × 音频输入价格
if (audioCompletionRatio != null && audioCompletionRatio.compareTo(BigDecimal.ZERO) > 0 && vo.getAudioInputPrice() != null) { if (audioCompletionRatio != null && audioCompletionRatio.compareTo(BigDecimal.ZERO) > 0 && vo.getAudioInputPrice() != null) {
vo.setAudioCompletionPrice(audioCompletionRatio.multiply(vo.getAudioInputPrice()).setScale(2, RoundingMode.HALF_UP)); vo.setAudioCompletionPrice(audioCompletionRatio.multiply(vo.getAudioInputPrice()).setScale(4, RoundingMode.HALF_UP));
} }
} }
} }
...@@ -551,7 +561,8 @@ public class AppAiModelController { ...@@ -551,7 +561,8 @@ public class AppAiModelController {
* 2. 后续档位 conditionDesc 包含前一档 params 的残骸 * 2. 后续档位 conditionDesc 包含前一档 params 的残骸
* 3. 最后一档(无 ? 的兜底档)被错误地给 null * 3. 最后一档(无 ? 的兜底档)被错误地给 null
*/ */
private List<AppModelPricingVO.TierVO> parseTieredExpr(String billingExpr, BigDecimal groupRatio) { private List<AppModelPricingVO.TierVO> parseTieredExpr(String billingExpr, BigDecimal groupRatio,
Map<String, Object> billingUsageSchema) {
List<AppModelPricingVO.TierVO> tiers = new java.util.ArrayList<>(); List<AppModelPricingVO.TierVO> tiers = new java.util.ArrayList<>();
if (billingExpr == null || billingExpr.isEmpty()) { if (billingExpr == null || billingExpr.isEmpty()) {
return tiers; return tiers;
...@@ -595,7 +606,7 @@ public class AppAiModelController { ...@@ -595,7 +606,7 @@ public class AppAiModelController {
tier.setName(info.label); tier.setName(info.label);
// 解析参数部分,提取各倍率 // 解析参数部分,提取各倍率
parseTierParams(tier, info.params); parseTierParams(tier, info.params, billingUsageSchema);
// 提取条件:在 (上一个 tier 结束 + 1) 到当前 tier 开始的段落里找顶层 ? // 提取条件:在 (上一个 tier 结束 + 1) 到当前 tier 开始的段落里找顶层 ?
int segStart = (i == 0) ? 0 : tierInfos.get(i - 1).end + 1; int segStart = (i == 0) ? 0 : tierInfos.get(i - 1).end + 1;
...@@ -717,21 +728,29 @@ public class AppAiModelController { ...@@ -717,21 +728,29 @@ public class AppAiModelController {
/** /**
* 解析 tier 参数部分,提取各扩展价格倍率 * 解析 tier 参数部分,提取各扩展价格倍率
* 格式: p * x + c * y + cr * a + cc * b + cc1h * c1 + img * d + img_o * e + ai * f + ao * g * 格式: p * x + c * y + cr * a + cc * b + cc1h * c1 + img * d + img_o * e + ai * f + ao * g
* 或 u("field") * X [/ 1000000] (seedance 风格按 token 计费,X 即每百万 token 价格) * 或 u("field") * X [/ 1000000] (动态计费:tokens 为每百万 token,其余字段按 schema.unit)
*
* @param billingUsageSchema 计费用法 schema(用于推断 u("field") 的字段单位)
*/ */
private void parseTierParams(AppModelPricingVO.TierVO tier, String params) { private void parseTierParams(AppModelPricingVO.TierVO tier, String params,
Map<String, Object> billingUsageSchema) {
if (params == null || params.isEmpty()) { if (params == null || params.isEmpty()) {
return; return;
} }
// 提取 u("field") * X [/ 1000000](seedance 风格按 token 计费) // 提取 u("field") * X [/ 1000000](动态计费)
// 例如 u("tokens") * 29.14 / 1000000 => tokensMultiplier = 29.14 // 例如 u("tokens") * 29.14 / 1000000 => tokensMultiplier = 29.14(每百万 token)
// u("seconds") * 0.4104 => tokensMultiplier = 0.4104(每 1 秒)
java.util.regex.Pattern tokensPattern = java.util.regex.Pattern.compile( java.util.regex.Pattern tokensPattern = java.util.regex.Pattern.compile(
"u\\s*\\(\\s*\"(\\w+)\"\\s*\\)\\s*\\*\\s*([\\d.]+)\\s*(?:/\\s*1000000)?" "u\\s*\\(\\s*\"(\\w+)\"\\s*\\)\\s*\\*\\s*([\\d.]+)\\s*(?:/\\s*1000000)?"
); );
java.util.regex.Matcher tokensMatcher = tokensPattern.matcher(params); java.util.regex.Matcher tokensMatcher = tokensPattern.matcher(params);
if (tokensMatcher.find()) { if (tokensMatcher.find()) {
tier.setTokensMultiplier(new BigDecimal(tokensMatcher.group(2)).setScale(4, RoundingMode.HALF_UP)); String field = tokensMatcher.group(1);
BigDecimal multiplier = new BigDecimal(tokensMatcher.group(2)).setScale(4, RoundingMode.HALF_UP);
tier.setTokensMultiplier(multiplier);
tier.setUsageField(field);
tier.setUsageUnit(resolveUsageUnit(billingUsageSchema, field));
} }
// 提取 p * x // 提取 p * x
...@@ -799,6 +818,25 @@ public class AppAiModelController { ...@@ -799,6 +818,25 @@ public class AppAiModelController {
} }
/** /**
* 通过 billing_usage_schema 推断 u("field") 引用字段的单位。
* 例如 field=seconds → unit=second,field=tokens → unit=token,field=input_images → unit=count。
* 推断失败时返回 null,由前端兜底为 "token"。
*/
private String resolveUsageUnit(Map<String, Object> billingUsageSchema, String field) {
if (billingUsageSchema == null || field == null || field.isEmpty()) {
return null;
}
Object fieldDef = billingUsageSchema.get(field);
if (!(fieldDef instanceof Map)) {
return null;
}
@SuppressWarnings("unchecked")
Map<String, Object> fieldMap = (Map<String, Object>) fieldDef;
Object unit = fieldMap.get("unit");
return unit == null ? null : unit.toString();
}
/**
* 简化条件描述 * 简化条件描述
*/ */
private String simplifyCondition(String condition) { private String simplifyCondition(String condition) {
......
...@@ -152,12 +152,27 @@ public class AppModelPricingVO { ...@@ -152,12 +152,27 @@ public class AppModelPricingVO {
@Schema(description = "音频输出倍率") @Schema(description = "音频输出倍率")
private BigDecimal audioOutputRatio; private BigDecimal audioOutputRatio;
/** /**
* 按 token 计费的倍率(seedance 风格 u("tokens") * X / 1000000 中的 X)。 * 按 usage 计费的倍率(seedance / happyhorse 风格 u("field") * X 中的 X)。
* 适用于 openai-video 等按 token 数动态计费的档位, * 适用于 openai-video 等按动态字段(token/second/count)计费的档位。
* 单位 = 元/百万 token。前端展示时优先于 inputRatio/outputRatio。 * 单位 = 元 / 每 usageUnit 单位(tokens 时为每百万 token,需配合 schema 推断)。
* 前端展示时优先于 inputRatio/outputRatio。
*/ */
@Schema(description = "按 token 计费的倍率(seedance 风格,每百万 token 价格)") @Schema(description = "按 usage 计费的倍率(u(\"field\") * X 中的 X)")
private BigDecimal tokensMultiplier; private BigDecimal tokensMultiplier;
/**
* tokensMultiplier 对应的计费字段名(如 tokens/seconds)。
* 仅当 tokensMultiplier 非空时有意义,用于前端明确当前档位在计什么。
*/
@Schema(description = "tokensMultiplier 对应的计费字段名(如 tokens/seconds/input_images)")
private String usageField;
/**
* usageField 在 billing_usage_schema 中的 unit(如 token/second/count)。
* 仅当 tokensMultiplier 非空时有意义,用于前端选择展示单位文案(token/秒/次)。
*/
@Schema(description = "usageField 在 schema 中的 unit(token/second/count),用于前端选择展示单位文案")
private String usageUnit;
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment