Skip to content

Commit 06847f2

Browse files
ClaudiaFangclaude
andcommitted
fix(kilo): fall back to pricing.ts when Kilo records 0 cost with real usage
Kilo doesn't compute cost for every provider/model combo — sessions routed through local/OpenAI-compatible endpoints (e.g. GLM 5.2 via Ollama) come back with cost=0 even though real tokens were used. Fall back to calculateCost() so Ollama-served models still get an estimated cost, matching how claude-code and codex already work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent f4b99c7 commit 06847f2

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

cli/src/providers/__tests__/kilo.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,29 @@ describe('KiloProvider', () => {
395395
expect(session!.usage!.estimatedCostUsd).toBe(0.03);
396396
});
397397

398+
it('falls back to pricing.ts when Kilo recorded cost is 0 but tokens are present (e.g. local/OpenAI-compatible providers like Ollama)', async () => {
399+
const db = new Database(tempDbPath, { readonly: false });
400+
db.prepare(`
401+
UPDATE session SET model = ?, cost = 0, tokens_input = ?, tokens_output = ?, tokens_cache_read = 0, tokens_cache_write = 0
402+
WHERE id = ?
403+
`).run(
404+
JSON.stringify({ id: 'glm-5.2', providerID: 'openai-compatible' }),
405+
1_000_000, 1_000_000,
406+
'db-ses-1',
407+
);
408+
db.prepare(`UPDATE message SET data = ? WHERE id = ?`).run(
409+
JSON.stringify({ role: 'assistant', model: { providerID: 'openai-compatible', modelID: 'glm-5.2' } }),
410+
'db-msg-2',
411+
);
412+
db.close();
413+
414+
const virtualPath = `${tempDbPath}#db-ses-1`;
415+
const session = await provider.parse(virtualPath);
416+
417+
// input: 1M * $1.4/1M = $1.40, output: 1M * $4.4/1M = $4.40 -> $5.80
418+
expect(session!.usage!.estimatedCostUsd).toBe(5.8);
419+
});
420+
398421
it('handles reasoning part type in database', async () => {
399422
const db = new Database(tempDbPath, { readonly: false });
400423
db.prepare(`

cli/src/providers/kilo.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { SessionProvider } from './types.js';
66
import type { ParsedSession, ParsedMessage, ToolCall, ToolResult, SessionUsage } from '../types.js';
77
import { getKiloDir } from '../utils/config.js';
88
import { generateTitle, detectSessionCharacter } from '../parser/titles.js';
9+
import { calculateCost, type UsageEntry } from '../utils/pricing.js';
910

1011
/**
1112
* Kilo session provider.
@@ -289,6 +290,7 @@ export class KiloProvider implements SessionProvider {
289290
let cacheReadTokens = 0;
290291
const modelsUsed = new Set<string>();
291292
let totalCost = 0;
293+
const usageEntries: UsageEntry[] = [];
292294

293295
for (const msg of messages) {
294296
if (msg.usage) {
@@ -297,9 +299,25 @@ export class KiloProvider implements SessionProvider {
297299
cacheReadTokens += msg.usage.cacheReadTokens;
298300
totalCost += msg.usage.estimatedCostUsd;
299301
modelsUsed.add(msg.usage.model);
302+
usageEntries.push({
303+
model: msg.usage.model,
304+
usage: {
305+
input_tokens: msg.usage.inputTokens,
306+
output_tokens: msg.usage.outputTokens,
307+
cache_creation_input_tokens: msg.usage.cacheCreationTokens,
308+
cache_read_input_tokens: msg.usage.cacheReadTokens,
309+
},
310+
});
300311
}
301312
}
302313

314+
// Kilo doesn't record cost for every provider (e.g. local/OpenAI-compatible
315+
// endpoints like Ollama) — its own `cost` field comes back 0 even when real
316+
// token usage was recorded. Fall back to our own pricing table in that case.
317+
if (totalCost === 0 && (totalInputTokens > 0 || totalOutputTokens > 0)) {
318+
totalCost = calculateCost(usageEntries);
319+
}
320+
303321
const primaryModel = Array.from(modelsUsed)[0] || 'unknown';
304322

305323
return {

0 commit comments

Comments
 (0)