mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 05:23:19 +00:00
fix(cline-cli): stop billing cache tokens twice (#1675)
Cline CLI's metrics.inputTokens is the full prompt, cache reads and writes included; Cline's own pricing bills max(0, input - cacheRead - cacheWrite) at the input rate. We treated it as uncached input, so every cache read was also charged at the full input rate. The split now happens in parseMetrics, covering per-message and rollup calls. Bumps the cline-cli parse version so cached calls re-parse and the daily cache to v63 so settled days re-derive. Call counts are unchanged.
This commit is contained in:
parent
4332b1762a
commit
d049b38255
4 changed files with 34 additions and 7 deletions
|
|
@ -304,7 +304,11 @@ import type { DateRange, ProjectSummary } from './types.js'
|
|||
// v62: Devin's SWE-2 prices at Cognition's list rate and swe-1-7-lightning as
|
||||
// swe-1.7-lightning instead of $0. Only cost rises; call counts are unchanged,
|
||||
// so no PENDING_REDERIVE_PROVIDER_VERSIONS entry is needed.
|
||||
export const DAILY_CACHE_VERSION = 62
|
||||
// v63: Cline CLI input tokens include cache reads and writes, which were then
|
||||
// billed again at the cache rates. Only input tokens and estimated cost fall;
|
||||
// call counts are unchanged, so no PENDING_REDERIVE_PROVIDER_VERSIONS entry is
|
||||
// needed.
|
||||
export const DAILY_CACHE_VERSION = 63
|
||||
const MIN_SUPPORTED_VERSION = 28
|
||||
|
||||
/// Providers whose per-day CALL COUNT means something different at
|
||||
|
|
|
|||
|
|
@ -89,11 +89,15 @@ type ClineCliMetrics = {
|
|||
|
||||
function parseMetrics(value: unknown): ClineCliMetrics | null {
|
||||
if (!isRecord(value)) return null
|
||||
const cacheReadTokens = safeTokenCount(value['cacheReadTokens'])
|
||||
const cacheWriteTokens = safeTokenCount(value['cacheWriteTokens'])
|
||||
const metrics: ClineCliMetrics = {
|
||||
inputTokens: safeTokenCount(value['inputTokens']),
|
||||
// Cline's inputTokens is the full prompt, cache read and write included
|
||||
// (its own pricing bills max(0, input - cacheRead - cacheWrite) at the input rate).
|
||||
inputTokens: Math.max(0, safeTokenCount(value['inputTokens']) - cacheReadTokens - cacheWriteTokens),
|
||||
outputTokens: safeTokenCount(value['outputTokens']),
|
||||
cacheReadTokens: safeTokenCount(value['cacheReadTokens']),
|
||||
cacheWriteTokens: safeTokenCount(value['cacheWriteTokens']),
|
||||
cacheReadTokens,
|
||||
cacheWriteTokens,
|
||||
cost: safeNonNegativeNumber(value['cost']),
|
||||
// A negative cost is not a credit we can represent — treat it as absent and
|
||||
// fall back to token pricing, rather than reporting a clamped $0 as metered.
|
||||
|
|
|
|||
|
|
@ -377,7 +377,9 @@ export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
|
|||
// reported-cost-v1: the CLI reports its own per-message cost, so entries
|
||||
// cached before cline-cli joined the reported-cost allowlist in parser.ts
|
||||
// hold costUSD: undefined and get re-priced from tokens on every read.
|
||||
'cline-cli': 'reported-cost-v1-est-reprice-v1',
|
||||
// cache-inclusive-input-v1: cached calls hold inputTokens with cache reads
|
||||
// and writes still inside it, so they must re-parse.
|
||||
'cline-cli': 'reported-cost-v1-est-reprice-v1-cache-inclusive-input-v1',
|
||||
codewhale: 'aggregate-session-v1-est-cost',
|
||||
'command-code': 'cache-inclusive-input-v1',
|
||||
// Bump when the Codex parser changes attribution so unchanged, already-cached
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { join } from 'path'
|
|||
import { tmpdir } from 'os'
|
||||
|
||||
import { clineCli, createClineCliProvider, getClineCliSessionsDir } from '../../src/providers/cline-cli.js'
|
||||
import { calculateCost } from '../../src/models.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
|
@ -211,7 +212,7 @@ describe('cline-cli provider - parsing', () => {
|
|||
const calls = await collect(tmpDir)
|
||||
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls.map(c => c.inputTokens)).toEqual([100, 200])
|
||||
expect(calls.map(c => c.inputTokens)).toEqual([93, 200])
|
||||
expect(calls.map(c => c.outputTokens)).toEqual([10, 20])
|
||||
expect(calls[0]?.cacheReadInputTokens).toBe(5)
|
||||
expect(calls[0]?.cacheCreationInputTokens).toBe(2)
|
||||
|
|
@ -328,6 +329,22 @@ describe('cline-cli provider - parsing', () => {
|
|||
expect(call?.costUSD).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('takes cache reads out of the cache-inclusive input before estimating', async () => {
|
||||
await writeSession(tmpDir, 'sess-a', {
|
||||
messages: [{
|
||||
role: 'assistant', text: 'a', model: 'cline-pass/deepseek-v4.1-flash',
|
||||
metrics: { inputTokens: 14689, outputTokens: 220, cacheReadTokens: 11136, cacheWriteTokens: 0 },
|
||||
}],
|
||||
})
|
||||
|
||||
const [call] = await collect(tmpDir)
|
||||
|
||||
expect(call?.inputTokens).toBe(3553)
|
||||
expect(call?.cacheReadInputTokens).toBe(11136)
|
||||
expect(call?.costUSD).toBeCloseTo(calculateCost('cline-pass/deepseek-v4.1-flash', 3553, 220, 0, 11136, 0), 12)
|
||||
expect(call?.costUSD).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('keeps a metered $0 cost reported instead of re-estimating it', async () => {
|
||||
await writeSession(tmpDir, 'sess-a', {
|
||||
messages: [{ role: 'assistant', text: 'a', metrics: { inputTokens: 1000, outputTokens: 100, cost: 0 } }],
|
||||
|
|
@ -428,7 +445,7 @@ describe('cline-cli provider - rollup fallback', () => {
|
|||
const calls = await collect(tmpDir)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]?.inputTokens).toBe(5483)
|
||||
expect(calls[0]?.inputTokens).toBe(5433)
|
||||
expect(calls[0]?.outputTokens).toBe(133)
|
||||
expect(calls[0]?.cacheReadInputTokens).toBe(50)
|
||||
expect(calls[0]?.costUSD).toBeCloseTo(0.0081984, 7)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue