refactor(pricing): fan-out batch 2 — zed, gemini, kimi, pi/omp

This commit is contained in:
iamtoruk 2026-07-26 08:18:23 -07:00
parent 7bc5f3b044
commit a3380b2453
8 changed files with 29 additions and 42 deletions

View file

@ -3,7 +3,6 @@ import { join } from 'path'
import { homedir } from 'os'
import { readSessionFile } from '../fs-utils.js'
import { calculateCost } from '../models.js'
import { extractBashCommands } from '../bash-utils.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
@ -119,11 +118,6 @@ function parseSession(data: GeminiSession, seenKeys: Set<string>): ParsedProvide
seenKeys.add(dedupKey)
// Gemini bills thoughts at the output token rate; calculateCost does not
// accept a reasoning parameter, so fold thoughts into the output count for
// pricing while keeping outputTokens / reasoningTokens reported separately.
const costUSD = calculateCost(msg.model, freshInput, totalOutput + totalThoughts, 0, totalCached, 0)
results.push({
provider: 'gemini',
model: msg.model,
@ -134,7 +128,7 @@ function parseSession(data: GeminiSession, seenKeys: Set<string>): ParsedProvide
cachedInputTokens: totalCached,
reasoningTokens: totalThoughts,
webSearchRequests: 0,
costUSD,
costBasis: 'estimated',
tools: [...new Set(tools)],
bashCommands: [...new Set(bashCommands)],
timestamp: tsDate.toISOString(),

View file

@ -5,7 +5,7 @@ import { homedir } from 'os'
import { extractBashCommands } from '../bash-utils.js'
import { readSessionLines } from '../fs-utils.js'
import { calculateCost, getShortModelName } from '../models.js'
import { getShortModelName } from '../models.js'
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
type JsonObject = Record<string, unknown>
@ -297,14 +297,6 @@ function createParser(source: SessionSource, shareDir: string, seenKeys: Set<str
seenKeys.add(dedupKey)
const model = stringField(envelope.payload, 'model') ?? stringField(envelope.payload, 'model_name') ?? configuredModel
const costUSD = calculateCost(
model,
usage.inputTokens,
usage.outputTokens,
usage.cacheCreationInputTokens,
usage.cacheReadInputTokens,
0,
)
yield {
provider: 'kimi',
@ -316,7 +308,7 @@ function createParser(source: SessionSource, shareDir: string, seenKeys: Set<str
cachedInputTokens: usage.cacheReadInputTokens,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
costBasis: 'estimated',
tools: [...tools],
bashCommands: [...bashCommands],
timestamp: envelope.timestamp,

View file

@ -3,7 +3,6 @@ import { basename, join } from 'path'
import { homedir } from 'os'
import { readSessionFile } from '../fs-utils.js'
import { calculateCost } from '../models.js'
import { extractBashCommands } from '../bash-utils.js'
import { normalizeContentBlocks } from '../content-utils.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
@ -223,7 +222,6 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
return typeof cmd === 'string' ? extractBashCommands(cmd) : []
})
const costUSD = calculateCost(model, input, output, cacheWrite, cacheRead, 0)
const timestamp = entry.timestamp ?? ''
yield {
@ -236,7 +234,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
cachedInputTokens: cacheRead,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
costBasis: 'estimated',
tools,
bashCommands,
skills,

View file

@ -3,7 +3,6 @@ import { join } from 'path'
import { homedir } from 'os'
import zlib from 'zlib'
import { calculateCost } from '../models.js'
import { getSqliteLoadError, isSqliteAvailable, openDatabase, type SqliteDatabase } from '../sqlite.js'
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
@ -88,7 +87,7 @@ function buildCall(opts: {
cachedInputTokens: cacheRead,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD: calculateCost(opts.model, input, output, cacheWrite, cacheRead, 0),
costBasis: 'estimated',
tools: [],
bashCommands: [],
timestamp: opts.timestamp,

View file

@ -5,6 +5,7 @@ import { join } from 'path'
import { tmpdir } from 'os'
import { createKimiProvider } from '../../src/providers/kimi.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
let tmpDir: string
@ -44,7 +45,7 @@ async function writeSession(workDir: string, sessionId: string, lines: string[])
async function collect(provider: ReturnType<typeof createKimiProvider>, path: string, seen = new Set<string>()): Promise<ParsedProviderCall[]> {
const parser = provider.createSessionParser({ path, project: 'app', provider: 'kimi' }, seen)
const calls: ParsedProviderCall[] = []
for await (const call of parser.parse()) calls.push(call)
for await (const call of parser.parse()) calls.push(priceProviderCall(call))
return calls
}

View file

@ -4,6 +4,7 @@ import { join } from 'path'
import { tmpdir } from 'os'
import { createOmpProvider } from '../../src/providers/pi.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
let tmpDir: string
@ -149,7 +150,7 @@ describe('omp provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'omp' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(1)
@ -179,7 +180,7 @@ describe('omp provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'omp' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
// cost must be calculated by codeburn, not taken from usage.cost (which is zeroed in fixture)
@ -199,7 +200,7 @@ describe('omp provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'omp' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.tools).toEqual(['Read', 'Edit', 'Bash'])
@ -217,7 +218,7 @@ describe('omp provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'omp' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(0)

View file

@ -4,6 +4,7 @@ import { join } from 'path'
import { tmpdir } from 'os'
import { createPiProvider } from '../../src/providers/pi.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
import { classifyTurn } from '../../src/classifier.js'
import type { ParsedApiCall, ParsedTurn } from '../../src/types.js'
@ -208,7 +209,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(1)
@ -249,7 +250,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
// The assistant turn is still parsed, and the string user content is paired.
@ -275,7 +276,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.tools).toEqual(['Read', 'Edit', 'Bash'])
@ -296,7 +297,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.bashCommands).toEqual(['git', 'bun'])
@ -319,7 +320,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
// The SKILL.md read is surfaced as the Skill tool (not Read); the plain
@ -341,7 +342,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.skills).toEqual(['commit-workflow'])
@ -361,7 +362,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.skills).toEqual(['deep-research'])
@ -381,7 +382,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls[0]!.skills).toEqual([])
@ -401,7 +402,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
// End to end: the parsed skill load must reach `subCategory`, or the
@ -422,7 +423,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(0)
@ -441,12 +442,12 @@ describe('pi provider - JSONL parsing', () => {
const firstRun: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, seenKeys).parse()) {
firstRun.push(call)
firstRun.push(priceProviderCall(call))
}
const secondRun: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, seenKeys).parse()) {
secondRun.push(call)
secondRun.push(priceProviderCall(call))
}
expect(firstRun).toHaveLength(1)
@ -467,7 +468,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: filePath, project: 'myproject', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(2)
@ -482,7 +483,7 @@ describe('pi provider - JSONL parsing', () => {
const source = { path: '/nonexistent/session.jsonl', project: 'test', provider: 'pi' }
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(0)
})

View file

@ -7,6 +7,7 @@ import zlib from 'zlib'
import { createZedProvider } from '../../src/providers/zed.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
const requireForTest = createRequire(import.meta.url)
@ -76,7 +77,7 @@ async function collectCalls(dbPath: string, seenKeys = new Set<string>()): Promi
const calls: ParsedProviderCall[] = []
for (const source of sources) {
for await (const call of provider.createSessionParser(source, seenKeys).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
return calls