antigravity-claude-proxy 2.9.0 → 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 +3 -2
- package/public/js/components/dashboard/charts.js +34 -2
- package/public/js/components/dashboard.js +2 -0
- package/public/views/dashboard.html +4 -2
- package/src/cloudcode/request-builder.js +23 -4
- package/src/constants.js +14 -2
- package/src/format/request-converter.js +14 -3
- package/src/format/thinking-utils.js +6 -1
- package/src/modules/usage-stats.js +47 -16
- package/src/server.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antigravity-claude-proxy",
|
|
3
|
-
"version": "2.9.
|
|
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
|
-
//
|
|
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;
|
|
@@ -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 [
|
|
54
|
-
out = out.
|
|
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
|
@@ -202,8 +202,8 @@ export const STRATEGY_LABELS = {
|
|
|
202
202
|
'hybrid': 'Hybrid (Smart Distribution)'
|
|
203
203
|
};
|
|
204
204
|
|
|
205
|
-
// Gemini-specific limits
|
|
206
|
-
export const GEMINI_MAX_OUTPUT_TOKENS =
|
|
205
|
+
// Gemini-specific limits (Gemini 3.8 / Gemini 4 Argon support up to 65536 output tokens)
|
|
206
|
+
export const GEMINI_MAX_OUTPUT_TOKENS = 65536;
|
|
207
207
|
|
|
208
208
|
// Gemini signature handling
|
|
209
209
|
// Sentinel value to skip thought signature validation when Claude Code strips the field
|
|
@@ -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,
|
|
@@ -202,13 +202,24 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
|
|
202
202
|
googleRequest.generationConfig.thinkingConfig = thinkingConfig;
|
|
203
203
|
} else if (isGeminiModel) {
|
|
204
204
|
// Gemini thinking config (uses camelCase)
|
|
205
|
-
//
|
|
205
|
+
// For callers that did not explicitly request thinking and asked for small max_tokens,
|
|
206
|
+
// use minimal thinking budget (32) and reserve output headroom so thinking doesn't
|
|
207
|
+
// consume the entire maxOutputTokens budget and return empty text.
|
|
208
|
+
const requestedBudget = thinking?.budget_tokens
|
|
209
|
+
? clampGeminiThinkingBudget(modelName, thinking.budget_tokens)
|
|
210
|
+
: (max_tokens && max_tokens <= 1024 ? 32 : clampGeminiThinkingBudget(modelName, undefined));
|
|
206
211
|
const thinkingConfig = {
|
|
207
|
-
includeThoughts:
|
|
208
|
-
thinkingBudget:
|
|
212
|
+
includeThoughts: Boolean(thinking?.type === 'enabled' || thinking?.budget_tokens || !max_tokens || max_tokens > 256),
|
|
213
|
+
thinkingBudget: requestedBudget
|
|
209
214
|
};
|
|
210
215
|
logger.debug(`[RequestConverter] Gemini thinking enabled with budget: ${thinkingConfig.thinkingBudget}`);
|
|
211
216
|
|
|
217
|
+
const currentMaxTokens = googleRequest.generationConfig.maxOutputTokens;
|
|
218
|
+
if (currentMaxTokens && currentMaxTokens <= thinkingConfig.thinkingBudget) {
|
|
219
|
+
const adjustedMaxTokens = Math.min(thinkingConfig.thinkingBudget + Math.max(currentMaxTokens, 8192), GEMINI_MAX_OUTPUT_TOKENS);
|
|
220
|
+
logger.debug(`[RequestConverter] Adjusting Gemini maxOutputTokens from ${currentMaxTokens} to ${adjustedMaxTokens} to exceed thinkingBudget (${thinkingConfig.thinkingBudget})`);
|
|
221
|
+
googleRequest.generationConfig.maxOutputTokens = adjustedMaxTokens;
|
|
222
|
+
}
|
|
212
223
|
|
|
213
224
|
googleRequest.generationConfig.thinkingConfig = thinkingConfig;
|
|
214
225
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { MIN_SIGNATURE_LENGTH } from '../constants.js';
|
|
7
|
-
import { getCachedSignatureFamily } from './signature-cache.js';
|
|
7
|
+
import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
@@ -447,6 +447,11 @@ function messageHasValidThinking(message) {
|
|
|
447
447
|
const content = message.content || message.parts || [];
|
|
448
448
|
if (!Array.isArray(content)) return false;
|
|
449
449
|
return content.some(block => {
|
|
450
|
+
if (block && (block.type === 'tool_use' || block.functionCall)) {
|
|
451
|
+
if (block.thoughtSignature && block.thoughtSignature.length >= MIN_SIGNATURE_LENGTH) return true;
|
|
452
|
+
if (block.id && getCachedSignature(block.id)) return true;
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
450
455
|
if (!isThinkingPart(block)) return false;
|
|
451
456
|
// Check for valid signature (Anthropic style)
|
|
452
457
|
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) return true;
|
|
@@ -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
|
|