一、概述
本脚本用于验证计费接口的计费准确性。通过提取 API 响应中的 token 使用量,结合配置的价格参数,计算期望的 amount 值,并与日志查询接口返回的实际 amount 进行比对。
二、涉及的接口
| 接口 | 说明 |
|---|---|
/v1/chat/completions-xxx | 基础用户接口 |
/v1/chat/completions-yyy | 分销商用户接口 |
/v1/chat/completions-zzz | 管理员用户接口 |
/api/xxx | 日志查询接口 |
/api/xxx/distributor | 分销商日志查询 |
/api/xxx/self | 普通用户消费明细查询 |
三、全局变量配置
在 Test Plan 的 User Defined Variables 中配置以下变量:
| 变量名 | 默认值 | 说明 |
|---|---|---|
RESPONSE_DELAY | 10000 | 延时(毫秒),等待数据落库 |
INPUT_PRICE | 0.8 | 输入价格(元/1M tokens) |
CACHE_PRICE | 0.4 | 缓存命中价格(元/1M tokens) |
OUTPUT_PRICE | 2.0 | 输出价格(元/1M tokens) |
PRICE_MULTIPLIER | 1.2 | 分销商倍率 |
SELF_MULTIPLIER | 1.2 | 普通用户倍率(在分销商基础上) |
四、数据关联流程
4.1 提取请求 ID
在请求后添加 Regular Expression Extractor:
Reference Name: REQUEST_ID
Regular Expression: x-xxx-request-id:\s*(.+)
Template: $1$
Match No.: 1
说明:
x-xxx-request-id是 API 响应头中的字段,用于唯一标识本次请求- 提取后存入变量
REQUEST_ID,供后续日志查询验证使用
4.2 提取 usage 信息
在同一个请求后添加 JSON Post Processor:
| Variable Name | JSON Path |
|---|---|
PROMPT_TOKENS | $.usage.prompt_tokens |
CACHED_TOKENS | $.usage.prompt_tokens_details.cached_tokens |
COMPLETION_TOKENS | $.usage.completion_tokens |
示例响应:
{
"usage": {
"prompt_tokens": 9,
"prompt_tokens_details": {
"cached_tokens": 0
},
"completion_tokens": 95,
"total_tokens": 104
},
"request_id": "2026062615162370368960030161971"
}
4.3 延时等待
在 /api/log 请求前添加 Constant Timer:
Timer Delay: ${RESPONSE_DELAY}
原因: API 返回成功不代表日志已写入数据库,需要等待数据落库。
4.4 验证逻辑
在 /api/log 请求后添加 JSR223 Assertion,使用 Groovy 脚本验证 amount。
五、计费公式
5.1 基础用户
expectedAmount = (普通输入 × INPUT_PRICE + 缓存命中 × CACHE_PRICE + 输出 × OUTPUT_PRICE) / 1M
其中:
- 普通输入 =
(PROMPT_TOKENS - CACHED_TOKENS) × INPUT_PRICE - 缓存命中 =
CACHED_TOKENS × CACHE_PRICE - 输出 =
COMPLETION_TOKENS × OUTPUT_PRICE
5.2 分销商用户
expectedAmount = (普通输入 × INPUT_PRICE + 缓存命中 × CACHE_PRICE + 输出 × OUTPUT_PRICE) × PRICE_MULTIPLIER / 1M
5.3 普通用户
totalMultiplier = PRICE_MULTIPLIER × SELF_MULTIPLIER
expectedAmount = (普通输入 × INPUT_PRICE + 缓存命中 × CACHE_PRICE + 输出 × OUTPUT_PRICE) × totalMultiplier / 1M
六、验证脚本示例
以普通用户为例(JSR223 Assertion):
def inputPrice = Double.parseDouble(vars.get("INPUT_PRICE") ?: "0.8")
def cachePrice = Double.parseDouble(vars.get("CACHE_PRICE") ?: "0.4")
def outputPrice = Double.parseDouble(vars.get("OUTPUT_PRICE") ?: "2.0")
def distributorMultiplier = Double.parseDouble(vars.get("PRICE_MULTIPLIER") ?: "1.2")
def selfMultiplier = Double.parseDouble(vars.get("SELF_MULTIPLIER") ?: "1.2")
// 提取 token 变量
def promptTokensStr = vars.get("SELF_PROMPT_TOKENS")
def cachedTokensStr = vars.get("SELF_CACHED_TOKENS")
def completionTokensStr = vars.get("SELF_COMPLETION_TOKENS")
def requestId = vars.get("SELF_REQUEST_ID")
// 检查变量是否提取成功
if (promptTokensStr == null || cachedTokensStr == null || completionTokensStr == null) {
throw new AssertionError("Token variables not extracted properly")
}
// 解析 token 数量
def promptTokens = Double.parseDouble(promptTokensStr)
def cachedTokens = Double.parseDouble(cachedTokensStr)
def completionTokens = Double.parseDouble(completionTokensStr)
// 计算期望 amount(包含倍率)
def totalMultiplier = distributorMultiplier * selfMultiplier
def normalInput = (promptTokens - cachedTokens) * inputPrice * totalMultiplier / 1000000
def cacheHit = cachedTokens * cachePrice * totalMultiplier / 1000000
def outputCost = completionTokens * outputPrice * totalMultiplier / 1000000
def expectedAmount = normalInput + cacheHit + outputCost
// 从 /api/log 响应中查找实际 amount
def response = prev.getResponseDataAsString()
def json = new groovy.json.JsonSlurper().parseText(response)
def actualAmount = null
for (def item : json.data) {
if (item.request_id == requestId) {
actualAmount = item.amount
break
}
}
if (actualAmount == null) {
throw new AssertionError("Request ID not found in /api/log response")
}
// 比对差异,允许误差 0.00001
def diff = Math.abs(actualAmount - expectedAmount)
if (diff > 0.00001) {
throw new AssertionError("Amount MISMATCH! Expected: " + expectedAmount + ", Actual: " + actualAmount + ", Diff: " + diff)
}
七、XML 中的配置位置
7.1 正则提取器(提取请求 ID)
<RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Extract request ID">
<stringProp name="RegexExtractor.useHeaders">true</stringProp>
<stringProp name="RegexExtractor.headers">x-xxx-request-id</stringProp>
<stringProp name="RegexExtractor.refname">REQUEST_ID</stringProp>
<stringProp name="RegexExtractor.regex">x-xxx-request-id:\s*(.+)</stringProp>
<stringProp name="RegexExtractor.template">$1$</stringProp>
<stringProp name="RegexExtractor.default"></stringProp>
</RegexExtractor>
7.2 JSON 提取器(提取 usage)
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Extract usage tokens">
<stringProp name="JSONPostProcessor.jsonPath">$.usage.prompt_tokens</stringProp>
<stringProp name="JSONPostProcessor.variableNames">PROMPT_TOKENS</stringProp>
</JSONPostProcessor>
7.3 延时定时器
<ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Response Delay">
<stringProp name="ConstantTimer.delay">${RESPONSE_DELAY}</stringProp>
</ConstantTimer>
7.4 验证脚本
<JSR223Assertion guiclass="TestBeanGUI" testclass="JSR223Assertion" testname="Verify amount calculation">
<stringProp name="language">groovy</stringProp>
<stringProp name="script"><![CDATA[...]]></stringProp>
</JSR223Assertion>
八、常见问题
8.1 为什么需要延时?
API 返回成功(HTTP 200)不代表日志已写入数据库。数据库写入是异步操作,需要等待一段时间(通常 10-15 秒)。
8.2 为什么倍率验证总是失败?
请确认:
- 变量是否正确提取(查看 jmeter.log 中的日志输出)
- 倍率计算顺序:普通用户 = 基础价 × 分销商倍率 × 普通用户倍率
8.3 如何调整允许的误差?
在脚本中修改 0.00001 的值:
if (diff > 0.00001) { ... }
8.4 为什么验证不生效?
可能的原因:
- 变量名不匹配(注意大小写)
- JSONPath 路径错误
- 正则表达式未匹配到响应头
建议在 View Results Tree 中查看实际响应内容进行调试。
九、日志输出
运行测试后,查看 jmeter.log,包含以下日志:
[Self] Tokens - PROMPT: [...], CACHED: [...], COMPLETION: [...][Self] Expected amount: ...[Self] Actual amount: ...[Self] Amount MISMATCH!或[Self] Amount verification PASSED