Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 17eada2

Browse files
authored
fix: DeepSeek v4 Claude pricing through stale runtime cache (getagentseal#367)
1 parent f2e023c commit 17eada2

6 files changed

Lines changed: 229 additions & 3 deletions

File tree

scripts/bundle-litellm.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const outPath = join(__dirname, '..', 'src', 'data', 'litellm-snapshot.json')
99
const MANUAL_ENTRIES = {
1010
'MiniMax-M2.7': [0.3e-6, 1.2e-6, 0.375e-6, 0.06e-6],
1111
'MiniMax-M2.7-highspeed': [0.6e-6, 2.4e-6, 0.375e-6, 0.06e-6],
12+
// LiteLLM PR #27056 is not merged yet. Source: https://api-docs.deepseek.com/quick_start/pricing
13+
'deepseek-v4-flash': [1.4e-7, 2.8e-7, 0, 2.8e-9],
14+
'deepseek-v4-pro': [4.35e-7, 8.7e-7, 0, 3.625e-9],
1215
}
1316

1417
const res = await fetch(LITELLM_URL)

src/data/litellm-snapshot.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/models.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function getSortedPricingKeys(): string[] {
5959
}
6060

6161
function getCacheDir(): string {
62+
if (process.env['CODEBURN_CACHE_DIR']) return process.env['CODEBURN_CACHE_DIR']
6263
return join(homedir(), '.cache', 'codeburn')
6364
}
6465

@@ -131,16 +132,23 @@ async function loadCachedPricing(): Promise<Map<string, ModelCosts> | null> {
131132
}
132133
}
133134

