feat(billing): 解析 usage.input_tokens_details 拆分图/文输入 token

/v1/images/edits 等带图请求,上游在 input_tokens_details.image_tokens
单独回传图片输入 token,此前被合并进 input_tokens 整体按文本输入价计费。

- openAIUsageFromGJSON 读取 input_tokens_details.image_tokens
  (回退 prompt_tokens_details.image_tokens)写入 OpenAIUsage.ImageInputTokens
- mergeOpenAIUsage 补充 ImageInputTokens 的流式合并

配合全局定价的 input_cost_per_image_token,gpt-image-2 图片编辑请求的
图像输入即按独立单价计费。相关 #4386。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wcJTKDddxXSQrepSs3wrU
This commit is contained in:
haruka
2026-07-15 09:24:44 -07:00
co-authored by Claude Opus 4.8
parent 10cb2ca42c
commit 7c43b7327d
2 changed files with 11 additions and 0 deletions
@@ -1015,8 +1015,16 @@ func openAIUsageFromGJSON(value gjson.Result) (OpenAIUsage, bool) {
if imageOutputTokens == 0 {
imageOutputTokens = value.Get("completion_tokens_details.image_tokens").Int()
}
// 图片输入 token(如 gpt-image-2 的 /v1/images/edits 带图请求),
// 上游在 input_tokens_details.image_tokens 单独回传,用于图/文输入分价计费。
// 普通文本请求该字段为 0,走原路径行为不变。
imageInputTokens := firstPositiveGJSONInt(
value.Get("input_tokens_details.image_tokens"),
value.Get("prompt_tokens_details.image_tokens"),
)
return OpenAIUsage{
InputTokens: int(inputTokens),
ImageInputTokens: imageInputTokens,
OutputTokens: int(outputTokens),
CacheCreationInputTokens: cacheCreationTokens,
CacheReadInputTokens: cacheReadTokens,
@@ -1146,6 +1146,9 @@ func mergeOpenAIUsage(dst *OpenAIUsage, body []byte) {
if parsed.CacheReadInputTokens > 0 {
dst.CacheReadInputTokens = parsed.CacheReadInputTokens
}
if parsed.ImageInputTokens > 0 {
dst.ImageInputTokens = parsed.ImageInputTokens
}
if parsed.ImageOutputTokens > 0 {
dst.ImageOutputTokens = parsed.ImageOutputTokens
}