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:
@@ -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 = [
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user