Skip to content

Commit 8cf68e7

Browse files
committed
Fix Antigravity dedup collision and add Codex ChatGPT Plus token estimation (#204)
- Antigravity: use loop index as fallback when responseId is empty to prevent all entries in a cascade sharing the same dedup key; bump CACHE_VERSION to force re-parse of stale cached data - Codex: estimate tokens from message text when info is null (ChatGPT Plus/Pro subscription sessions), feeding through calculateCost so subscription users see API-equivalent spend; add costIsEstimated flag to ParsedProviderCall - Update LiteLLM pricing snapshot
1 parent 95585fe commit 8cf68e7

4 files changed

Lines changed: 60 additions & 5 deletions

File tree

src/data/litellm-snapshot.json

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

src/providers/antigravity.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { calculateCost } from '../models.js'
99
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
1010

1111
const CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity', 'conversations')
12-
const CACHE_VERSION = 1
12+
const CACHE_VERSION = 2
1313

1414
const RPC_TIMEOUT_MS = 5000
1515
const MAX_RESPONSE_BYTES = 16 * 1024 * 1024
@@ -283,7 +283,8 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
283283

284284
const results: ParsedProviderCall[] = []
285285

286-
for (const entry of metadata) {
286+
for (let i = 0; i < metadata.length; i++) {
287+
const entry = metadata[i]!
287288
const usage = entry.chatModel?.usage
288289
if (!usage) continue
289290

@@ -294,7 +295,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
294295

295296
if (inputTokens === 0 && outputTokens === 0) continue
296297

297-
const responseId = usage.responseId ?? ''
298+
const responseId = usage.responseId || String(i)
298299
const dedupKey = `antigravity:${cascadeId}:${responseId}`
299300

300301
const model = modelMap[usage.model] ?? usage.model

src/providers/codex.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type CodexTokenUsage = {
6464
total_tokens?: number
6565
}
6666

67+
const CHARS_PER_TOKEN = 4
68+
6769
function getCodexDir(override?: string): string {
6870
return override ?? process.env['CODEX_HOME'] ?? join(homedir(), '.codex')
6971
}
@@ -211,6 +213,8 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
211213
let prevReasoning = 0
212214
let pendingTools: string[] = []
213215
let pendingUserMessage = ''
216+
let pendingOutputChars = 0
217+
let estCounter = 0
214218
const results: ParsedProviderCall[] = []
215219

216220
for (const line of lines) {
@@ -252,9 +256,57 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
252256
continue
253257
}
254258

259+
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload?.role === 'assistant') {
260+
const texts = (entry.payload.content ?? [])
261+
.filter(c => c.type === 'output_text' || c.type === 'text')
262+
.map(c => c.text ?? '')
263+
pendingOutputChars += texts.join('').length
264+
continue
265+
}
266+
255267
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count') {
256268
const info = entry.payload.info
257-
if (!info) continue
269+
if (!info) {
270+
if (pendingOutputChars === 0 && pendingUserMessage.length === 0) continue
271+
const estInput = Math.ceil(pendingUserMessage.length / CHARS_PER_TOKEN)
272+
const estOutput = Math.ceil(pendingOutputChars / CHARS_PER_TOKEN)
273+
if (estInput === 0 && estOutput === 0) continue
274+
275+
const model = sessionModel ?? 'gpt-5'
276+
const timestamp = entry.timestamp ?? ''
277+
const dedupKey = `codex:${sessionId}:${timestamp}:est${estCounter++}`
278+
279+
if (seenKeys.has(dedupKey)) { pendingTools = []; pendingUserMessage = ''; pendingOutputChars = 0; continue }
280+
seenKeys.add(dedupKey)
281+
282+
const costUSD = calculateCost(model, estInput, estOutput, 0, 0, 0)
283+
284+
results.push({
285+
provider: 'codex',
286+
model,
287+
inputTokens: estInput,
288+
outputTokens: estOutput,
289+
cacheCreationInputTokens: 0,
290+
cacheReadInputTokens: 0,
291+
cachedInputTokens: 0,
292+
reasoningTokens: 0,
293+
webSearchRequests: 0,
294+
costUSD,
295+
costIsEstimated: true,
296+
tools: pendingTools,
297+
bashCommands: [],
298+
timestamp,
299+
speed: 'standard',
300+
deduplicationKey: dedupKey,
301+
userMessage: pendingUserMessage,
302+
sessionId,
303+
})
304+
305+
pendingTools = []
306+
pendingUserMessage = ''
307+
pendingOutputChars = 0
308+
continue
309+
}
258310

259311
const cumulativeTotal = info.total_token_usage?.total_tokens ?? 0
260312
if (cumulativeTotal > 0 && cumulativeTotal === prevCumulativeTotal) continue
@@ -335,6 +387,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
335387

336388
pendingTools = []
337389
pendingUserMessage = ''
390+
pendingOutputChars = 0
338391
}
339392
}
340393

src/providers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type ParsedProviderCall = {
1919
reasoningTokens: number
2020
webSearchRequests: number
2121
costUSD: number
22+
costIsEstimated?: boolean
2223
tools: string[]
2324
bashCommands: string[]
2425
timestamp: string

0 commit comments

Comments
 (0)