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

Skip to content

Commit d0f1f82

Browse files
authored
Fix Antigravity 2 Gemini 3.5 Flash tracking (#377)
1 parent e472e37 commit d0f1f82

6 files changed

Lines changed: 253 additions & 41 deletions

File tree

src/models.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,12 @@ const BUILTIN_ALIASES: Record<string, string> = {
245245
'gemini-3.1-pro-high': 'gemini-3.1-pro-preview',
246246
'gemini-3.1-pro-low': 'gemini-3.1-pro-preview',
247247
'gemini-3-flash-agent': 'gemini-3-flash-preview',
248+
'gemini-3.5-flash-high': 'gemini-3.5-flash',
249+
'gemini-3.5-flash-medium': 'gemini-3.5-flash',
250+
'gemini-3.5-flash-low': 'gemini-3.5-flash',
251+
'Gemini 3.5 Flash (High)': 'gemini-3.5-flash',
252+
'Gemini 3.5 Flash (Medium)': 'gemini-3.5-flash',
253+
'Gemini 3.5 Flash (Low)': 'gemini-3.5-flash',
248254
'gemini-3-pro': 'gemini-3-pro-preview',
249255
'gemini-3.1-flash-image': 'gemini-3.1-flash-image-preview',
250256
'gemini-3.1-flash-lite': 'gemini-3.1-flash-lite-preview',
@@ -424,6 +430,7 @@ const SHORT_NAMES: Record<string, string> = {
424430
'gpt-5-nano': 'GPT-5 Nano',
425431
'gpt-5-mini': 'GPT-5 Mini',
426432
'gpt-5': 'GPT-5',
433+
'gemini-3.5-flash': 'Gemini 3.5 Flash',
427434
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro',
428435
'gemini-3-flash-preview': 'Gemini 3 Flash',
429436
'gemini-2.5-pro': 'Gemini 2.5 Pro',

src/parser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { readSessionLines } from './fs-utils.js'
44
import { calculateCost, getShortModelName } from './models.js'
55
import { discoverAllSessions, getProvider } from './providers/index.js'
66
import { flushCodexCache } from './codex-cache.js'
7-
import { flushAntigravityCache } from './providers/antigravity.js'
7+
import { antigravityCascadeIdFromPath, flushAntigravityCache } from './providers/antigravity.js'
88
import { isSqliteBusyError } from './sqlite.js'
99
import {
1010
type CachedCall,
@@ -1718,7 +1718,7 @@ function getOrCreateProviderSection(cache: SessionCache, provider: string): Prov
17181718
}
17191719

17201720
function cachedFileNeedsProviderReparse(providerName: string, cached: CachedFile): boolean {
1721-
// Antigravity data comes from the live server, not from the .pb file.
1721+
// Antigravity data comes from the live server, not from the conversation file.
17221722
// A 0-turn cache entry may just mean the server was unavailable last run.
17231723
if (providerName === 'antigravity' && cached.turns.length === 0) return true
17241724

@@ -1820,7 +1820,7 @@ async function parseProviderSources(
18201820
} finally {
18211821
if (didParse && providerName === 'codex') await flushCodexCache()
18221822
if (didParse && providerName === 'antigravity') {
1823-
const liveIds = new Set(sources.map(s => basename(s.path, '.pb')))
1823+
const liveIds = new Set(sources.map(s => antigravityCascadeIdFromPath(s.path)))
18241824
await flushAntigravityCache(liveIds)
18251825
}
18261826
}

src/providers/antigravity.ts

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,29 @@ import https from 'https'
88
import { calculateCost } from '../models.js'
99
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
1010

11-
const CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity', 'conversations')
11+
type AntigravityConversationRoot = {
12+
dir: string
13+
project: string
14+
extensions: readonly string[]
15+
}
16+
17+
const CONVERSATION_ROOTS: readonly AntigravityConversationRoot[] = [
18+
{
19+
dir: join(homedir(), '.gemini', 'antigravity', 'conversations'),
20+
project: 'antigravity',
21+
extensions: ['.pb', '.db'],
22+
},
23+
{
24+
dir: join(homedir(), '.gemini', 'antigravity-cli', 'conversations'),
25+
project: 'antigravity-cli',
26+
extensions: ['.pb'],
27+
},
28+
{
29+
dir: join(homedir(), '.gemini', 'antigravity-cli', 'implicit'),
30+
project: 'antigravity-cli',
31+
extensions: ['.pb'],
32+
},
33+
] as const
1234
const CACHE_VERSION = 2
1335

1436
const RPC_TIMEOUT_MS = 5000
@@ -19,6 +41,10 @@ export type ServerInfo = {
1941
csrfToken: string
2042
}
2143

44+
type ServerCandidate = ServerInfo & {
45+
appDataDir?: 'antigravity' | 'antigravity-cli'
46+
}
47+
2248
type ModelMap = Record<string, string>
2349

2450
type UsageEntry = {
@@ -67,14 +93,15 @@ type AntigravityCache = {
6793
cascades: Record<string, CachedCascade>
6894
}
6995

70-
let cachedServer: ServerInfo | null | undefined
71-
let cachedModelMap: ModelMap | undefined
96+
const cachedServers = new Map<string, ServerInfo | null>()
97+
const cachedModelMaps = new Map<string, ModelMap>()
7298
let memCache: AntigravityCache | null = null
7399
let cacheDirty = false
74100
let httpsAgent: https.Agent | undefined
75101

76102
const SERVER_PORT_FLAGS = ['https_server_port', 'extension_server_port']
77103
const CSRF_TOKEN_FLAGS = ['csrf_token', 'extension_server_csrf_token']
104+
const APP_DATA_DIR_FLAGS = ['app_data_dir']
78105

79106
function getAgent(): https.Agent {
80107
if (!httpsAgent) httpsAgent = new https.Agent({ rejectUnauthorized: false })
@@ -111,7 +138,19 @@ function isLikelyCsrfToken(value: string): boolean {
111138
return value.length >= 16 && /^[A-Za-z0-9._~:/+=-]+$/.test(value)
112139
}
113140

114-
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | { port: 0; csrfToken: string } | null {
141+
function normalizeAppDataDir(value: string | null): 'antigravity' | 'antigravity-cli' | undefined {
142+
if (!value) return undefined
143+
const normalized = value.replace(/\\/g, '/').toLowerCase()
144+
if (normalized.includes('antigravity-cli')) return 'antigravity-cli'
145+
if (normalized.includes('antigravity')) return 'antigravity'
146+
return undefined
147+
}
148+
149+
export function extractAntigravityAppDataDirFromLine(line: string): 'antigravity' | 'antigravity-cli' | undefined {
150+
return normalizeAppDataDir(getFlagValue(line, APP_DATA_DIR_FLAGS))
151+
}
152+
153+
function parseAntigravityServerCandidateFromLine(line: string): ServerCandidate | null {
115154
const lower = line.toLowerCase()
116155
if (!lower.includes('language_server') || !lower.includes('antigravity')) return null
117156

@@ -123,7 +162,16 @@ export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | {
123162
const port = Number(rawPort)
124163
if (!Number.isInteger(port) || port < 0 || port > 65535) return null
125164

126-
return { port, csrfToken }
165+
return {
166+
port,
167+
csrfToken,
168+
appDataDir: extractAntigravityAppDataDirFromLine(line),
169+
}
170+
}
171+
172+
export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | { port: 0; csrfToken: string } | null {
173+
const candidate = parseAntigravityServerCandidateFromLine(line)
174+
return candidate ? { port: candidate.port, csrfToken: candidate.csrfToken } : null
127175
}
128176

129177
export function parseAntigravityServerInfo(lines: string[]): ServerInfo | null {
@@ -134,6 +182,12 @@ export function parseAntigravityServerInfo(lines: string[]): ServerInfo | null {
134182
return null
135183
}
136184

185+
function parseAntigravityServerCandidates(lines: string[]): ServerCandidate[] {
186+
return lines
187+
.map(parseAntigravityServerCandidateFromLine)
188+
.filter((server): server is ServerCandidate => server !== null)
189+
}
190+
137191
export function extractAntigravityModelMap(resp: unknown): ModelMap {
138192
if (!resp || typeof resp !== 'object') return {}
139193
const data = resp as ModelMapResponse
@@ -222,11 +276,21 @@ async function readProcessCommandLines(): Promise<string[]> {
222276
return output.split('\n')
223277
}
224278

225-
async function resolveEphemeralPort(csrfToken: string): Promise<ServerInfo | null> {
279+
async function resolveEphemeralPort(csrfToken: string, appDataDir?: 'antigravity' | 'antigravity-cli'): Promise<ServerInfo | null> {
226280
if (process.platform === 'win32') return null
227281
try {
228-
const pidOutput = await execFileText('pgrep', ['-f', 'language_server.*antigravity'])
229-
const pid = pidOutput.trim().split('\n')[0]
282+
const processOutput = await execFileText('ps', ['-ww', '-eo', 'pid=,args='])
283+
let pid = ''
284+
for (const line of processOutput.split('\n')) {
285+
const match = line.trim().match(/^(\d+)\s+(.+)$/)
286+
if (!match) continue
287+
const candidate = parseAntigravityServerCandidateFromLine(match[2]!)
288+
if (!candidate) continue
289+
if (candidate.csrfToken !== csrfToken) continue
290+
if (appDataDir && candidate.appDataDir && candidate.appDataDir !== appDataDir) continue
291+
pid = match[1]!
292+
break
293+
}
230294
if (!pid) return null
231295
const lsofOutput = await execFileText('lsof', ['-a', '-i', '-P', '-n', '-p', pid])
232296
for (const line of lsofOutput.split('\n')) {
@@ -241,20 +305,29 @@ async function resolveEphemeralPort(csrfToken: string): Promise<ServerInfo | nul
241305
return null
242306
}
243307

244-
async function detectServer(): Promise<ServerInfo | null> {
245-
if (cachedServer !== undefined) return cachedServer
308+
export function antigravityAppDataDirFromSourcePath(path: string): 'antigravity' | 'antigravity-cli' {
309+
return path.replace(/\\/g, '/').toLowerCase().includes('/.gemini/antigravity-cli/')
310+
? 'antigravity-cli'
311+
: 'antigravity'
312+
}
313+
314+
async function detectServer(appDataDir: 'antigravity' | 'antigravity-cli' = 'antigravity'): Promise<ServerInfo | null> {
315+
if (cachedServers.has(appDataDir)) return cachedServers.get(appDataDir)!
246316
try {
247-
const info = parseAntigravityServerInfo(await readProcessCommandLines())
317+
const candidates = parseAntigravityServerCandidates(await readProcessCommandLines())
318+
const info = candidates.find(candidate => candidate.appDataDir === appDataDir)
319+
?? (appDataDir === 'antigravity' ? candidates.find(candidate => candidate.appDataDir === undefined) : undefined)
320+
?? null
248321
if (info && info.port > 0) {
249-
cachedServer = info as ServerInfo
322+
cachedServers.set(appDataDir, { port: info.port, csrfToken: info.csrfToken })
250323
} else if (info && info.port === 0) {
251-
cachedServer = await resolveEphemeralPort(info.csrfToken)
324+
cachedServers.set(appDataDir, await resolveEphemeralPort(info.csrfToken, appDataDir))
252325
} else {
253-
cachedServer = null
326+
cachedServers.set(appDataDir, null)
254327
}
255-
return cachedServer
328+
return cachedServers.get(appDataDir)!
256329
} catch { /* process discovery failed or timed out */ }
257-
cachedServer = null
330+
cachedServers.set(appDataDir, null)
258331
return null
259332
}
260333

@@ -307,13 +380,16 @@ async function rpc(server: ServerInfo, method: string, body: Record<string, unkn
307380
}
308381

309382
async function getModelMap(server: ServerInfo): Promise<ModelMap> {
383+
const cacheKey = `${server.port}:${server.csrfToken}`
384+
const cachedModelMap = cachedModelMaps.get(cacheKey)
310385
if (cachedModelMap) return cachedModelMap
311386
try {
312-
cachedModelMap = extractAntigravityModelMap(await rpc(server, 'GetAvailableModels'))
313-
return cachedModelMap
387+
const modelMap = extractAntigravityModelMap(await rpc(server, 'GetAvailableModels'))
388+
cachedModelMaps.set(cacheKey, modelMap)
389+
return modelMap
314390
} catch { /* best-effort */ }
315-
cachedModelMap = {}
316-
return cachedModelMap
391+
cachedModelMaps.set(cacheKey, {})
392+
return {}
317393
}
318394

319395
// Strip Antigravity-specific suffixes so the pricing DB can match
@@ -322,34 +398,50 @@ const PRICING_ALIASES: Record<string, string> = {
322398
}
323399

324400
function normalizePricingModel(model: string): string {
325-
const stripped = model.replace(/-(high|low|agent)$/, '')
401+
const stripped = model.replace(/-(high|medium|low|agent)$/, '')
326402
return PRICING_ALIASES[stripped] ?? stripped
327403
}
328404

329-
async function discoverSessions(): Promise<SessionSource[]> {
405+
export function antigravityCascadeIdFromPath(path: string): string {
406+
return basename(path).replace(/\.(pb|db)$/i, '')
407+
}
408+
409+
function isConversationFile(file: string, extensions: readonly string[]): boolean {
410+
const lowerFile = file.toLowerCase()
411+
return extensions.some(ext => lowerFile.endsWith(ext))
412+
}
413+
414+
export async function discoverAntigravitySessionSources(
415+
roots: readonly AntigravityConversationRoot[] = CONVERSATION_ROOTS,
416+
): Promise<SessionSource[]> {
330417
const sources: SessionSource[] = []
331-
let files: string[]
332-
try {
333-
files = await readdir(CONVERSATIONS_DIR)
334-
} catch {
335-
return sources
336-
}
418+
for (const root of roots) {
419+
let files: string[]
420+
try {
421+
files = await readdir(root.dir)
422+
} catch {
423+
continue
424+
}
337425

338-
for (const file of files) {
339-
if (!file.endsWith('.pb')) continue
340-
sources.push({
341-
path: join(CONVERSATIONS_DIR, file),
342-
project: 'antigravity',
343-
provider: 'antigravity',
344-
})
426+
for (const file of files.sort()) {
427+
if (!isConversationFile(file, root.extensions)) continue
428+
const path = join(root.dir, file)
429+
const s = await stat(path).catch(() => null)
430+
if (!s?.isFile()) continue
431+
sources.push({
432+
path,
433+
project: root.project,
434+
provider: 'antigravity',
435+
})
436+
}
345437
}
346438
return sources
347439
}
348440

349441
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
350442
return {
351443
async *parse(): AsyncGenerator<ParsedProviderCall> {
352-
const cascadeId = basename(source.path, '.pb')
444+
const cascadeId = antigravityCascadeIdFromPath(source.path)
353445
const cache = await loadCache()
354446

355447
const s = await stat(source.path).catch(() => null)
@@ -365,7 +457,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
365457
return
366458
}
367459

368-
const server = await detectServer()
460+
const server = await detectServer(antigravityAppDataDirFromSourcePath(source.path))
369461
if (!server) {
370462
if (cached) {
371463
for (const call of cached.calls) {
@@ -461,6 +553,9 @@ const modelDisplayNames: Record<string, string> = {
461553
'gemini-3.1-pro-low': 'Gemini 3.1 Pro (Low)',
462554
'gemini-3-flash': 'Gemini 3 Flash',
463555
'gemini-3-flash-agent': 'Gemini 3 Flash',
556+
'gemini-3.5-flash': 'Gemini 3.5 Flash',
557+
'gemini-3.5-flash-high': 'Gemini 3.5 Flash',
558+
'gemini-3.5-flash-medium': 'Gemini 3.5 Flash',
464559
'gemini-3.5-flash-low': 'Gemini 3.5 Flash',
465560
'gemini-3.1-flash-image': 'Gemini 3.1 Flash',
466561
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
@@ -482,7 +577,7 @@ export function createAntigravityProvider(): Provider {
482577
},
483578

484579
async discoverSessions(): Promise<SessionSource[]> {
485-
return discoverSessions()
580+
return discoverAntigravitySessionSources()
486581
},
487582

488583
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {

src/providers/gemini.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export function createGeminiProvider(): Provider {
259259
if (model === 'gemini-auto') return 'Gemini (auto)'
260260
const display: Record<string, string> = {
261261
'gemini-3-flash-preview': 'Gemini 3 Flash',
262+
'gemini-3.5-flash': 'Gemini 3.5 Flash',
262263
'gemini-3.1-pro-preview': 'Gemini 3.1 Pro',
263264
'gemini-2.5-pro': 'Gemini 2.5 Pro',
264265
'gemini-2.5-flash': 'Gemini 2.5 Flash',

tests/models.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ describe('builtin aliases - getShortModelName', () => {
128128
})
129129
})
130130

131+
describe('Antigravity Gemini 3.5 Flash variants resolve to pricing', () => {
132+
const variants = [
133+
'gemini-3.5-flash',
134+
'gemini-3.5-flash-high',
135+
'gemini-3.5-flash-medium',
136+
'gemini-3.5-flash-low',
137+
'Gemini 3.5 Flash (High)',
138+
]
139+
140+
for (const variant of variants) {
141+
it(`${variant} resolves to Gemini 3.5 Flash`, () => {
142+
expect(getModelCosts(variant)).toEqual(getModelCosts('gemini-3.5-flash'))
143+
expect(getShortModelName(variant)).toBe('Gemini 3.5 Flash')
144+
})
145+
}
146+
147+
it('calculates non-zero cost for high thinking labels', () => {
148+
expect(calculateCost('gemini-3.5-flash-high', 1000, 100, 0, 0, 0)).toBeGreaterThan(0)
149+
})
150+
})
151+
131152
describe('user aliases via setModelAliases', () => {
132153
it('user alias resolves for getModelCosts', () => {
133154
setModelAliases({ 'my-internal-model': 'claude-sonnet-4-6' })

0 commit comments

Comments
 (0)