Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7437b28
docs: add Antigravity log analysis design spec
SecH0us3 Jun 30, 2026
5fb1108
docs: add Antigravity log analysis implementation plan
SecH0us3 Jun 30, 2026
a73ad00
feat: add Antigravity discovery and protobuf decoder
SecH0us3 Jun 30, 2026
94a7d8c
feat: implement Antigravity sqlite database parser
SecH0us3 Jun 30, 2026
bbf9f0a
feat: register Antigravity external harness and discovery paths
SecH0us3 Jun 30, 2026
b72d0ec
feat: add color styling for Antigravity harness in dashboard UI
SecH0us3 Jun 30, 2026
82e5f34
chore: update spellcheck dictionary and finalize testing
SecH0us3 Jun 30, 2026
ab377da
fix: implement non-blocking asynchronous parsing for Antigravity data…
SecH0us3 Jun 30, 2026
e4d7dc9
perf: cache resolved directory mappings in Claude workspace path parser
SecH0us3 Jun 30, 2026
c94880e
perf: fix exponential recursion in protobuf decoder and add SQLite bu…
SecH0us3 Jun 30, 2026
a3cd503
perf: merge metadata and steps queries into a single process execution
SecH0us3 Jun 30, 2026
37d7434
perf: fix offset increment bug and whitelist fields in protobuf decoder
SecH0us3 Jun 30, 2026
20de6f1
feat: improve Antigravity parser precision with path-based filtering …
SecH0us3 Jul 1, 2026
829aeee
style: update Antigravity color to purple in Context Health page
SecH0us3 Jul 1, 2026
5a66aee
chore: remove temporary design docs and plans
SecH0us3 Jul 1, 2026
895fa4a
test: add unit tests for Antigravity model detection, skills usage, a…
SecH0us3 Jul 1, 2026
c134b41
fix: add nested protobuf parsing support for prompt and attachments i…
SecH0us3 Jul 1, 2026
301c250
chore: remove temporary scratch file scratch_db.ts
SecH0us3 Jul 1, 2026
d9fd1da
fix: resolve test failures for Antigravity protobuf and image extraction
SecH0us3 Jul 1, 2026
3044cf9
fix: correct protobuf empty-record decoding fallback and fix test db …
SecH0us3 Jul 1, 2026
a440558
fix: add support and correct cost/multiplier lookup for Antigravity m…
SecH0us3 Jul 1, 2026
770214a
feat: preserve model suffixes in charts, tables and legend
SecH0us3 Jul 1, 2026
c7afa9c
feat: explicitly support all Antigravity suffix model variants in MOD…
SecH0us3 Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"minWordLength": 5,
"words": [
"aicoach",
"varint",
"swazz",
"acked",
"affordances",
"akiaiosfodnn",
Expand Down
10 changes: 5 additions & 5 deletions src/core/analyzer-consumption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function attributeSessionLevel(
const byModel = new Map<string, SessionRequest[]>();
for (const r of session.requests) {
if (delegatedRequests?.has(r)) continue;
const m = normalizeModel(r.modelId || 'untracked');
const m = normalizeModel(r.modelId || 'untracked', true);
if (!byModel.has(m)) byModel.set(m, []);
byModel.get(m)!.push(r);
}
Expand Down Expand Up @@ -219,7 +219,7 @@ export class ConsumptionAnalyzer extends AnalyzerBase {
const defaultMultipliers: Record<string, number> = {};

for (const r of reqs) {
const model = normalizeModel(r.modelId || 'untracked');
const model = normalizeModel(r.modelId || 'untracked', true);
const mult = modelMultiplier(model);
defaultMultipliers[model] = mult;
modelTotals.set(model, (modelTotals.get(model) || 0) + 1);
Expand Down Expand Up @@ -289,7 +289,7 @@ export class ConsumptionAnalyzer extends AnalyzerBase {
const dailyReqs = new Map<number, number>();
for (const r of reqs) {
const d = new Date(r.timestamp!).getDate();
const mult = modelMultiplier(normalizeModel(r.modelId || 'untracked'));
const mult = modelMultiplier(normalizeModel(r.modelId || 'untracked', true));
dailyReqs.set(d, (dailyReqs.get(d) || 0) + mult);
}

Expand Down Expand Up @@ -426,7 +426,7 @@ export class ConsumptionAnalyzer extends AnalyzerBase {
request: SessionRequest,
billing: RequestBilling | undefined,
): void {
const model = normalizeModel(request.modelId || 'untracked');
const model = normalizeModel(request.modelId || 'untracked', true);
const resolvedBilling: RequestBilling = billing ?? {
uncachedInput: 0,
totalInput: 0,
Expand Down Expand Up @@ -754,7 +754,7 @@ export class ConsumptionAnalyzer extends AnalyzerBase {
countedCount++;
const tokens = (requestBilling?.totalInput ?? 0) + (requestBilling?.output ?? 0);
dailyTokens.set(dayIdx + 1, (dailyTokens.get(dayIdx + 1) || 0) + tokens);
const model = normalizeModel(request.modelId ?? 'unknown');
const model = normalizeModel(request.modelId ?? 'unknown', true);
if (!dailyTokensByModel.has(model)) dailyTokensByModel.set(model, new Map());
const modelDay = dailyTokensByModel.get(model)!;
modelDay.set(dayIdx + 1, (modelDay.get(dayIdx + 1) || 0) + tokens);
Expand Down
4 changes: 2 additions & 2 deletions src/core/analyzer-production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class ProductionAnalyzer extends AnalyzerBase {
const day = toDateStr(request.timestamp!);
const session = this.requestSessionMap.get(request);
const workspaceName = session?.workspaceName || '';
const model = normalizeModel(request.modelId || 'unknown');
const model = normalizeModel(request.modelId || 'unknown', true);
const harness = session?.harness || 'unknown';
for (const block of request.aiCode) {
totalAiLoc += block.loc;
Expand All @@ -46,7 +46,7 @@ export class ProductionAnalyzer extends AnalyzerBase {
const day = request.timestamp ? toDateStr(request.timestamp) : null;
const session = this.requestSessionMap.get(request);
const workspaceName = session?.workspaceName || '';
const model = normalizeModel(request.modelId || 'unknown');
const model = normalizeModel(request.modelId || 'unknown', true);
const harness = session?.harness || 'unknown';
for (const [file, loc] of editLocs) {
totalAiLoc += loc;
Expand Down
6 changes: 5 additions & 1 deletion src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const MODEL_MULTIPLIERS: Record<string, number> = {
'claude-opus-4.7': 7.5,
'claude-haiku-4.5': 0.33,
'gemini-2.0-flash': 0.25, 'gemini-2.5-pro': 1, 'gemini-3-flash': 0.33,
'gemini-3-pro': 1, 'gemini-3.1-pro': 1,
'gemini-3-pro': 1, 'gemini-3.1-pro': 1, 'gemini-3.5-flash': 0.33, 'gemini-3.1-flash': 0.33,
'gpt-oss-120b': 0.5,
'grok-code-fast-1': 0.25, 'raptor-mini': 0, 'goldeneye': 1,
'copilot-internal': 0, 'auto': 1, 'custom-model': 1,
};
Expand Down Expand Up @@ -59,6 +60,9 @@ export const MODEL_TOKEN_RATES: Record<string, TokenRate> = {
'gemini-3-flash': { input: 0.50, cached: 0.05, output: 3.00 },
'gemini-3-pro': { input: 2.00, cached: 0.20, output: 12.00 },
'gemini-3.1-pro': { input: 2.00, cached: 0.20, output: 12.00 },
'gemini-3.5-flash': { input: 0.50, cached: 0.05, output: 3.00 },
'gemini-3.1-flash': { input: 0.50, cached: 0.05, output: 3.00 },
'gpt-oss-120b': { input: 1.00, cached: 0.10, output: 4.00 },
'grok-code-fast-1': { input: 0.20, cached: 0.02, output: 1.50 },
'raptor-mini': { input: 0.25, cached: 0.025, output: 2.00 },
'goldeneye': { input: 1.25, cached: 0.125, output: 10.00 },
Expand Down
11 changes: 9 additions & 2 deletions src/core/dsl/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,21 @@ const MODEL_TIERS: Record<string, number> = {
'o4-mini': 2, 'o3-mini': 1, 'o3': 3, 'o1-mini': 1, 'o1-preview': 2, 'o1': 2,
'gpt-4.1-nano': 0.2, 'gpt-4.1-mini': 0.5, 'gpt-4.1': 1,
'gpt-4-turbo': 1, 'gpt-4': 1,
'gemini-3.1-pro': 1, 'gemini-3-pro': 1, 'gemini-3-flash': 0.33,
'gemini-3.5-flash-low': 0.33, 'gemini-3.5-flash-medium': 0.33, 'gemini-3.5-flash-high': 0.33,
'gemini-3.5-flash': 0.33,
'gemini-3.1-flash-image2': 0.33, 'gemini-3.1-flash': 0.33,
'gemini-3.1-pro-low': 1, 'gemini-3.1-pro-high': 1, 'gemini-3.1-pro': 1,
'gemini-3-pro': 1, 'gemini-3-flash': 0.33,
'gemini-2.5-pro': 1, 'gemini-2.0-flash': 0.3,
'claude-sonnet-4.6-thinking': 1, 'claude-opus-4.6-thinking': 3,
'gpt-oss-120b-medium': 0.5, 'gpt-oss-120b': 0.5,
'grok-code-fast-1': 0.25,
};

function modelTierLookup(raw: string): number {
const id = raw.replace(/^(openai\/|anthropic\/|google\/)/, '').replace(/-\d{4}-\d{2}-\d{2}$/, '').toLowerCase();
for (const [k, v] of Object.entries(MODEL_TIERS)) {
const sortedTiers = Object.entries(MODEL_TIERS).sort((a, b) => b[0].length - a[0].length);
for (const [k, v] of sortedTiers) {
if (id.includes(k)) return v;
}
return 0;
Expand Down
26 changes: 25 additions & 1 deletion src/core/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { describe, it, expect } from 'vitest';
import { fileUriToPath, toDateStr, startOfDay, endOfDay, isoWeek, normalizeModel, modelMultiplier, classifyWorkType } from './helpers';
import { fileUriToPath, toDateStr, startOfDay, endOfDay, isoWeek, normalizeModel, modelMultiplier, classifyWorkType, tokenCostInCredits } from './helpers';

describe('fileUriToPath', () => {
it('converts a Windows file URI', () => {
Expand Down Expand Up @@ -150,6 +150,30 @@ describe('modelMultiplier', () => {
it('returns 1 for unknown models', () => {
expect(modelMultiplier('totally-unknown-model')).toBe(1);
});

it('correctly maps Antigravity model variants via prefix matching', () => {
expect(modelMultiplier('gemini-3.5-flash-low')).toBe(0.33);
expect(modelMultiplier('gemini-3.5-flash-medium')).toBe(0.33);
expect(modelMultiplier('gemini-3.1-pro-high')).toBe(1);
expect(modelMultiplier('claude-sonnet-4.6-thinking')).toBe(1);
expect(modelMultiplier('claude-opus-4.6-thinking')).toBe(3);
expect(modelMultiplier('gpt-oss-120b-medium')).toBe(0.5);
});
});

describe('tokenCostInCredits', () => {
it('calculates cost using prefix match rates', () => {
// gemini-3.5-flash-low should use gemini-3.5-flash rates (input: 0.50, output: 3.00)
// 1M input tokens * $0.50 + 1M output tokens * $3.00 = $3.50 * 100 = 350 credits
const cost = tokenCostInCredits('gemini-3.5-flash-low', 1_000_000, 1_000_000);
expect(cost).toBe(350);
});

it('falls back to modelMultiplier when no prefix rate matches', () => {
// unknown-model-here should fallback to modelMultiplier (1) = 1 credit
const cost = tokenCostInCredits('unknown-model-here', 1_000, 1_000);
expect(cost).toBe(1);
});
});

describe('classifyWorkType', () => {
Expand Down
21 changes: 16 additions & 5 deletions src/core/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ export function fillMonthRange(months: string[]): string[] {
* while the effort value is captured separately via extractReasoningEffortFromModelId. */
const EFFORT_SUFFIX_RE = /-(xhigh|extra-high|max|high|medium|med|low|minimal)$/;

export function normalizeModel(modelId: string): string {
export function normalizeModel(modelId: string, keepSuffix = false): string {
let m = modelId.trim();
for (const prefix of ['copilot/', 'github.copilot-chat/', 'github/']) {
if (m.startsWith(prefix)) { m = m.slice(prefix.length); break; }
}
m = m.replace(/-thought$/, '').replace(/-preview$/, '').replace(/-latest$/, '');
m = m.replace(/-(thought|thinking)$/, '').replace(/-preview$/, '').replace(/-latest$/, '');
// Claude's API returns hyphenated version numbers (claude-opus-4-6) where
// our rate tables use dots (claude-opus-4.6). Also strip -YYYYMMDD date
// suffixes that older API responses append (claude-haiku-4-5-20251001).
Expand All @@ -128,7 +128,7 @@ export function normalizeModel(modelId: string): string {
}
// Strip effort suffixes ONLY for known reasoning-capable families (avoid
// false positives on models like gpt-5.4-mini or gemini-3-flash).
if (EFFORT_BEARING_RE.test(m) && !NON_EFFORT_SUFFIXES_RE.test(m)) {
if (!keepSuffix && EFFORT_BEARING_RE.test(m) && !NON_EFFORT_SUFFIXES_RE.test(m)) {
m = m.replace(EFFORT_SUFFIX_RE, '');
}
return m.trim();
Expand All @@ -137,7 +137,8 @@ export function normalizeModel(modelId: string): string {
export function modelMultiplier(modelId: string): number {
const key = normalizeModel(modelId);
if (MODEL_MULTIPLIERS[key] !== undefined) return MODEL_MULTIPLIERS[key];
for (const [k, v] of Object.entries(MODEL_MULTIPLIERS)) {
const sortedEntries = Object.entries(MODEL_MULTIPLIERS).sort((a, b) => b[0].length - a[0].length);
for (const [k, v] of sortedEntries) {
if (key.startsWith(k)) return v;
}
return 1;
Expand Down Expand Up @@ -263,7 +264,17 @@ export function tokenCostInCredits(
cacheReadTokens: number = 0,
cacheWriteTokens: number = 0,
): number {
const rates = MODEL_TOKEN_RATES[normalizeModel(model)];
let rates = MODEL_TOKEN_RATES[normalizeModel(model)];
if (!rates) {
const norm = normalizeModel(model);
const sortedRates = Object.entries(MODEL_TOKEN_RATES).sort((a, b) => b[0].length - a[0].length);
for (const [k, v] of sortedRates) {
if (norm.startsWith(k)) {
rates = v;
break;
}
}
}
if (!rates) {
// Fallback: use model multiplier as rough proxy (1 PRU ≈ 1 credit)
return modelMultiplier(model);
Expand Down
Loading