codeburn/tests/providers/grok.test.ts
iamtoruk 37796a568e models: distinguish grok-4.5-build from grok-4.5 in reports
Reports bucket rows by model id and label them afterwards, so the two ids
collapsing onto one display name printed what looked like the same row twice
with different numbers. Give the variant its own SHORT_NAMES entry, which the
longest-first match picks over the grok-4.5 prefix. Display only: ids are
untouched, so nothing re-parses and no cost moves.

Fixed in the shared table rather than the grok provider so the menubar and
model-breakdown, which call getShortModelName directly, get it too.

Fixes #1029.
2026-08-18 12:01:12 -07:00

504 lines
17 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
import { createGrokProvider } from '../../src/providers/grok.js'
import { calculateCost } from '../../src/models.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
// `chooseAuthoritativeModel` branches on whether a modelUsage id resolves to a
// price, so pin the reporter's real id from #998 as unpriced here rather than
// letting the bundled LiteLLM snapshot decide it: xAI pricing landing upstream
// would otherwise silently flip these assertions. Only this lookup is stubbed,
// so `calculateCost` still prices off the real tables.
vi.mock('../../src/models.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../src/models.js')>()
return {
...actual,
getModelCosts: (model: string) => (model === 'grok-4.6-build' ? null : actual.getModelCosts(model)),
}
})
let tmpDir: string
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'grok-test-'))
})
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true })
})
// Mirrors the real on-disk layout:
// <sessionsDir>/<url-encoded-cwd>/<uuid>/{summary.json, signals.json, updates.jsonl}
async function writeSession(opts: {
cwdEncoded?: string
uuid?: string
cwd?: string
model?: string
turns?: Array<{ promptId: string; totals: number[] }>
completedTurns?: Array<{ promptId?: string; usage: unknown }>
toolCalls?: Array<{ title: string; rawInput: Record<string, unknown> }>
toolsUsed?: string[]
} = {}) {
const cwdEncoded = opts.cwdEncoded ?? '%2FUsers%2Ftest'
const uuid = opts.uuid ?? '019edf9c-0000-7000-8000-000000000001'
const cwd = opts.cwd ?? '/Users/test/myproject'
const model = opts.model ?? 'grok-build'
const dir = join(tmpDir, cwdEncoded, uuid)
await mkdir(dir, { recursive: true })
await writeFile(join(dir, 'summary.json'), JSON.stringify({
info: { id: uuid, cwd },
created_at: '2026-06-19T11:20:40.686261Z',
updated_at: '2026-06-19T11:31:12.282793Z',
last_active_at: '2026-06-19T11:31:12.222328Z',
num_messages: 42,
current_model_id: model,
session_summary: 'User asks about the repo',
generated_title: 'User asks about the repo',
}))
await writeFile(join(dir, 'signals.json'), JSON.stringify({
primaryModelId: model,
modelsUsed: [model],
toolsUsed: opts.toolsUsed ?? ['read_file', 'run_terminal_command', 'grep'],
contextTokensUsed: 40000,
contextWindowTokens: 512000,
}))
const turns = opts.turns ?? [
{ promptId: 'p1', totals: [20000, 25000] },
{ promptId: 'p2', totals: [30000, 35000] },
{ promptId: 'p3', totals: [40000, 45000] },
]
const lines: string[] = []
for (const turn of turns) {
for (const total of turn.totals) {
lines.push(JSON.stringify({
timestamp: '2026-06-19T11:30:00.000Z',
method: 'session/update',
params: {
sessionId: uuid,
update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'hi' } },
_meta: { totalTokens: total, promptId: turn.promptId, updateType: 'AgentMessageChunk', modelId: model },
},
}))
}
}
for (const completed of opts.completedTurns ?? []) {
lines.push(JSON.stringify({
timestamp: 1786724773,
method: '_x.ai/session/update',
params: {
sessionId: uuid,
update: {
sessionUpdate: 'turn_completed',
...(completed.promptId === undefined ? {} : { prompt_id: completed.promptId }),
usage: completed.usage,
},
_meta: { eventId: 'event-1', agentTimestampMs: 1786724773589 },
},
}))
}
for (const tc of opts.toolCalls ?? [
{ title: 'read_file', rawInput: { target_directory: '.' } },
{ title: 'grep', rawInput: { pattern: 'x' } },
{ title: 'run_terminal_command', rawInput: { command: 'git status' } },
{ title: 'spawn_subagent', rawInput: { subagent_type: 'general-purpose', prompt: 'x' } },
]) {
lines.push(JSON.stringify({
timestamp: '2026-06-19T11:30:05.000Z',
method: 'session/update',
params: { sessionId: uuid, update: { sessionUpdate: 'tool_call', toolCallId: 'c1', title: tc.title, rawInput: tc.rawInput } },
}))
}
await writeFile(join(dir, 'updates.jsonl'), lines.join('\n') + '\n')
return { dir, uuid }
}
function authoritativeUsage(opts: {
input?: number
output?: number
cacheRead?: number
cacheCreation?: number
reasoning?: number
model?: string
modelUsage?: Record<string, Record<string, unknown>>
} = {}): Record<string, unknown> {
const input = opts.input ?? 1000
const output = opts.output ?? 100
const cacheRead = opts.cacheRead ?? 0
const cacheCreation = opts.cacheCreation ?? 0
const reasoning = opts.reasoning ?? 0
const model = opts.model ?? 'grok-4.6-build'
const singleModelUsage = {
inputTokens: input,
outputTokens: output,
totalTokens: input + output,
cachedReadTokens: cacheRead,
cacheCreationTokens: cacheCreation,
reasoningTokens: reasoning,
modelCalls: 1,
apiDurationMs: 1000,
costUsdTicks: 125117780000,
}
return {
inputTokens: input,
outputTokens: output,
totalTokens: input + output,
cachedReadTokens: cacheRead,
cacheCreationTokens: cacheCreation,
reasoningTokens: reasoning,
modelCalls: 1,
apiDurationMs: 1000,
costUsdTicks: 125117780000,
modelUsage: opts.modelUsage ?? { [model]: singleModelUsage },
numTurns: 1,
}
}
describe('grok provider - discovery', () => {
it('discovers each session dir and derives project from cwd', async () => {
await writeSession({ cwd: '/Users/test/myproject' })
const sessions = await createGrokProvider(tmpDir).discoverSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0]!.provider).toBe('grok')
expect(sessions[0]!.project).toBe('myproject')
expect(sessions[0]!.path).toMatch(/updates\.jsonl$/)
})
it('returns empty for a non-existent sessions dir', async () => {
const sessions = await createGrokProvider('/nope/does/not/exist').discoverSessions()
expect(sessions).toEqual([])
})
it('skips directories without a summary.json', async () => {
await mkdir(join(tmpDir, '%2Ftmp', 'not-a-session'), { recursive: true })
const sessions = await createGrokProvider(tmpDir).discoverSessions()
expect(sessions).toEqual([])
})
})
describe('grok provider - parsing', () => {
async function parse(seen = new Set<string>()) {
const provider = createGrokProvider(tmpDir)
const [source] = await provider.discoverSessions()
const calls: ParsedProviderCall[] = []
if (!source) return calls
for await (const call of provider.createSessionParser(source, seen).parse()) {
calls.push(call)
}
return calls
}
it('emits one estimated call per session from the totalTokens fallback curve', async () => {
await writeSession()
const calls = await parse()
expect(calls).toHaveLength(1)
const call = calls[0]!
expect(call.model).toBe('grok-build')
// input = peak context (max totalTokens across the session)
expect(call.inputTokens).toBe(45000)
// cache reads = re-sent context (sum of per-turn starts 90000 minus peak 45000)
expect(call.cacheReadInputTokens).toBe(45000)
// output = sum of per-turn growth (3 turns x 5000)
expect(call.outputTokens).toBe(15000)
expect(call.costIsEstimated).toBe(true)
expect(call.costUSD).toBeGreaterThan(0)
expect(call.tools).toEqual(['Read', 'Grep', 'Bash', 'Agent'])
expect(call.bashCommands).toContain('git')
expect(call.subagentTypes).toEqual(['general-purpose'])
expect(call.project).toBe('myproject')
expect(call.deduplicationKey).toContain('grok:')
})
it('uses one turn_completed usage record as authoritative and splits cache subsets from input', async () => {
await writeSession({
turns: [],
completedTurns: [{
promptId: 'real-prompt-1',
usage: authoritativeUsage({
input: 12851663,
output: 36633,
cacheRead: 12092032,
cacheCreation: 0,
reasoning: 29077,
}),
}],
})
const calls = await parse()
expect(calls).toHaveLength(1)
const call = calls[0]!
expect(call.model).toBe('grok-build')
expect(call.inputTokens).toBe(759631) // 12851663 - 12092032 - 0
expect(call.cacheReadInputTokens).toBe(12092032)
expect(call.cacheCreationInputTokens).toBe(0)
// Grok reports reasoning inside outputTokens; the repo contract wants them
// split, so output is emitted exclusive of reasoning and the two sum back
// to the 36633 the record reported.
expect(call.outputTokens).toBe(7556) // 36633 - 29077
expect(call.reasoningTokens).toBe(29077)
expect(call.outputTokens + call.reasoningTokens).toBe(36633)
expect(call.costIsEstimated).toBe(false)
expect(call.costUSD).toBe(calculateCost('grok-build', 759631, 36633, 0, 12092032, 0))
})
it('sums distinct turn_completed prompt ids exactly once each', async () => {
await writeSession({
turns: [],
completedTurns: [
{ promptId: 'p1', usage: authoritativeUsage({ input: 1000, output: 100, cacheRead: 600, cacheCreation: 50, reasoning: 10 }) },
{ promptId: 'p2', usage: authoritativeUsage({ input: 2000, output: 200, cacheRead: 1000, cacheCreation: 100, reasoning: 20 }) },
],
})
const [call] = await parse()
expect(call).toMatchObject({
inputTokens: 1250,
cacheReadInputTokens: 1600,
cacheCreationInputTokens: 150,
outputTokens: 270, // 300 reported - 30 reasoning
reasoningTokens: 30,
})
})
it('keeps one authoritative call and uses top-level totals for multi-model usage', async () => {
await writeSession({
turns: [],
completedTurns: [{
promptId: 'multi-model',
usage: authoritativeUsage({
input: 3000,
output: 300,
cacheRead: 600,
cacheCreation: 100,
reasoning: 30,
modelUsage: {
'grok-build-0.1': {
inputTokens: 1000,
outputTokens: 100,
cachedReadTokens: 100,
cacheCreationTokens: 0,
reasoningTokens: 10,
},
'grok-latest': {
inputTokens: 2000,
outputTokens: 200,
cachedReadTokens: 500,
cacheCreationTokens: 100,
reasoningTokens: 20,
},
},
}),
}],
})
const calls = await parse()
expect(calls).toHaveLength(1)
expect(calls[0]).toMatchObject({
model: 'grok-build-0.1',
inputTokens: 2300,
outputTokens: 270,
reasoningTokens: 30,
costUSD: calculateCost('grok-build-0.1', 2300, 300, 100, 600, 0),
})
expect(calls[0]!.outputTokens + calls[0]!.reasoningTokens).toBe(300)
expect(calls[0]!.turnId).toBeUndefined()
})
it('uses the last turn_completed record for a duplicate prompt id', async () => {
await writeSession({
turns: [],
completedTurns: [
{ promptId: 'same-prompt', usage: authoritativeUsage({ input: 500, output: 50, cacheRead: 100, reasoning: 5 }) },
{ promptId: 'same-prompt', usage: authoritativeUsage({ input: 800, output: 80, cacheRead: 200, cacheCreation: 25, reasoning: 8 }) },
],
})
const [call] = await parse()
expect(call).toMatchObject({
inputTokens: 575,
cacheReadInputTokens: 200,
cacheCreationInputTokens: 25,
outputTokens: 72, // 80 reported - 8 reasoning
reasoningTokens: 8,
})
})
it('uses unique fallback keys when completed records omit prompt_id', async () => {
await writeSession({
turns: [],
completedTurns: [
{ usage: authoritativeUsage({ input: 100, output: 10 }) },
{ usage: authoritativeUsage({ input: 200, output: 20 }) },
],
})
const [call] = await parse()
expect(call).toMatchObject({ inputTokens: 300, outputTokens: 30, reasoningTokens: 0 })
})
it('ignores a still-streaming turn but marks mixed coverage estimated', async () => {
await writeSession({
turns: [{ promptId: 'still-streaming', totals: [10000, 15000] }],
completedTurns: [{ promptId: 'completed', usage: authoritativeUsage({ input: 900, output: 90, cacheRead: 300, reasoning: 20 }) }],
})
const [call] = await parse()
expect(call).toMatchObject({
inputTokens: 600,
cacheReadInputTokens: 300,
outputTokens: 70, // 90 reported - 20 reasoning
reasoningTokens: 20,
costIsEstimated: true,
})
})
it('treats malformed authoritative fields as absent without throwing or corrupting totals', async () => {
await writeSession({
turns: [],
completedTurns: [{
promptId: 'malformed',
usage: {
inputTokens: -1,
outputTokens: 4,
totalTokens: 'not-a-number',
cachedReadTokens: Number.NaN,
cacheCreationTokens: 'not-a-number',
reasoningTokens: -2,
modelUsage: {},
},
}],
})
const [call] = await parse()
expect(call).toBeDefined()
expect(call!.inputTokens).toBe(0)
expect(call!.outputTokens).toBe(4)
expect(call!.cacheReadInputTokens).toBe(0)
expect(call!.cacheCreationInputTokens).toBe(0)
expect(call!.reasoningTokens).toBe(0)
expect(Number.isFinite(call!.costUSD)).toBe(true)
expect(call!.costUSD).toBeGreaterThanOrEqual(0)
})
it('keeps the heuristic when a completed record reports all-zero usage', async () => {
await writeSession({
turns: [
{ promptId: 'streaming-1', totals: [20000, 25000] },
{ promptId: 'streaming-2', totals: [30000, 35000] },
],
completedTurns: [{
promptId: 'zero-usage',
usage: authoritativeUsage({ input: 0, output: 0, cacheRead: 0, cacheCreation: 0, reasoning: 0 }),
}],
})
const [call] = await parse()
expect(call).toBeDefined()
expect(call!.inputTokens).toBe(35000)
expect(call!.cacheReadInputTokens).toBe(15000)
expect(call!.outputTokens).toBe(10000)
expect(call!.costIsEstimated).toBe(true)
})
it('clamps cache-exclusive input per completed record before summing', async () => {
await writeSession({
turns: [],
completedTurns: [
{ promptId: 'inconsistent', usage: authoritativeUsage({ input: 100, output: 10, cacheRead: 80, cacheCreation: 50 }) },
{ promptId: 'consistent', usage: authoritativeUsage({ input: 100, output: 20 }) },
],
})
const [call] = await parse()
expect(call).toMatchObject({
inputTokens: 100,
cacheReadInputTokens: 80,
cacheCreationInputTokens: 50,
outputTokens: 30,
})
})
it('does not add reasoning tokens on top of provider-reported output for cost', async () => {
await writeSession({
turns: [],
model: 'grok-build',
completedTurns: [{
promptId: 'reasoning-subset',
usage: authoritativeUsage({
input: 1000,
output: 200,
cacheRead: 500,
cacheCreation: 100,
reasoning: 150,
model: 'grok-build',
}),
}],
})
const [call] = await parse()
expect(call).toBeDefined()
expect(call!.inputTokens).toBe(400)
// Output is emitted exclusive of reasoning, and the two sum to the 200 the
// record reported. The cost prices that full 200 once - the downstream
// `outputTokens + reasoningTokens` recompute lands on the same number.
expect(call!.outputTokens).toBe(50) // 200 - 150
expect(call!.reasoningTokens).toBe(150)
expect(call!.outputTokens + call!.reasoningTokens).toBe(200)
expect(call!.costUSD).toBe(calculateCost('grok-build', 400, 200, 100, 500, 0))
expect(call!.costUSD).not.toBe(calculateCost('grok-build', 400, 350, 100, 500, 0))
})
it('skips a session with no token growth', async () => {
await writeSession({ turns: [{ promptId: 'p1', totals: [0, 0] }] })
expect(await parse()).toHaveLength(0)
})
it('deduplicates across repeated parses', async () => {
await writeSession()
const seen = new Set<string>()
expect(await parse(seen)).toHaveLength(1)
expect(await parse(seen)).toHaveLength(0)
})
it('sums fresh input across a compaction instead of only the last peak', async () => {
await writeSession({ turns: [
{ promptId: 'p1', totals: [100000, 400000] },
{ promptId: 'p2', totals: [20000, 50000] },
] })
const calls = await parse()
expect(calls).toHaveLength(1)
// 400k (segment 1 peak) + 50k (post-compaction segment), not just the 400k global peak
expect(calls[0]!.inputTokens).toBe(450000)
})
})
describe('grok provider - display names', () => {
const provider = createGrokProvider('/tmp')
it('has the right name and displayName', () => {
expect(provider.name).toBe('grok')
expect(provider.displayName).toBe('Grok Build')
})
it('labels grok-build', () => {
expect(provider.modelDisplayName('grok-build')).toBe('Grok Build')
})
// Two distinct ids, so two rows; identical names made them look like one row
// printed twice (#1029).
it('distinguishes the build variant of a model from the model itself', () => {
expect(provider.modelDisplayName('grok-4.5')).toBe('Grok 4.5')
expect(provider.modelDisplayName('grok-4.5-build')).toBe('Grok 4.5 (build)')
})
it('normalizes tool names', () => {
expect(provider.toolDisplayName('run_terminal_command')).toBe('Bash')
expect(provider.toolDisplayName('mystery_tool')).toBe('mystery_tool')
})
})