antigravity-claude-proxy 2.7.7 → 2.8.0
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/README.md +15 -5
- package/bin/cli.js +417 -36
- package/package.json +3 -2
- package/public/css/style.css +1 -1
- package/public/views/accounts.html +15 -2
- package/src/account-manager/storage.js +27 -7
- package/src/cloudcode/message-handler.js +9 -0
- package/src/cloudcode/model-api.js +15 -0
- package/src/cloudcode/rate-limit-state.js +12 -0
- package/src/cloudcode/streaming-handler.js +9 -0
- package/src/format/request-converter.js +19 -17
- package/src/format/thinking-utils.js +50 -0
- package/src/server.js +14 -1
- package/src/utils/version-detector.js +1 -1
|
@@ -190,12 +190,18 @@
|
|
|
190
190
|
<div class="w-2 h-2 rounded-full flex-shrink-0"
|
|
191
191
|
:class="acc.status === 'ok' ?
|
|
192
192
|
'bg-neon-green shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse' :
|
|
193
|
-
|
|
193
|
+
(acc.status === 'banned' ?
|
|
194
|
+
'bg-red-800 shadow-[0_0_8px_rgba(153,27,27,0.6)]' :
|
|
195
|
+
'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]')">
|
|
194
196
|
</div>
|
|
195
197
|
<span class="text-xs font-mono font-semibold"
|
|
196
|
-
:class="acc.status === 'ok' ? 'text-neon-green' : 'text-red-400'"
|
|
198
|
+
:class="acc.status === 'ok' ? 'text-neon-green' : (acc.status === 'banned' ? 'text-red-800' : 'text-red-400')"
|
|
197
199
|
x-text="acc.status.toUpperCase()">
|
|
198
200
|
</span>
|
|
201
|
+
<p x-show="acc.status === 'banned'"
|
|
202
|
+
class="text-[9px] text-red-600/70 mt-0.5 max-w-[200px] leading-tight"
|
|
203
|
+
x-text="acc.error || 'Gemini disabled for ToS violation'">
|
|
204
|
+
</p>
|
|
199
205
|
</div>
|
|
200
206
|
</td>
|
|
201
207
|
<td class="py-4 pr-6">
|
|
@@ -207,6 +213,13 @@
|
|
|
207
213
|
x-text="$store.global.t('fix')">
|
|
208
214
|
FIX
|
|
209
215
|
</button>
|
|
216
|
+
<!-- Appeal Button (banned accounts only) -->
|
|
217
|
+
<a x-show="acc.status === 'banned'"
|
|
218
|
+
href="mailto:gemini-code-assist-user-feedback@google.com"
|
|
219
|
+
class="px-3 py-1 text-[10px] font-bold font-mono uppercase tracking-wider rounded bg-red-900/20 text-red-400 hover:bg-red-900/30 border border-red-800/30 hover:border-red-800/50 transition-all"
|
|
220
|
+
title="Email Google to appeal this ban">
|
|
221
|
+
APPEAL
|
|
222
|
+
</a>
|
|
210
223
|
<!-- Settings Button (threshold) -->
|
|
211
224
|
<button class="p-2 rounded hover:bg-white/10 text-gray-500 hover:text-neon-yellow transition-colors"
|
|
212
225
|
@click="openThresholdModal(acc)"
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
* Handles loading and saving account configuration to disk.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
7
|
+
import { readFile, writeFile, mkdir, access, rename } from 'fs/promises';
|
|
8
8
|
import { constants as fsConstants } from 'fs';
|
|
9
9
|
import { dirname } from 'path';
|
|
10
10
|
import { ACCOUNT_CONFIG_PATH } from '../constants.js';
|
|
11
11
|
import { getAuthStatus } from '../auth/database.js';
|
|
12
12
|
import { logger } from '../utils/logger.js';
|
|
13
13
|
|
|
14
|
+
let writeLock = null;
|
|
15
|
+
|
|
14
16
|
/**
|
|
15
17
|
* Load accounts from the config file
|
|
16
18
|
*
|
|
@@ -107,8 +109,18 @@ export function loadDefaultAccount(dbPath) {
|
|
|
107
109
|
* @param {number} activeIndex - Current active account index
|
|
108
110
|
*/
|
|
109
111
|
export async function saveAccounts(configPath, accounts, settings, activeIndex) {
|
|
112
|
+
// Serialize writes to prevent concurrent corruption
|
|
113
|
+
const previousLock = writeLock;
|
|
114
|
+
let resolve;
|
|
115
|
+
writeLock = new Promise(r => { resolve = r; });
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (previousLock) await previousLock;
|
|
119
|
+
} catch {
|
|
120
|
+
// Previous write failed, proceed anyway
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
try {
|
|
111
|
-
// Ensure directory exists
|
|
112
124
|
const dir = dirname(configPath);
|
|
113
125
|
await mkdir(dir, { recursive: true });
|
|
114
126
|
|
|
@@ -116,7 +128,7 @@ export async function saveAccounts(configPath, accounts, settings, activeIndex)
|
|
|
116
128
|
accounts: accounts.map(acc => ({
|
|
117
129
|
email: acc.email,
|
|
118
130
|
source: acc.source,
|
|
119
|
-
enabled: acc.enabled !== false,
|
|
131
|
+
enabled: acc.enabled !== false,
|
|
120
132
|
dbPath: acc.dbPath || null,
|
|
121
133
|
refreshToken: acc.source === 'oauth' ? acc.refreshToken : undefined,
|
|
122
134
|
apiKey: acc.source === 'manual' ? acc.apiKey : undefined,
|
|
@@ -127,19 +139,27 @@ export async function saveAccounts(configPath, accounts, settings, activeIndex)
|
|
|
127
139
|
verifyUrl: acc.verifyUrl || null,
|
|
128
140
|
modelRateLimits: acc.modelRateLimits || {},
|
|
129
141
|
lastUsed: acc.lastUsed,
|
|
130
|
-
// Persist subscription and quota data
|
|
131
142
|
subscription: acc.subscription || { tier: 'unknown', projectId: null, detectedAt: null },
|
|
132
143
|
quota: acc.quota || { models: {}, lastChecked: null },
|
|
133
|
-
|
|
134
|
-
quotaThreshold: acc.quotaThreshold, // undefined omitted from JSON
|
|
144
|
+
quotaThreshold: acc.quotaThreshold,
|
|
135
145
|
modelQuotaThresholds: Object.keys(acc.modelQuotaThresholds || {}).length > 0 ? acc.modelQuotaThresholds : undefined
|
|
136
146
|
})),
|
|
137
147
|
settings: settings,
|
|
138
148
|
activeIndex: activeIndex
|
|
139
149
|
};
|
|
140
150
|
|
|
141
|
-
|
|
151
|
+
const json = JSON.stringify(config, null, 2);
|
|
152
|
+
|
|
153
|
+
// Validate JSON before writing (prevent saving corrupt data)
|
|
154
|
+
JSON.parse(json);
|
|
155
|
+
|
|
156
|
+
// Atomic write: write to temp file then rename
|
|
157
|
+
const tmpPath = configPath + '.tmp';
|
|
158
|
+
await writeFile(tmpPath, json);
|
|
159
|
+
await rename(tmpPath, configPath);
|
|
142
160
|
} catch (error) {
|
|
143
161
|
logger.error('[AccountManager] Failed to save config:', error.message);
|
|
162
|
+
} finally {
|
|
163
|
+
resolve();
|
|
144
164
|
}
|
|
145
165
|
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
isModelCapacityExhausted,
|
|
34
34
|
isValidationRequired,
|
|
35
35
|
extractVerificationUrl,
|
|
36
|
+
isAccountBanned,
|
|
36
37
|
calculateSmartBackoff
|
|
37
38
|
} from './rate-limit-state.js';
|
|
38
39
|
|
|
@@ -295,6 +296,14 @@ export async function sendMessage(anthropicRequest, accountManager, fallbackEnab
|
|
|
295
296
|
throw new AccountForbiddenError(errorText, account.email);
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// 403 with permanent ToS ban — account is permanently disabled by Google
|
|
300
|
+
// Unlike VALIDATION_REQUIRED, this cannot be resolved by verification
|
|
301
|
+
if (response.status === 403 && isAccountBanned(errorText)) {
|
|
302
|
+
logger.warn(`[CloudCode] 403 ToS BANNED for ${account.email}, marking invalid permanently...`);
|
|
303
|
+
accountManager.markInvalid(account.email, 'Account banned — Gemini disabled for Terms of Service violation');
|
|
304
|
+
throw new AccountForbiddenError(errorText, account.email);
|
|
305
|
+
}
|
|
306
|
+
|
|
298
307
|
lastError = new Error(`API error ${response.status}: ${errorText}`);
|
|
299
308
|
// Try next endpoint for 403/404/5xx errors (matches opencode-antigravity-auth behavior)
|
|
300
309
|
if (response.status === 403 || response.status === 404) {
|
|
@@ -96,6 +96,13 @@ export async function fetchAvailableModels(token, projectId = null) {
|
|
|
96
96
|
if (!response.ok) {
|
|
97
97
|
const errorText = await response.text();
|
|
98
98
|
logger.warn(`[CloudCode] fetchAvailableModels error at ${endpoint}: ${response.status}`);
|
|
99
|
+
// Detect permanent ToS ban — no point trying other endpoints
|
|
100
|
+
if (response.status === 403) {
|
|
101
|
+
const lower = (errorText || '').toLowerCase();
|
|
102
|
+
if (lower.includes('has been disabled') && lower.includes('violation of terms of service')) {
|
|
103
|
+
throw new Error(`ACCOUNT_BANNED: ${errorText}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
99
106
|
continue;
|
|
100
107
|
}
|
|
101
108
|
|
|
@@ -189,7 +196,15 @@ export async function getSubscriptionTier(token) {
|
|
|
189
196
|
});
|
|
190
197
|
|
|
191
198
|
if (!response.ok) {
|
|
199
|
+
const errorText = await response.text().catch(() => '');
|
|
192
200
|
logger.warn(`[CloudCode] loadCodeAssist error at ${endpoint}: ${response.status}`);
|
|
201
|
+
// Detect permanent ToS ban — no point trying other endpoints
|
|
202
|
+
if (response.status === 403) {
|
|
203
|
+
const lower = (errorText || '').toLowerCase();
|
|
204
|
+
if (lower.includes('has been disabled') && lower.includes('violation of terms of service')) {
|
|
205
|
+
throw new Error(`ACCOUNT_BANNED: ${errorText}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
193
208
|
continue;
|
|
194
209
|
}
|
|
195
210
|
|
|
@@ -139,6 +139,18 @@ export function extractVerificationUrl(errorText) {
|
|
|
139
139
|
return raw[0].replace(/[,.)}>\]]+$/, '');
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Detect if 403 error is due to a permanent account ban (ToS violation).
|
|
144
|
+
* These accounts are permanently disabled by Google and cannot be recovered
|
|
145
|
+
* by retrying or re-authenticating. User must contact Google support to appeal.
|
|
146
|
+
* @param {string} errorText - Error message from API
|
|
147
|
+
* @returns {boolean} True if account is permanently banned
|
|
148
|
+
*/
|
|
149
|
+
export function isAccountBanned(errorText) {
|
|
150
|
+
const lower = (errorText || '').toLowerCase();
|
|
151
|
+
return lower.includes('has been disabled') && lower.includes('violation of terms of service');
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
/**
|
|
143
155
|
* Detect if 429 error is due to model capacity (not user quota).
|
|
144
156
|
* Capacity issues should retry on same account with shorter delay.
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isModelCapacityExhausted,
|
|
33
33
|
isValidationRequired,
|
|
34
34
|
extractVerificationUrl,
|
|
35
|
+
isAccountBanned,
|
|
35
36
|
calculateSmartBackoff
|
|
36
37
|
} from './rate-limit-state.js';
|
|
37
38
|
import crypto from 'crypto';
|
|
@@ -290,6 +291,14 @@ export async function* sendMessageStream(anthropicRequest, accountManager, fallb
|
|
|
290
291
|
throw new AccountForbiddenError(errorText, account.email);
|
|
291
292
|
}
|
|
292
293
|
|
|
294
|
+
// 403 with permanent ToS ban — account is permanently disabled by Google
|
|
295
|
+
// Unlike VALIDATION_REQUIRED, this cannot be resolved by verification
|
|
296
|
+
if (response.status === 403 && isAccountBanned(errorText)) {
|
|
297
|
+
logger.warn(`[CloudCode] 403 ToS BANNED for ${account.email}, marking invalid permanently...`);
|
|
298
|
+
accountManager.markInvalid(account.email, 'Account banned — Gemini disabled for Terms of Service violation');
|
|
299
|
+
throw new AccountForbiddenError(errorText, account.email);
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
lastError = new Error(`API error ${response.status}: ${errorText}`);
|
|
294
303
|
|
|
295
304
|
// Try next endpoint for 403/404/5xx errors (matches opencode-antigravity-auth behavior)
|
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
hasUnsignedThinkingBlocks,
|
|
20
20
|
needsThinkingRecovery,
|
|
21
21
|
closeToolLoopForThinking,
|
|
22
|
-
cleanCacheControl
|
|
22
|
+
cleanCacheControl,
|
|
23
|
+
clampGeminiThinkingBudget
|
|
23
24
|
} from './thinking-utils.js';
|
|
24
25
|
import { logger } from '../utils/logger.js';
|
|
25
26
|
|
|
@@ -165,31 +166,32 @@ export function convertAnthropicToGoogle(anthropicRequest) {
|
|
|
165
166
|
include_thoughts: true
|
|
166
167
|
};
|
|
167
168
|
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
// Cloud Code API requires thinking_budget to actually produce thinking blocks.
|
|
170
|
+
// Without it, include_thoughts alone is ignored and Claude falls back to
|
|
171
|
+
// <thinking> XML tags in text. Default to 32000 when not provided (e.g. adaptive mode).
|
|
172
|
+
const thinkingBudget = thinking?.budget_tokens || 32000;
|
|
173
|
+
thinkingConfig.thinking_budget = thinkingBudget;
|
|
174
|
+
logger.debug(`[RequestConverter] Claude thinking enabled with budget: ${thinkingBudget}${!thinking?.budget_tokens ? ' (default)' : ''}`);
|
|
175
|
+
|
|
176
|
+
// Validate max_tokens > thinking_budget as required by the API
|
|
177
|
+
const currentMaxTokens = googleRequest.generationConfig.maxOutputTokens;
|
|
178
|
+
if (currentMaxTokens && currentMaxTokens <= thinkingBudget) {
|
|
179
|
+
const adjustedMaxTokens = thinkingBudget + 8192;
|
|
180
|
+
if (thinking?.budget_tokens) {
|
|
180
181
|
logger.warn(`[RequestConverter] max_tokens (${currentMaxTokens}) <= thinking_budget (${thinkingBudget}). Adjusting to ${adjustedMaxTokens} to satisfy API requirements`);
|
|
181
|
-
|
|
182
|
+
} else {
|
|
183
|
+
logger.debug(`[RequestConverter] Adjusting max_tokens to ${adjustedMaxTokens} for default thinking budget`);
|
|
182
184
|
}
|
|
183
|
-
|
|
184
|
-
logger.debug('[RequestConverter] Claude thinking enabled (no budget specified)');
|
|
185
|
+
googleRequest.generationConfig.maxOutputTokens = adjustedMaxTokens;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
googleRequest.generationConfig.thinkingConfig = thinkingConfig;
|
|
188
189
|
} else if (isGeminiModel) {
|
|
189
190
|
// Gemini thinking config (uses camelCase)
|
|
191
|
+
// Clamp budget to model-specific max (e.g., Gemini 2.5 Flash max is 24,576)
|
|
190
192
|
const thinkingConfig = {
|
|
191
193
|
includeThoughts: true,
|
|
192
|
-
thinkingBudget: thinking?.budget_tokens
|
|
194
|
+
thinkingBudget: clampGeminiThinkingBudget(modelName, thinking?.budget_tokens)
|
|
193
195
|
};
|
|
194
196
|
logger.debug(`[RequestConverter] Gemini thinking enabled with budget: ${thinkingConfig.thinkingBudget}`);
|
|
195
197
|
|
|
@@ -7,6 +7,56 @@ import { MIN_SIGNATURE_LENGTH } from '../constants.js';
|
|
|
7
7
|
import { getCachedSignatureFamily } from './signature-cache.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
9
|
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Gemini Thinking Budget Limits (Issue #289)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
// Max thinking budget per Gemini model version
|
|
15
|
+
// Gemini 2.5 Flash: max 24,576 (API error: "supported values are integers from 1 to 24576")
|
|
16
|
+
const GEMINI_THINKING_BUDGET_LIMITS = {
|
|
17
|
+
'2.5': 24576,
|
|
18
|
+
};
|
|
19
|
+
const GEMINI_DEFAULT_THINKING_BUDGET = 16000;
|
|
20
|
+
const GEMINI_DEFAULT_THINKING_BUDGET_LIMIT = 128000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Clamp thinking budget to the maximum supported by a Gemini model.
|
|
24
|
+
* Different Gemini versions have different limits (e.g., 2.5 Flash max is 24,576).
|
|
25
|
+
*
|
|
26
|
+
* @param {string} modelName - The Gemini model name
|
|
27
|
+
* @param {number|undefined} budget - Requested thinking budget from the client
|
|
28
|
+
* @returns {number} Clamped thinking budget
|
|
29
|
+
*/
|
|
30
|
+
export function clampGeminiThinkingBudget(modelName, budget) {
|
|
31
|
+
const requestedBudget = budget || GEMINI_DEFAULT_THINKING_BUDGET;
|
|
32
|
+
const lower = (modelName || '').toLowerCase();
|
|
33
|
+
|
|
34
|
+
// Extract version like "2.5" or "3" from "gemini-2.5-flash-thinking"
|
|
35
|
+
const versionMatch = lower.match(/gemini-(\d+(?:\.\d+)?)/);
|
|
36
|
+
let maxBudget = GEMINI_DEFAULT_THINKING_BUDGET_LIMIT;
|
|
37
|
+
|
|
38
|
+
if (versionMatch) {
|
|
39
|
+
const version = versionMatch[1];
|
|
40
|
+
// Check exact version (e.g., "2.5")
|
|
41
|
+
if (GEMINI_THINKING_BUDGET_LIMITS[version]) {
|
|
42
|
+
maxBudget = GEMINI_THINKING_BUDGET_LIMITS[version];
|
|
43
|
+
} else {
|
|
44
|
+
// Check major version (e.g., "2" for "2.5")
|
|
45
|
+
const major = version.split('.')[0];
|
|
46
|
+
if (GEMINI_THINKING_BUDGET_LIMITS[major]) {
|
|
47
|
+
maxBudget = GEMINI_THINKING_BUDGET_LIMITS[major];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (requestedBudget > maxBudget) {
|
|
53
|
+
logger.debug(`[ThinkingUtils] Clamping Gemini thinking budget from ${requestedBudget} to ${maxBudget} for ${modelName}`);
|
|
54
|
+
return maxBudget;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return requestedBudget;
|
|
58
|
+
}
|
|
59
|
+
|
|
10
60
|
// ============================================================================
|
|
11
61
|
// Cache Control Cleaning (Issue #189)
|
|
12
62
|
// ============================================================================
|
package/src/server.js
CHANGED
|
@@ -259,9 +259,11 @@ app.get('/health', async (req, res) => {
|
|
|
259
259
|
|
|
260
260
|
// Skip invalid accounts for quota check
|
|
261
261
|
if (account.isInvalid) {
|
|
262
|
+
const isBanned = account.invalidReason?.toLowerCase().includes('banned') ||
|
|
263
|
+
account.invalidReason?.toLowerCase().includes('terms of service');
|
|
262
264
|
return {
|
|
263
265
|
...baseInfo,
|
|
264
|
-
status: 'invalid',
|
|
266
|
+
status: isBanned ? 'banned' : 'invalid',
|
|
265
267
|
error: account.invalidReason,
|
|
266
268
|
models: {}
|
|
267
269
|
};
|
|
@@ -394,6 +396,17 @@ app.get('/account-limits', async (req, res) => {
|
|
|
394
396
|
models: quotas
|
|
395
397
|
};
|
|
396
398
|
} catch (error) {
|
|
399
|
+
// Detect ToS ban from quota/subscription fetch and mark account invalid
|
|
400
|
+
if (error.message?.startsWith('ACCOUNT_BANNED:')) {
|
|
401
|
+
accountManager.markInvalid(account.email, 'Account banned — Gemini disabled for Terms of Service violation');
|
|
402
|
+
return {
|
|
403
|
+
email: account.email,
|
|
404
|
+
status: 'banned',
|
|
405
|
+
error: 'Account banned — Gemini disabled for Terms of Service violation',
|
|
406
|
+
subscription: account.subscription || { tier: 'unknown', projectId: null },
|
|
407
|
+
models: {}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
397
410
|
return {
|
|
398
411
|
email: account.email,
|
|
399
412
|
status: 'error',
|