135+
function mergeSnapshotFallbacks(pricing: Map<string, ModelCosts>): Map<string, ModelCosts> {
136+
for (const [name, costs] of loadSnapshot()) {
137+
if (!pricing.has(name)) pricing.set(name, costs)
138+
}
139+
return pricing
140+
}
141+
134142
export async function loadPricing(): Promise<void> {
135143
const cached = await loadCachedPricing()
136144
if (cached) {
137-
pricingCache = cached
145+
pricingCache = mergeSnapshotFallbacks(cached)
138146
sortedPricingKeys = null
139147
return
140148
}
141149

142150
try {
143-
pricingCache = await fetchAndCachePricing()
151+
pricingCache = mergeSnapshotFallbacks(await fetchAndCachePricing())
144152
sortedPricingKeys = null
145153
} catch {
146154
// snapshot already loaded at init; nothing more to do
@@ -431,6 +439,8 @@ const SHORT_NAMES: Record<string, string> = {
431439
'kimi-k2': 'Kimi K2',
432440
'kimi-latest': 'Kimi Latest',
433441
'moonshot-v1': 'Moonshot v1',
442+
'deepseek-v4-pro': 'DeepSeek v4 Pro',
443+
'deepseek-v4-flash': 'DeepSeek v4 Flash',
434444
'deepseek-coder-max': 'DeepSeek Coder Max',
435445
'deepseek-coder': 'DeepSeek Coder',
436446
'deepseek-r1': 'DeepSeek R1',

src/providers/claude.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const shortNames: Record<string, string> = {
1717
'claude-3-5-sonnet': 'Sonnet 3.5',
1818
'claude-haiku-4-5': 'Haiku 4.5',
1919
'claude-3-5-haiku': 'Haiku 3.5',
20+
'deepseek-v4-pro': 'DeepSeek v4 Pro',
21+
'deepseek-v4-flash': 'DeepSeek v4 Flash',
2022
}
2123

2224
function expandHome(p: string): string {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2+
import { tmpdir } from 'node:os'
3+
import { join } from 'node:path'
4+
import { spawnSync } from 'node:child_process'
5+
6+
import { describe, expect, it } from 'vitest'
7+
8+
function runCli(args: string[], home: string) {
9+
return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
10+
cwd: process.cwd(),
11+
env: {
12+
...process.env,
13+
CLAUDE_CONFIG_DIR: join(home, '.claude'),
14+
HOME: home,
15+
TZ: 'UTC',
16+
},
17+
encoding: 'utf-8',
18+
timeout: 30_000,
19+
})
20+
}
21+
22+
function userLine(content: string, timestamp: string): string {
23+
return JSON.stringify({
24+
type: 'user',
25+
sessionId: 'deepseek-v4-session',
26+
timestamp,
27+
cwd: '/tmp/deepseek-v4-validation',
28+
message: { role: 'user', content },
29+
})
30+
}
31+
32+
function assistantLine(model: string, timestamp: string, messageId: string, usage: Record<string, number>): string {
33+
return JSON.stringify({
34+
type: 'assistant',
35+
sessionId: 'deepseek-v4-session',
36+
timestamp,
37+
cwd: '/tmp/deepseek-v4-validation',
38+
message: {
39+
id: messageId,
40+
type: 'message',
41+
role: 'assistant',
42+
model,
43+
content: [
44+
{ type: 'text', text: 'updated pricing code' },
45+
{ type: 'tool_use', id: `tu-${messageId}`, name: 'Edit', input: { file_path: '/tmp/deepseek-v4-validation/pricing.ts', old_string: 'old', new_string: 'new' } },
46+
],
47+
usage,
48+
},
49+
})
50+
}
51+
52+
describe('CLI DeepSeek v4 Claude pricing regression', () => {
53+
it('prices DeepSeek v4 Claude sessions even when the runtime LiteLLM cache lacks those models', async () => {
54+
const home = await mkdtemp(join(tmpdir(), 'codeburn-deepseek-v4-cli-'))
55+
56+
try {
57+
const projectDir = join(home, '.claude', 'projects', 'deepseek-v4-validation')
58+
const cacheDir = join(home, '.cache', 'codeburn')
59+
await mkdir(projectDir, { recursive: true })
60+
await mkdir(cacheDir, { recursive: true })
61+
62+
await writeFile(join(cacheDir, 'litellm-pricing.json'), JSON.stringify({
63+
timestamp: Date.now(),
64+
data: {
65+
'gpt-4o-mini': {
66+
inputCostPerToken: 1.5e-7,
67+
outputCostPerToken: 6e-7,
68+
cacheWriteCostPerToken: 0,
69+
cacheReadCostPerToken: 7.5e-8,
70+
webSearchCostPerRequest: 0.01,
71+
fastMultiplier: 1,
72+
},
73+
},
74+
}))
75+
76+
await writeFile(
77+
join(projectDir, 'session.jsonl'),
78+
[
79+
userLine('Use DeepSeek v4 through the Claude-compatible endpoint.', '2026-05-20T10:00:00.000Z'),
80+
assistantLine('deepseek-v4-pro', '2026-05-20T10:01:00.000Z', 'deepseek-v4-pro', {
81+
input_tokens: 2_477_914,
82+
output_tokens: 762_994,
83+
cache_read_input_tokens: 258_556_928,
84+
cache_creation_input_tokens: 0,
85+
}),
86+
userLine('Validate the flash model path too.', '2026-05-20T10:02:00.000Z'),
87+
assistantLine('deepseek-v4-flash', '2026-05-20T10:03:00.000Z', 'deepseek-v4-flash', {
88+
input_tokens: 1_552_573,
89+
output_tokens: 353_914,
90+
cache_read_input_tokens: 48_388_608,
91+
cache_creation_input_tokens: 0,
92+
}),
93+
].join('\n') + '\n',
94+
)
95+
96+
const result = runCli([
97+
'--format', 'json',
98+
'--from', '2026-05-20',
99+
'--to', '2026-05-20',
100+
'--provider', 'claude',
101+
], home)
102+
103+
expect(result.status, `stderr: ${result.stderr}`).toBe(0)
104+
105+
const report = JSON.parse(result.stdout) as {
106+
overview: { cost: number; calls: number; tokens: { cacheRead: number } }
107+
models: Array<{ name: string; cost: number; calls: number; inputTokens: number; outputTokens: number; cacheReadTokens: number }>
108+
}
109+
const pro = report.models.find(m => m.name === 'DeepSeek v4 Pro')
110+
const flash = report.models.find(m => m.name === 'DeepSeek v4 Flash')
111+
112+
expect(report.overview.calls).toBe(2)
113+
expect(report.overview.tokens.cacheRead).toBe(306_945_536)
114+
expect(report.overview.cost).toBeCloseTo(3.13091, 5)
115+
116+
expect(pro).toBeDefined()
117+
expect(pro!.calls).toBe(1)
118+
expect(pro!.inputTokens).toBe(2_477_914)
119+
expect(pro!.outputTokens).toBe(762_994)
120+
expect(pro!.cacheReadTokens).toBe(258_556_928)
121+
expect(pro!.cost).toBeCloseTo(2.678966, 6)
122+
123+
expect(flash).toBeDefined()
124+
expect(flash!.calls).toBe(1)
125+
expect(flash!.inputTokens).toBe(1_552_573)
126+
expect(flash!.outputTokens).toBe(353_914)
127+
expect(flash!.cacheReadTokens).toBe(48_388_608)
128+
expect(flash!.cost).toBeCloseTo(0.451944, 6)
129+
} finally {
130+
await rm(home, { recursive: true, force: true })
131+
}
132+
})
133+
})

tests/models.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
2+
import { tmpdir } from 'os'
3+
import { join } from 'path'
14
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
25

36
import { getModelCosts, getShortModelName, calculateCost, loadPricing, setModelAliases } from '../src/models.js'
@@ -260,3 +263,78 @@ describe('Cursor model variants resolve to pricing', () => {
260263
})
261264
}
262265
})
266+
267+
describe('DeepSeek v4 models resolve to pricing', () => {
268+
it('deepseek-v4-pro has current official discounted pricing', () => {
269+
const costs = getModelCosts('deepseek-v4-pro')
270+
expect(costs).not.toBeNull()
271+
expect(costs!.inputCostPerToken).toBe(4.35e-7)
272+
expect(costs!.outputCostPerToken).toBe(8.7e-7)
273+
expect(costs!.cacheReadCostPerToken).toBe(3.625e-9)
274+
expect(costs!.cacheWriteCostPerToken).toBe(0)
275+
})
276+
277+
it('deepseek-v4-flash has current official pricing', () => {
278+
const costs = getModelCosts('deepseek-v4-flash')
279+
expect(costs).not.toBeNull()
280+
expect(costs!.inputCostPerToken).toBe(1.4e-7)
281+
expect(costs!.outputCostPerToken).toBe(2.8e-7)
282+
expect(costs!.cacheReadCostPerToken).toBe(2.8e-9)
283+
expect(costs!.cacheWriteCostPerToken).toBe(0)
284+
})
285+
286+
it('provider-prefixed DeepSeek v4 names resolve to the same pricing', () => {
287+
expect(getModelCosts('deepseek/deepseek-v4-pro')).toEqual(getModelCosts('deepseek-v4-pro'))
288+
expect(getModelCosts('deepseek/deepseek-v4-flash')).toEqual(getModelCosts('deepseek-v4-flash'))
289+
})
290+
291+
it('calculates non-zero costs for observed DeepSeek v4 Claude usage', () => {
292+
const pro = calculateCost('deepseek-v4-pro', 2_477_914, 762_994, 0, 258_556_928, 0)
293+
const flash = calculateCost('deepseek-v4-flash', 1_552_573, 353_914, 0, 48_388_608, 0)
294+
295+
expect(pro).toBeCloseTo(2.68, 2)
296+
expect(flash).toBeCloseTo(0.45, 2)
297+
})
298+
299+
it('uses DeepSeek v4 display names', () => {
300+
expect(getShortModelName('deepseek-v4-pro')).toBe('DeepSeek v4 Pro')
301+
expect(getShortModelName('deepseek-v4-flash')).toBe('DeepSeek v4 Flash')
302+
})
303+
304+
it('keeps bundled DeepSeek v4 fallback entries when runtime pricing cache is stale', async () => {
305+
const previousCacheDir = process.env['CODEBURN_CACHE_DIR']
306+
const cacheRoot = await mkdtemp(join(tmpdir(), 'codeburn-pricing-cache-'))
307+
308+
try {
309+
process.env['CODEBURN_CACHE_DIR'] = cacheRoot
310+
await mkdir(cacheRoot, { recursive: true })
311+
await writeFile(join(cacheRoot, 'litellm-pricing.json'), JSON.stringify({
312+
timestamp: Date.now(),
313+
data: {
314+
'gpt-4o-mini': {
315+
inputCostPerToken: 9e-7,
316+
outputCostPerToken: 1.8e-6,
317+
cacheWriteCostPerToken: 0,
318+
cacheReadCostPerToken: 9e-8,
319+
webSearchCostPerRequest: 0.01,
320+
fastMultiplier: 1,
321+
},
322+
},
323+
}), 'utf-8')
324+
325+
await loadPricing()
326+
327+
expect(getModelCosts('gpt-4o-mini')!.inputCostPerToken).toBe(9e-7)
328+
expect(getModelCosts('deepseek-v4-pro')!.inputCostPerToken).toBe(4.35e-7)
329+
expect(getModelCosts('deepseek-v4-flash')!.inputCostPerToken).toBe(1.4e-7)
330+
} finally {
331+
if (previousCacheDir === undefined) {
332+
delete process.env['CODEBURN_CACHE_DIR']
333+
} else {
334+
process.env['CODEBURN_CACHE_DIR'] = previousCacheDir
335+
}
336+
await rm(cacheRoot, { recursive: true, force: true })
337+
await loadPricing()
338+
}
339+
})
340+
})

0 commit comments

Comments
 (0)