fix(pricing): 目录 cache 侧字段契约哨兵与 Gemini pro 缓存写入价修正

阶梯计价数据驱动后,目录条目的字段缺失或版本拼接会直接变成计费偏差,
解析层补两道数据契约哨兵,并修正出厂快照中 Gemini pro 系的缓存写入价:

- orphanCacheTierFields / warnOrphanCacheTierFields:条目带 cache 侧
  above 档(含 _priority/_flex 变体与 above_1hr 形态)却没有基础价时打
  WARN——cache 侧 above 档不参与计费取值,按"基础价 × 输入倍率"计,基础价
  缺失即该分项按 0 计;变体缺自身基础价时沿 变体→1h→标准 回落链任一存在
  即不算孤儿,与计费回落一致
- isLopsidedLongContextLadder / warnLopsidedLongContextLadders:above 档
  折算出只有一侧带附加费的阶梯时打 WARN(基础价与 above 档来自不同价格
  版本的形态),显式 long_context_* 字段不告警
- deriveLongContextFromAboveTierFields 注释改为陈述契约:cache above 档
  须恰为基础价 × 输入倍率
- 出厂快照 6 个 Gemini pro 条目补 cache_creation_input_token_cost(= 标准
  输入价,Google 对缓存写入不另收费)、above_200k 改为输入 above 价、带
  priority 输入价的 4 条补 priority 孪生,与 model-price-repo 数据修正同值

测试:
- 解析层:孤儿字段 WARN(含 priority/1h 孤儿正例、1h 回落 5m 反例)、单侧
  阶梯 WARN、出厂快照契约(全条目无孤儿字段、无单侧阶梯、Gemini pro 缓存
  写入价 = 输入价)
- 用量契约:extractGeminiUsage 与 Antigravity 非流式/流式转换均不产生
  cache_creation token(Gemini usageMetadata 无该类别)
- 计费:gemini-2.5-pro 阶梯对 cache_read / cache_creation 分项生效,阈值
  计入缓存写入 token;阶梯表场景与夹具同步为现行目录值
This commit is contained in:
feeeei
2026-09-01 01:26:55 +08:00
parent 593fc93659
commit e2cfaa46e7
7 changed files with 320 additions and 16 deletions
@@ -0,0 +1,40 @@
//go:build unit
package antigravity
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// Gemini usageMetadata 只有 cachedContentTokenCount(缓存命中),没有缓存写入的 token 类别:
// 两条转换路径产出的 Claude usage 中 cache_creation_input_tokens 恒为 0,缓存命中计入
// cache_read_input_tokens 并从 input_tokens 中扣除。
func TestGeminiUsageMapping_NoCacheCreationTokens(t *testing.T) {
const geminiBody = `{"candidates":[{"content":{"parts":[{"text":"hi"}],"role":"model"},"finishReason":"STOP"}],` +
`"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"cachedContentTokenCount":30,"thoughtsTokenCount":5}}`
t.Run("non-stream", func(t *testing.T) {
_, usage, err := TransformGeminiToClaude([]byte(geminiBody), "gemini-3.1-pro-preview")
require.NoError(t, err)
require.NotNil(t, usage)
require.Equal(t, 70, usage.InputTokens)
require.Equal(t, 25, usage.OutputTokens)
require.Equal(t, 30, usage.CacheReadInputTokens)
require.Zero(t, usage.CacheCreationInputTokens)
})
t.Run("stream", func(t *testing.T) {
p := NewStreamingProcessor("gemini-3.1-pro-preview")
out := p.ProcessLine(`data: {"response":` + geminiBody + `}`)
require.True(t, strings.Contains(string(out), `"message_start"`))
require.NotContains(t, string(out), `"cache_creation_input_tokens"`, "message_start 的 usage 不应携带缓存写入分项")
_, usage := p.Finish()
require.NotNil(t, usage)
require.Equal(t, 70, usage.InputTokens)
require.Equal(t, 30, usage.CacheReadInputTokens)
require.Zero(t, usage.CacheCreationInputTokens)
})
}