antigravity-claude-proxy 2.9.1 → 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antigravity-claude-proxy",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "Proxy server to use Antigravity's Claude models with Claude Code CLI",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "test:sanitizer": "node tests/test-schema-sanitizer.cjs",
39
39
  "test:strategies": "node tests/test-strategies.cjs",
40
40
  "test:cache-control": "node tests/test-cache-control.cjs",
41
- "test:billing-header": "node tests/test-billing-header.cjs"
41
+ "test:billing-header": "node tests/test-billing-header.cjs",
42
+ "test:stats": "node tests/test-usage-stats.js"
42
43
  },
43
44
  "keywords": [
44
45
  "claude",
@@ -461,8 +461,18 @@ window.DashboardCharts.updateTrendChart = function (component) {
461
461
  });
462
462
  });
463
463
 
464
- // Build datasets for families
464
+ // Build datasets for families (only include families with activity > 0)
465
+ const activeFamilies = new Set();
466
+ Object.entries(dataByFamily).forEach(([family, values]) => {
467
+ if (values.some((v) => v > 0)) {
468
+ activeFamilies.add(family);
469
+ }
470
+ });
471
+ component.activeFamilies = activeFamilies;
472
+
465
473
  component.selectedFamilies.forEach((family) => {
474
+ if (!activeFamilies.has(family)) return;
475
+
466
476
  const color = window.DashboardFilters.getFamilyColor(family);
467
477
  const familyKey =
468
478
  "family" + family.charAt(0).toUpperCase() + family.slice(1);
@@ -501,10 +511,22 @@ window.DashboardCharts.updateTrendChart = function (component) {
501
511
  });
502
512
  });
503
513
 
