一、概述

本脚本用于验证计费接口的计费准确性。通过提取 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_DELAY10000延时(毫秒),等待数据落库
INPUT_PRICE0.8输入价格(元/1M tokens)
CACHE_PRICE0.4缓存命中价格(元/1M tokens)
OUTPUT_PRICE2.0输出价格(元/1M tokens)
PRICE_MULTIPLIER1.2分销商倍率
SELF_MULTIPLIER1.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 NameJSON 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 为什么倍率验证总是失败?

请确认:

  1. 变量是否正确提取(查看 jmeter.log 中的日志输出)
  2. 倍率计算顺序:普通用户 = 基础价 × 分销商倍率 × 普通用户倍率

8.3 如何调整允许的误差?

在脚本中修改 0.00001 的值:

if (diff > 0.00001) { ... }

8.4 为什么验证不生效?

可能的原因:

  1. 变量名不匹配(注意大小写)
  2. JSONPath 路径错误
  3. 正则表达式未匹配到响应头

建议在 View Results Tree 中查看实际响应内容进行调试。


九、日志输出

运行测试后,查看 jmeter.log,包含以下日志:

  • [Self] Tokens - PROMPT: [...], CACHED: [...], COMPLETION: [...]
  • [Self] Expected amount: ...
  • [Self] Actual amount: ...
  • [Self] Amount MISMATCH![Self] Amount verification PASSED