fix: correct OpenCode cache hit rates

Include cache reads and writes in the complete input denominator for OpenCode usage. Use the same calculation for stored records, summary totals, and renderer fallbacks so existing history is corrected without migration.

Add regressions for full cache hits, cache writes, mixed Runtime aggregation, and persisted conversation/system usage. Focused validation passed 41 tests; repository-wide checks were blocked by concurrent work-mode changes.

Release note: 修复 OpenCode 缓存命中率偏高或完全命中时显示为空的问题,已有用量记录在重新查询时按完整输入量计算。
This commit is contained in:
mesalogo
2026-10-04 18:03:33 +08:00
parent 4d18794e74
commit 0cd5da33f4
6 changed files with 88 additions and 23 deletions
+28
View File
@@ -125,6 +125,34 @@ describe('token usage aggregation', () => {
})
})
it.each([
{ provider: 'goodbuddy-openai-chat', input: 60, cacheRead: 40, cacheWrite: 0, rate: 0.4 },
{ provider: 'goodbuddy-openai-responses', input: 10, cacheRead: 90, cacheWrite: 0, rate: 0.9 },
{ provider: 'custom', input: 0, cacheRead: 100, cacheWrite: 0, rate: 1 },
{ provider: 'goodbuddy-anthropic', input: 50, cacheRead: 30, cacheWrite: 20, rate: 0.3 },
{ provider: 'openai', input: 100, cacheRead: 0, cacheWrite: 0, rate: 0 }
])('uses complete OpenCode input for $provider ($rate)', ({ provider, input, cacheRead, cacheWrite, rate }) => {
const usage = makeTokenUsage()
usage.records = [{
...usage.records[0]!,
runtime: 'opencode', provider, input, cacheRead, cacheWrite
}]
expect(groupTokenUsage(usage, 'model')[0]).toMatchObject({
cacheInputTokens: 100,
cacheHitRate: rate
})
usage.records[0]!.cacheInput = 100
expect(groupTokenUsage(usage, 'model')[0]?.cacheHitRate).toBe(rate)
// Mixed Runtime groups must sum complete inputs, not average percentages.
usage.records.push({ ...makeTokenUsage().records[1]!, input: 300 })
expect(groupTokenUsage(usage, 'project')[0]).toMatchObject({
cacheInputTokens: 400,
cacheHitRate: cacheRead / 400
})
})
it('uses fallback labels when grouping metadata is unavailable', () => {
const usage = makeTokenUsage()
usage.records = [
+4 -3
View File
@@ -31,7 +31,8 @@ function usesSeparatedAnthropicInput(provider: unknown): boolean {
function usageNumbers(
source: unknown,
provider?: unknown
provider?: unknown,
runtime?: unknown
): TokenUsageTotals {
const values = source as Record<string, unknown>
const read = (preferred: string, legacy: string): number => {
@@ -50,7 +51,7 @@ function usageNumbers(
Number.isFinite(reportedCacheInput)
? reportedCacheInput
: inputTokens +
(usesSeparatedAnthropicInput(provider)
(runtime === 'opencode' || usesSeparatedAnthropicInput(provider)
? cacheReadTokens + cacheWriteTokens
: 0)
@@ -172,7 +173,7 @@ export function groupTokenUsage(
records.push(record)
groupedRecords.set(identity.key, records)
}
const usage = usageNumbers(record, record.provider)
const usage = usageNumbers(record, record.provider, record.runtime)
const existing = rows.get(identity.key)
if (existing) {