Commit 17beaa7b by renyizhao

价格展示修改

parent 763e8dce
......@@ -200,19 +200,6 @@ public class AppAiModelController {
vo.setDescription((String) item.get("description"));
vo.setTags((String) item.get("tags"));
// endpoints
Object ep = item.get("endpoints");
if (ep instanceof String) {
try {
vo.setSupportedEndpoints(com.luhu.computility.framework.common.util.json.JsonUtils.parseArray((String) ep, String.class));
} catch (Exception ignored) {
}
} else if (ep instanceof List) {
@SuppressWarnings("unchecked")
List<String> eps = (List<String>) ep;
vo.setSupportedEndpoints(eps);
}
// enable_groups
Object eg = item.get("enable_groups");
if (eg instanceof List) {
......@@ -224,6 +211,14 @@ public class AppAiModelController {
// 合并 pricing
Map<String, Object> pricing = pricingIndex.get(modelName);
if (pricing != null) {
// supportedEndpoints:从 pricing 数据的 supported_endpoint_types 读取
// (projects memory 硬约束:不可为 null,来源于 NewAPI 的 supported_endpoint_types)
Object pricingEps = pricing.get("supported_endpoint_types");
if (pricingEps instanceof List) {
@SuppressWarnings("unchecked")
List<String> pEps = (List<String>) pricingEps;
vo.setSupportedEndpoints(pEps);
}
vo.setQuotaType(toInteger(pricing.get("quota_type")));
vo.setModelRatio(toBigDecimal(pricing.get("model_ratio")));
vo.setModelPrice(toBigDecimal(pricing.get("model_price")));
......@@ -549,6 +544,12 @@ public class AppAiModelController {
/**
* 解析分档计费表达式
* 格式: (条件1 ? tier("档位1", p*x + c*y + cr*a + cc*b + ...) : 条件2 ? tier("档位2", ...) : tier("档位3", ...)) * (条件 ? 倍率 : 1) * ...
*
* 使用括号深度匹配(而非简单正则)来正确支持参数内嵌的 u("field") 嵌套调用,
* 否则旧实现会把 u("tokens") 的内层 ) 误识别为外层 tier(...) 的闭合,导致:
* 1. params 截断为 u("tokens"
* 2. 后续档位 conditionDesc 包含前一档 params 的残骸
* 3. 最后一档(无 ? 的兜底档)被错误地给 null
*/
private List<AppModelPricingVO.TierVO> parseTieredExpr(String billingExpr, BigDecimal groupRatio) {
List<AppModelPricingVO.TierVO> tiers = new java.util.ArrayList<>();
......@@ -557,87 +558,63 @@ public class AppAiModelController {
}
try {
// 找到最后一个 tier(...) 结束的位置(用于截取分档表达式部分)
int lastTierEnd = -1;
int searchStart = 0;
while (true) {
int tierStart = billingExpr.indexOf("tier(", searchStart);
if (tierStart == -1) break;
int depth = 0;
int i = tierStart;
for (; i < billingExpr.length(); i++) {
char ch = billingExpr.charAt(i);
if (ch == '(') depth++;
else if (ch == ')') {
depth--;
if (depth == 0) {
lastTierEnd = i + 1;
break;
}
}
}
searchStart = i + 1;
}
if (lastTierEnd == -1) {
// 1. 用括号深度匹配找出所有 tier(...) 的精确 [start, end] 范围
List<int[]> tierRanges = findAllTierRanges(billingExpr);
if (tierRanges.isEmpty()) {
return tiers;
}
// 提取分档表达式部分(从开头到最后一个 tier(...) 结束)
// 截取到最后一个 tier(...) 结束,去掉尾部的条件乘数
int lastTierEnd = tierRanges.get(tierRanges.size() - 1)[1];
String tierExpr = billingExpr.substring(0, lastTierEnd);
// 匹配 tier("名称", 参数部分) 的正则
java.util.regex.Pattern tierPattern = java.util.regex.Pattern.compile(
"tier\\s*\\(\\s*\"([^\"]+)\"\\s*,\\s*((?:[^)]+))\\s*\\)"
);
java.util.regex.Matcher tierMatcher = tierPattern.matcher(tierExpr);
// 收集所有 tier 的位置和内容
java.util.List<java.util.regex.MatchResult> tierMatches = new java.util.ArrayList<>();
while (tierMatcher.find()) {
tierMatches.add(tierMatcher.toMatchResult());
// 2. 解析每个 tier 的标签和参数
List<TierInfo> tierInfos = new java.util.ArrayList<>();
for (int[] range : tierRanges) {
int start = range[0];
int end = range[1];
// tier(...) 内部的全部内容(去掉首尾括号)
String inner = billingExpr.substring(start + "tier(".length(), end).trim();
// 找顶层逗号(depth=0,不在字符串里)来切分 label 和 params
int commaIdx = findTopLevelComma(inner);
if (commaIdx < 0) continue;
String label = inner.substring(0, commaIdx).trim();
if (label.startsWith("\"") && label.endsWith("\"")) {
label = label.substring(1, label.length() - 1);
}
String params = inner.substring(commaIdx + 1).trim();
tierInfos.add(new TierInfo(start, end, label, params));
}
if (tierMatches.isEmpty()) {
if (tierInfos.isEmpty()) {
return tiers;
}
// 遍历每个 tier,提取其对应的条件
for (int i = 0; i < tierMatches.size(); i++) {
// 3. 遍历每个 tier,提取其条件描述
for (int i = 0; i < tierInfos.size(); i++) {
TierInfo info = tierInfos.get(i);
AppModelPricingVO.TierVO tier = new AppModelPricingVO.TierVO();
java.util.regex.MatchResult mr = tierMatches.get(i);
tier.setName(mr.group(1));
tier.setName(info.label);
// 解析参数部分,提取各倍率
String params = mr.group(2);
parseTierParams(tier, params);
// 提取条件:从上一个 tier 结束(或表达式开头)到当前 tier 开始的 ? 之前
int tierStart = mr.start();
String segment;
if (i == 0) {
// 第一个 tier:条件在表达式开头到 tier 之前
segment = tierExpr.substring(0, tierStart);
} else {
// 后续 tier:条件在上一个 tier 结束后到当前 tier 之前
int prevTierEnd = tierMatches.get(i - 1).end();
segment = tierExpr.substring(prevTierEnd, tierStart);
}
// 找到 ? 的位置,? 之前是条件
int questionMarkIndex = segment.lastIndexOf('?');
if (questionMarkIndex > 0) {
String condition = segment.substring(0, questionMarkIndex).trim();
// 清理:去掉开头的 ( : 和结尾的空格
condition = condition.replaceAll("^[\\s(:)]+", "");
// 去掉首尾括号对
parseTierParams(tier, info.params);
// 提取条件:在 (上一个 tier 结束 + 1) 到当前 tier 开始的段落里找顶层 ?
int segStart = (i == 0) ? 0 : tierInfos.get(i - 1).end + 1;
int segEnd = info.start;
String segment = tierExpr.substring(segStart, segEnd);
int qIdx = findTopLevelQuestionMark(segment);
if (qIdx >= 0) {
String condition = segment.substring(0, qIdx).trim();
// 清理前导 ( : 和空白
condition = condition.replaceAll("^[\\s(:]+", "");
// 去掉首尾成对的括号
while (condition.startsWith("(") && condition.endsWith(")")) {
condition = condition.substring(1, condition.length() - 1).trim();
}
tier.setConditionDesc(simplifyCondition(condition));
} else if (i == tierMatches.size() - 1) {
// 最后一个 tier 没有 ?,是默认档位
tier.setConditionDesc(null);
} else if (i == tierInfos.size() - 1) {
// 最后一个 tier 没有 ?: 出现,是默认兜底档位
tier.setConditionDesc("默认");
}
tiers.add(tier);
......@@ -649,14 +626,114 @@ public class AppAiModelController {
}
/**
* tier 解析过程的中间结果
*/
private static class TierInfo {
final int start;
final int end;
final String label;
final String params;
TierInfo(int start, int end, String label, String params) {
this.start = start;
this.end = end;
this.label = label;
this.params = params;
}
}
/**
* 用括号深度匹配找出表达式中所有 tier(...) 的精确起止位置。
* 兼容参数里嵌套的 u("field") 调用,避免被内层 ) 误截断。
*/
private List<int[]> findAllTierRanges(String expr) {
List<int[]> ranges = new java.util.ArrayList<>();
int searchFrom = 0;
while (true) {
int idx = expr.indexOf("tier(", searchFrom);
if (idx < 0) break;
// depth 初始为 1(外层 tier( 的左括号)
int depth = 1;
int end = -1;
for (int i = idx + "tier(".length(); i < expr.length(); i++) {
char ch = expr.charAt(i);
if (ch == '(') depth++;
else if (ch == ')') {
depth--;
if (depth == 0) {
end = i;
break;
}
}
}
if (end < 0) break;
ranges.add(new int[]{idx, end});
searchFrom = end + 1;
}
return ranges;
}
/**
* 在字符串中找到深度=0 且不在引号内的第一个逗号位置(用于切分 tier(label, params))
*/
private int findTopLevelComma(String inner) {
int depth = 0;
boolean inQuote = false;
for (int i = 0; i < inner.length(); i++) {
char ch = inner.charAt(i);
if (ch == '"') {
inQuote = !inQuote;
} else if (!inQuote) {
if (ch == '(') depth++;
else if (ch == ')') depth--;
else if (ch == ',' && depth == 0) return i;
}
}
return -1;
}
/**
* 在段落里找顶层 ? 的位置(深度=0、未在引号内)。
* 返回最右侧的顶层 ?,因为三元表达式段落里通常只有一个 ? 在末尾。
*/
private int findTopLevelQuestionMark(String segment) {
int depth = 0;
int lastQ = -1;
boolean inQuote = false;
for (int i = 0; i < segment.length(); i++) {
char ch = segment.charAt(i);
if (ch == '"') {
inQuote = !inQuote;
} else if (!inQuote) {
if (ch == '(') depth++;
else if (ch == ')') depth--;
else if (ch == '?' && depth == 0) {
lastQ = i;
}
}
}
return lastQ;
}
/**
* 解析 tier 参数部分,提取各扩展价格倍率
* 格式: 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 价格)
*/
private void parseTierParams(AppModelPricingVO.TierVO tier, String params) {
if (params == null || params.isEmpty()) {
return;
}
// 提取 u("field") * X [/ 1000000](seedance 风格按 token 计费)
// 例如 u("tokens") * 29.14 / 1000000 => tokensMultiplier = 29.14
java.util.regex.Pattern tokensPattern = java.util.regex.Pattern.compile(
"u\\s*\\(\\s*\"(\\w+)\"\\s*\\)\\s*\\*\\s*([\\d.]+)\\s*(?:/\\s*1000000)?"
);
java.util.regex.Matcher tokensMatcher = tokensPattern.matcher(params);
if (tokensMatcher.find()) {
tier.setTokensMultiplier(new BigDecimal(tokensMatcher.group(2)).setScale(4, RoundingMode.HALF_UP));
}
// 提取 p * x
java.util.regex.Pattern pPattern = java.util.regex.Pattern.compile("p\\s*\\*\\s*([\\d.]+)");
java.util.regex.Matcher pMatcher = pPattern.matcher(params);
......
......@@ -151,6 +151,13 @@ public class AppModelPricingVO {
private BigDecimal audioInputRatio;
@Schema(description = "音频输出倍率")
private BigDecimal audioOutputRatio;
/**
* 按 token 计费的倍率(seedance 风格 u("tokens") * X / 1000000 中的 X)。
* 适用于 openai-video 等按 token 数动态计费的档位,
* 单位 = 元/百万 token。前端展示时优先于 inputRatio/outputRatio。
*/
@Schema(description = "按 token 计费的倍率(seedance 风格,每百万 token 价格)")
private BigDecimal tokensMultiplier;
}
/**
......
......@@ -212,8 +212,10 @@ wx:
# 芋道配置项,设置当前项目所有自定义的配置
computility:
new-api:
base-url: http://172.25.164.0:3000
admin-token: nfcJyVtYb6xtREHA2MyL4f4o5TDg/qU=
# 线上 NewAPI 地址(用于获取 /api/pricing 数据)
base-url: http://phslgld.hnluchuan.com:3000
# 线上 NewAPI 管理员 token(在 NewAPI 后台 → 用户管理 → 管理员账号详情获取)
admin-token: V33oMN4fI5H0wKSs0gXipXZ4WvU6DQ==
admin-user-id: 1
captcha:
enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
......
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