504
- // Build datasets for models
514
+ // Track models with activity > 0 in this filtered time range
515
+ const activeModelKeys = new Set();
516
+ Object.entries(dataByModel).forEach(([key, values]) => {
517
+ if (values.some((v) => v > 0)) {
518
+ activeModelKeys.add(key);
519
+ }
520
+ });
521
+ component.activeModelKeys = activeModelKeys;
522
+
523
+ // Build datasets for models (only include models with activity > 0 in this time range)
505
524
  component.families.forEach((family) => {
506
525
  (component.selectedModels[family] || []).forEach((model, modelIndex) => {
507
526
  const key = `${family}:${model}`;
527
+ // Skip models with 0 calls in the selected period
528
+ if (!activeModelKeys.has(key)) return;
529
+
508
530
  const color = window.DashboardFilters.getModelColor(family, modelIndex);
509
531
  datasets.push(
510
532
  window.DashboardCharts.createDataset(
@@ -518,6 +540,13 @@ window.DashboardCharts.updateTrendChart = function (component) {
518
540
  });
519
541
  }
520
542
 
543
+ if (datasets.length === 0) {
544
+ if (window.UILogger) window.UILogger.debug("No active datasets for trend chart (all zero in time range)");
545
+ component.hasFilteredTrendData = false;
546
+ _trendChartUpdateLock = false;
547
+ return;
548
+ }
549
+
521
550
  try {
522
551
  const newChart = new Chart(canvas, {
523
552
  type: "line",
@@ -543,6 +572,9 @@ window.DashboardCharts.updateTrendChart = function (component) {
543
572
  borderWidth: 1,
544
573
  padding: 10,
545
574
  displayColors: true,
575
+ filter: function (tooltipItem) {
576
+ return (tooltipItem.parsed?.y || 0) > 0;
577
+ },
546
578
  callbacks: {
547
579
  label: function (context) {
548
580
  return context.dataset.label + ": " + context.parsed.y;
@@ -14,6 +14,8 @@ window.Components.dashboard = () => ({
14
14
  historyData: {},
15
15
  modelTree: {},
16
16
  families: [],
17
+ activeModelKeys: null,
18
+ activeFamilies: null,
17
19
 
18
20
  // Claude config status
19
21
  claudeConfigStatus: {
@@ -451,7 +451,8 @@
451
451
  <!-- Family Mode Legend -->
452
452
  <template x-if="displayMode === 'family'">
453
453
  <template x-for="family in selectedFamilies" :key="family">
454
- <div class="flex items-center gap-1.5 text-[10px] font-mono">
454
+ <div class="flex items-center gap-1.5 text-[10px] font-mono"
455
+ x-show="!activeFamilies || activeFamilies.has(family)">
455
456
  <div class="w-2 h-2 rounded-full" :style="'background-color:' + getFamilyColor(family)"></div>
456
457
  <span class="text-gray-400"
457
458
  x-text="$store.global.t('family' + family.charAt(0).toUpperCase() + family.slice(1))"></span>
@@ -462,7 +463,8 @@
462
463
  <template x-if="displayMode === 'model'">
463
464
  <template x-for="family in families" :key="'legend-' + family">
464
465
  <template x-for="(model, modelIndex) in (selectedModels[family] || [])" :key="family + ':' + model">
465
- <div class="flex items-center gap-1.5 text-[10px] font-mono">
466
+ <div class="flex items-center gap-1.5 text-[10px] font-mono"
467
+ x-show="!activeModelKeys || activeModelKeys.has(family + ':' + model)">
466
468
  <div class="w-2 h-2 rounded-full"
467
469
  :style="'background-color:' + getModelColor(family, modelIndex)"></div>
468
470
  <span class="text-gray-400" x-text="model"></span>
@@ -24,10 +24,23 @@ import { deriveSessionId } from './session-manager.js';
24
24
  // Defaults cover agents commonly used with this proxy; extend or override with
25
25
  // the ANTIGRAVITY_SCRUB_IDENTITY env var, formatted as a comma-separated list of
26
26
  // "Term=>Replacement" pairs, e.g. "Nous Research=>the team,Hermes=>the assistant".
27
+ //
28
+ // Claude Code and Anthropic are listed explicitly: issue #375 identified
29
+ // "You are Claude Code, Anthropic's official CLI for Claude." as a 429 trigger
30
+ // alongside the billing header, but the merged fix (2f6afae) only stripped the
31
+ // header and left both names in place.
32
+ //
33
+ // Matching is case-sensitive on purpose, so lower-case header names
34
+ // (x-anthropic-beta, anthropic-version) and file names (CLAUDE.md) are left
35
+ // alone. The bare word "Claude" is deliberately NOT scrubbed: it is legitimate
36
+ // in model self-descriptions, and requests succeed with it present once the two
37
+ // names above are neutralized.
27
38
  const DEFAULT_IDENTITY_SCRUB = [
28
39
  ['Nous Research', 'the assistant team'],
29
40
  ['Hermes Agent', 'the assistant'],
30
- ['Hermes', 'the assistant']
41
+ ['Hermes', 'the assistant'],
42
+ ['Claude Code', 'the assistant'],
43
+ ['Anthropic', 'the vendor']
31
44
  ];
32
45
 
33
46
  function parseIdentityScrubEnv(raw) {
@@ -42,16 +55,22 @@ function parseIdentityScrubEnv(raw) {
42
55
  .filter((rule) => rule && rule[0].length > 0);
43
56
  }
44
57
 
58
+ function escapeRegExp(text) {
59
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
+ }
61
+
62
+ // Terms match as whole words only, so identifiers that merely contain a term
63
+ // (convertAnthropicToGoogle, AnthropicRequest, HermesHandler) are left alone.
45
64
  const IDENTITY_SCRUB_RULES = [
46
65
  ...DEFAULT_IDENTITY_SCRUB,
47
66
  ...parseIdentityScrubEnv(process.env.ANTIGRAVITY_SCRUB_IDENTITY)
48
- ];
67
+ ].map(([term, replacement]) => [new RegExp(`\\b${escapeRegExp(term)}\\b`, 'g'), replacement]);
49
68
 
50
69
  function scrubClientIdentity(text) {
51
70
  if (typeof text !== 'string') return text;
52
71
  let out = text;
53
- for (const [term, replacement] of IDENTITY_SCRUB_RULES) {
54
- out = out.split(term).join(replacement);
72
+ for (const [pattern, replacement] of IDENTITY_SCRUB_RULES) {
73
+ out = out.replace(pattern, () => replacement);
55
74
  }
56
75
  return out;
57
76
  }
package/src/constants.js CHANGED
@@ -290,6 +290,17 @@ export const MODEL_FALLBACK_MAP = {
290
290
  'claude-sonnet-4-6': 'gemini-3-flash'
291
291
  };
292
292
 
293
+ // Built-in model aliases mapped to canonical backend models
294
+ export const BUILTIN_MODEL_MAPPINGS = {
295
+ 'claude-3-5-sonnet-20241022': 'claude-sonnet-4-6',
296
+ 'claude-3-7-sonnet-20250219': 'claude-sonnet-4-6',
297
+ 'claude-opus-5-5': 'claude-opus-4-6-thinking',
298
+ 'claude-sonnet-5-5': 'claude-sonnet-4-6',
299
+ 'claude-haiku-5-5': 'claude-sonnet-4-6',
300
+ 'claude-opus-5': 'claude-opus-4-6-thinking',
301
+ 'claude-sonnet-5': 'claude-sonnet-4-6'
302
+ };
303
+
293
304
  // Default test models for each family (used by test suite)
294
305
  export const TEST_MODELS = {
295
306
  claude: 'claude-sonnet-4-6',
@@ -522,6 +533,7 @@ export default {
522
533
  OAUTH_REDIRECT_URI,
523
534
  STRATEGY_LABELS,
524
535
  MODEL_FALLBACK_MAP,
536
+ BUILTIN_MODEL_MAPPINGS,
525
537
  TEST_MODELS,
526
538
  DEFAULT_PRESETS,
527
539
  DEFAULT_SERVER_PRESETS,
@@ -10,11 +10,47 @@ const DATA_DIR = path.dirname(HISTORY_FILE);
10
10
  const OLD_DATA_DIR = path.join(process.cwd(), 'data');
11
11
  const OLD_HISTORY_FILE = path.join(OLD_DATA_DIR, 'usage-history.json');
12
12
 
13
+ // Legacy model aliases that should be migrated to canonical backend model names in history
14
+ export const LEGACY_MODEL_MIGRATIONS = {
15
+ 'opus-5-5': 'opus-4-6-thinking',
16
+ 'sonnet-5-5': 'sonnet-4-6',
17
+ 'haiku-5-5': 'sonnet-4-6'
18
+ };
19
+
13
20
  // In-memory storage
14
21
  // Structure: { "YYYY-MM-DDTHH:00:00.000Z": { "claude": { "model-name": count, "_subtotal": count }, "_total": count } }
15
22
  let history = {};
16
23
  let isDirty = false;
17
24
 
25
+ /**
26
+ * Migrate legacy/phantom model names in history to canonical backend model names
27
+ * @param {object} data - History data
28
+ * @returns {boolean} Whether any migration took place
29
+ */
30
+ export function normalizeHistory(data) {
31
+ if (!data || typeof data !== 'object') return false;
32
+ let modified = false;
33
+
34
+ for (const [timestamp, hourData] of Object.entries(data)) {
35
+ if (!hourData || typeof hourData !== 'object') continue;
36
+
37
+ for (const [family, familyData] of Object.entries(hourData)) {
38
+ if (!familyData || typeof familyData !== 'object' || family.startsWith('_')) continue;
39
+
40
+ for (const [legacyName, targetName] of Object.entries(LEGACY_MODEL_MIGRATIONS)) {
41
+ if (familyData[legacyName] !== undefined) {
42
+ const count = familyData[legacyName];
43
+ delete familyData[legacyName];
44
+ familyData[targetName] = (familyData[targetName] || 0) + count;
45
+ modified = true;
46
+ logger.info(`[UsageStats] Migrated legacy model ${legacyName} -> ${targetName} (${count} calls at ${timestamp})`);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return modified;
52
+ }
53
+
18
54
  /**
19
55
  * Extract model family from model ID
20
56
  * @param {string} modelId - The model identifier (e.g., "claude-opus-4-6-thinking")
@@ -62,6 +98,10 @@ function load() {
62
98
  if (fs.existsSync(HISTORY_FILE)) {
63
99
  const data = fs.readFileSync(HISTORY_FILE, 'utf8');
64
100
  history = JSON.parse(data);
101
+ if (normalizeHistory(history)) {
102
+ isDirty = true;
103
+ save();
104
+ }
65
105
  }
66
106
  } catch (err) {
67
107
  logger.error('[UsageStats] Failed to load history:', err);
@@ -143,28 +183,17 @@ function setupMiddleware(app) {
143
183
  load();
144
184
 
145
185
  // Auto-save every minute
146
- setInterval(() => {
186
+ const saveInterval = setInterval(() => {
147
187
  save();
148
188
  prune();
149
189
  }, 60 * 1000);
190
+ if (saveInterval.unref) {
191
+ saveInterval.unref();
192
+ }
150
193
 
151
194
  // Save on exit
152
195
  process.on('SIGINT', () => { save(); process.exit(); });
153
196
  process.on('SIGTERM', () => { save(); process.exit(); });
154
-
155
- // Request interceptor
156
- // Track both Anthropic (/v1/messages) and OpenAI compatible (/v1/chat/completions) endpoints
157
- const TRACKED_PATHS = ['/v1/messages', '/v1/chat/completions'];
158
-
159
- app.use((req, res, next) => {
160
- if (req.method === 'POST' && TRACKED_PATHS.includes(req.path)) {
161
- const model = req.body?.model;
162
- if (model) {
163
- track(model);
164
- }
165
- }
166
- next();
167
- });
168
197
  }
169
198
 
170
199
  /**
@@ -202,5 +231,7 @@ export default {
202
231
  track,
203
232
  getFamily,
204
233
  getShortName,
205
- getHistory
234
+ getHistory,
235
+ normalizeHistory,
236
+ LEGACY_MODEL_MIGRATIONS
206
237
  };
package/src/server.js CHANGED
@@ -15,7 +15,7 @@ import { config } from './config.js';
15
15
  const __filename = fileURLToPath(import.meta.url);
16
16
  const __dirname = path.dirname(__filename);
17
17
  import { forceRefresh } from './auth/token-extractor.js';
18
- import { REQUEST_BODY_LIMIT } from './constants.js';
18
+ import { REQUEST_BODY_LIMIT, BUILTIN_MODEL_MAPPINGS } from './constants.js';
19
19
  import { AccountManager } from './account-manager/index.js';
20
20
  import { clearThinkingSignatureCache } from './format/signature-cache.js';
21
21
  import { formatDuration } from './utils/helpers.js';
@@ -730,6 +730,10 @@ app.post('/v1/messages', async (req, res) => {
730
730
  const targetModel = modelMapping[requestedModel].mapping;
731
731
  logger.info(`[Server] Mapping model ${requestedModel} -> ${targetModel}`);
732
732
  requestedModel = targetModel;
733
+ } else if (BUILTIN_MODEL_MAPPINGS[requestedModel]) {
734
+ const targetModel = BUILTIN_MODEL_MAPPINGS[requestedModel];
735
+ logger.info(`[Server] Built-in mapping model ${requestedModel} -> ${targetModel}`);
736
+ requestedModel = targetModel;
733
737
  }
734
738
 
735
739
  const modelId = requestedModel;
@@ -812,6 +816,7 @@ app.post('/v1/messages', async (req, res) => {
812
816
  const firstResult = await generator.next();
813
817
 
814
818
  // If we get here, the stream started successfully.
819
+ usageStats.track(modelId);
815
820
  res.status(200);
816
821
  res.setHeader('Content-Type', 'text/event-stream');
817
822
  res.setHeader('Cache-Control', 'no-cache');
@@ -863,6 +868,7 @@ app.post('/v1/messages', async (req, res) => {
863
868
  } else {
864
869
  // Handle non-streaming response
865
870
  const response = await sendMessage(request, accountManager, FALLBACK_ENABLED);
871
+ usageStats.track(response?.model || modelId);
866
872
  res.json(response);
867
873
  }
868
874