antigravity-claude-proxy 2.4.0 → 2.4.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/README.md +70 -6
- package/package.json +3 -2
- package/public/index.html +2 -1
- package/public/js/components/dashboard/charts.js +17 -16
- package/public/js/components/dashboard/filters.js +2 -2
- package/public/js/components/logs-viewer.js +2 -2
- package/public/js/data-store.js +7 -4
- package/public/js/utils/ui-logger.js +143 -0
- package/src/auth/oauth.js +86 -12
- package/src/constants.js +9 -3
- package/src/format/request-converter.js +9 -2
- package/src/format/thinking-utils.js +92 -2
package/README.md
CHANGED
|
@@ -84,6 +84,8 @@ Choose one of the following methods to authorize the proxy:
|
|
|
84
84
|
2. Navigate to the **Accounts** tab and click **Add Account**.
|
|
85
85
|
3. Complete the Google OAuth authorization in the popup window.
|
|
86
86
|
|
|
87
|
+
> **Headless/Remote Servers**: If running on a server without a browser, the WebUI supports a "Manual Authorization" mode. After clicking "Add Account", you can copy the OAuth URL, complete authorization on your local machine, and paste the authorization code back.
|
|
88
|
+
|
|
87
89
|
#### **Method B: CLI (Desktop or Headless)**
|
|
88
90
|
|
|
89
91
|
If you prefer the terminal or are on a remote server:
|
|
@@ -152,15 +154,13 @@ Add this configuration:
|
|
|
152
154
|
"ANTHROPIC_MODEL": "claude-opus-4-5-thinking",
|
|
153
155
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-5-thinking",
|
|
154
156
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5-thinking",
|
|
155
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "
|
|
157
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-sonnet-4-5",
|
|
156
158
|
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-5-thinking",
|
|
157
159
|
"ENABLE_EXPERIMENTAL_MCP_CLI": "true"
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
```
|
|
161
163
|
|
|
162
|
-
(Please use **gemini-2.5-flash-lite** as the default haiku model, even if others are claude, as claude code makes several calls via the haiku model for background tasks. If you use claude model for it, you may use you claude usage sooner)
|
|
163
|
-
|
|
164
164
|
Or to use Gemini models:
|
|
165
165
|
|
|
166
166
|
```json
|
|
@@ -171,7 +171,7 @@ Or to use Gemini models:
|
|
|
171
171
|
"ANTHROPIC_MODEL": "gemini-3-pro-high[1m]",
|
|
172
172
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gemini-3-pro-high[1m]",
|
|
173
173
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gemini-3-flash[1m]",
|
|
174
|
-
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-
|
|
174
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gemini-3-flash[1m]",
|
|
175
175
|
"CLAUDE_CODE_SUBAGENT_MODEL": "gemini-3-flash[1m]",
|
|
176
176
|
"ENABLE_EXPERIMENTAL_MCP_CLI": "true"
|
|
177
177
|
}
|
|
@@ -280,7 +280,7 @@ Choose a strategy based on your needs:
|
|
|
280
280
|
|
|
281
281
|
| Strategy | Best For | Description |
|
|
282
282
|
| --- | --- | --- |
|
|
283
|
-
| **Hybrid** (Default) | Most users | Smart selection combining health score, token bucket rate limiting, and LRU freshness |
|
|
283
|
+
| **Hybrid** (Default) | Most users | Smart selection combining health score, token bucket rate limiting, quota awareness, and LRU freshness |
|
|
284
284
|
| **Sticky** | Prompt caching | Stays on the same account to maximize cache hits, switches only when rate-limited |
|
|
285
285
|
| **Round-Robin** | Even distribution | Cycles through accounts sequentially for balanced load |
|
|
286
286
|
|
|
@@ -298,6 +298,8 @@ antigravity-claude-proxy start --strategy=round-robin # Load-balanced
|
|
|
298
298
|
|
|
299
299
|
- **Health Score Tracking**: Accounts earn points for successful requests and lose points for failures/rate-limits
|
|
300
300
|
- **Token Bucket Rate Limiting**: Client-side throttling with regenerating tokens (50 max, 6/minute)
|
|
301
|
+
- **Quota Awareness**: Accounts with critical quota (<5%) are deprioritized; exhausted accounts trigger emergency fallback
|
|
302
|
+
- **Emergency Fallback**: When all accounts appear exhausted, bypasses checks with throttle delays (250-500ms)
|
|
301
303
|
- **Automatic Cooldown**: Rate-limited accounts recover automatically after reset time expires
|
|
302
304
|
- **Invalid Account Detection**: Accounts needing re-authentication are marked and skipped
|
|
303
305
|
- **Prompt Caching Support**: Session IDs derived from conversation enable cache hits across turns
|
|
@@ -340,13 +342,14 @@ The proxy includes a built-in, modern web interface for real-time monitoring and
|
|
|
340
342
|
- **Real-time Dashboard**: Monitor request volume, active accounts, model health, and subscription tier distribution.
|
|
341
343
|
- **Visual Model Quota**: Track per-model usage and next reset times with color-coded progress indicators.
|
|
342
344
|
- **Account Management**: Add/remove Google accounts via OAuth, view subscription tiers (Free/Pro/Ultra) and quota status at a glance.
|
|
345
|
+
- **Manual OAuth Mode**: Add accounts on headless servers by copying the OAuth URL and pasting the authorization code.
|
|
343
346
|
- **Claude CLI Configuration**: Edit your `~/.claude/settings.json` directly from the browser.
|
|
344
347
|
- **Persistent History**: Tracks request volume by model family for 30 days, persisting across server restarts.
|
|
345
348
|
- **Time Range Filtering**: Analyze usage trends over 1H, 6H, 24H, 7D, or All Time periods.
|
|
346
349
|
- **Smart Analysis**: Auto-select top 5 most used models or toggle between Family/Model views.
|
|
347
350
|
- **Live Logs**: Stream server logs with level-based filtering and search.
|
|
348
351
|
- **Advanced Tuning**: Configure retries, timeouts, and debug mode on the fly.
|
|
349
|
-
- **
|
|
352
|
+
- **Multi-language Interface**: Full support for English, Chinese (中文), Indonesian (Bahasa), and Portuguese (PT-BR).
|
|
350
353
|
|
|
351
354
|
---
|
|
352
355
|
|
|
@@ -360,9 +363,11 @@ While most users can use the default settings, you can tune the proxy behavior v
|
|
|
360
363
|
- **WebUI Password**: Secure your dashboard with `WEBUI_PASSWORD` env var or in config.
|
|
361
364
|
- **Custom Port**: Change the default `8080` port.
|
|
362
365
|
- **Retry Logic**: Configure `maxRetries`, `retryBaseMs`, and `retryMaxMs`.
|
|
366
|
+
- **Rate Limit Handling**: Comprehensive rate limit detection from headers and error messages with intelligent retry-after parsing.
|
|
363
367
|
- **Load Balancing**: Adjust `defaultCooldownMs` and `maxWaitBeforeErrorMs`.
|
|
364
368
|
- **Persistence**: Enable `persistTokenCache` to save OAuth sessions across restarts.
|
|
365
369
|
- **Max Accounts**: Set `maxAccounts` (1-100) to limit the number of Google accounts. Default: 10.
|
|
370
|
+
- **Endpoint Fallback**: Automatic 403/404 endpoint fallback for API compatibility.
|
|
366
371
|
|
|
367
372
|
Refer to `config.example.json` for a complete list of fields and documentation.
|
|
368
373
|
|
|
@@ -421,12 +426,71 @@ npm run test:interleaved # Interleaved thinking
|
|
|
421
426
|
npm run test:images # Image processing
|
|
422
427
|
npm run test:caching # Prompt caching
|
|
423
428
|
npm run test:strategies # Account selection strategies
|
|
429
|
+
npm run test:cache-control # Cache control field stripping
|
|
424
430
|
```
|
|
425
431
|
|
|
426
432
|
---
|
|
427
433
|
|
|
428
434
|
## Troubleshooting
|
|
429
435
|
|
|
436
|
+
### Windows: OAuth Port Error (EACCES)
|
|
437
|
+
|
|
438
|
+
On Windows, the default OAuth callback port (51121) may be reserved by Hyper-V, WSL2, or Docker. If you see:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
Error: listen EACCES: permission denied 0.0.0.0:51121
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The proxy will automatically try fallback ports (51122-51126). If all ports fail, try these solutions:
|
|
445
|
+
|
|
446
|
+
#### Option 1: Use a Custom Port (Recommended)
|
|
447
|
+
|
|
448
|
+
Set a custom port outside the reserved range:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
# Windows PowerShell
|
|
452
|
+
$env:OAUTH_CALLBACK_PORT = "3456"
|
|
453
|
+
antigravity-claude-proxy start
|
|
454
|
+
|
|
455
|
+
# Windows CMD
|
|
456
|
+
set OAUTH_CALLBACK_PORT=3456
|
|
457
|
+
antigravity-claude-proxy start
|
|
458
|
+
|
|
459
|
+
# Or add to your .env file
|
|
460
|
+
OAUTH_CALLBACK_PORT=3456
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Option 2: Reset Windows NAT
|
|
464
|
+
|
|
465
|
+
Run as Administrator:
|
|
466
|
+
|
|
467
|
+
```powershell
|
|
468
|
+
net stop winnat
|
|
469
|
+
net start winnat
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### Option 3: Check Reserved Ports
|
|
473
|
+
|
|
474
|
+
See which ports are reserved:
|
|
475
|
+
|
|
476
|
+
```powershell
|
|
477
|
+
netsh interface ipv4 show excludedportrange protocol=tcp
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
If 51121 is in a reserved range, use Option 1 with a port outside those ranges.
|
|
481
|
+
|
|
482
|
+
#### Option 4: Permanently Exclude Port (Admin)
|
|
483
|
+
|
|
484
|
+
Reserve the port before Hyper-V claims it (run as Administrator):
|
|
485
|
+
|
|
486
|
+
```powershell
|
|
487
|
+
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
> **Note:** The server automatically tries fallback ports (51122-51126) if the primary port fails.
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
430
494
|
### "Could not extract token from Antigravity"
|
|
431
495
|
|
|
432
496
|
If using single-account mode with Antigravity:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antigravity-claude-proxy",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.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",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"test:oauth": "node tests/test-oauth-no-browser.cjs",
|
|
36
36
|
"test:emptyretry": "node tests/test-empty-response-retry.cjs",
|
|
37
37
|
"test:sanitizer": "node tests/test-schema-sanitizer.cjs",
|
|
38
|
-
"test:strategies": "node tests/test-strategies.cjs"
|
|
38
|
+
"test:strategies": "node tests/test-strategies.cjs",
|
|
39
|
+
"test:cache-control": "node tests/test-cache-control.cjs"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [
|
|
41
42
|
"claude",
|
package/public/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<body
|
|
20
20
|
class="bg-space-950 text-gray-300 font-sans antialiased min-h-screen overflow-hidden selection:bg-neon-purple selection:text-white"
|
|
21
|
-
x-cloak x-data="app" x-init="
|
|
21
|
+
x-cloak x-data="app" x-init="if(window.UILogger) window.UILogger.debug('App initialized')">
|
|
22
22
|
|
|
23
23
|
<!-- Toast Notification -->
|
|
24
24
|
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-2 pointer-events-none">
|
|
@@ -389,6 +389,7 @@
|
|
|
389
389
|
<!-- Scripts - Loading Order Matters! -->
|
|
390
390
|
<!-- 1. Config & Utils (global helpers) -->
|
|
391
391
|
<script src="js/config/constants.js"></script>
|
|
392
|
+
<script src="js/utils/ui-logger.js"></script><!-- Issue #183: Conditional logging utility -->
|
|
392
393
|
<script src="js/utils.js"></script>
|
|
393
394
|
<script src="js/utils/error-handler.js"></script>
|
|
394
395
|
<script src="js/utils/account-actions.js"></script>
|
|
@@ -106,7 +106,7 @@ window.DashboardCharts.createDataset = function (label, data, color, canvas) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
} catch (e) {
|
|
109
|
-
|
|
109
|
+
if (window.UILogger) window.UILogger.debug("Gradient fallback:", e.message);
|
|
110
110
|
gradient = null;
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -149,7 +149,7 @@ window.DashboardCharts.updateCharts = function (component) {
|
|
|
149
149
|
console.debug("Destroying existing quota chart from canvas property");
|
|
150
150
|
try {
|
|
151
151
|
canvas._chartInstance.destroy();
|
|
152
|
-
} catch(e) {
|
|
152
|
+
} catch(e) { if (window.UILogger) window.UILogger.debug(e); }
|
|
153
153
|
canvas._chartInstance = null;
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -170,11 +170,11 @@ window.DashboardCharts.updateCharts = function (component) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (typeof Chart === "undefined") {
|
|
173
|
-
|
|
173
|
+
if (window.UILogger) window.UILogger.warn("Chart.js not loaded");
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
if (!isCanvasReady(canvas)) {
|
|
177
|
-
|
|
177
|
+
if (window.UILogger) window.UILogger.debug("quotaChart canvas not ready, skipping update");
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -319,12 +319,13 @@ window.DashboardCharts.updateCharts = function (component) {
|
|
|
319
319
|
window.DashboardCharts.updateTrendChart = function (component) {
|
|
320
320
|
// Prevent concurrent updates (fixes race condition on rapid toggling)
|
|
321
321
|
if (_trendChartUpdateLock) {
|
|
322
|
-
|
|
322
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Update already in progress, skipping");
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
_trendChartUpdateLock = true;
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
const logger = window.UILogger || console;
|
|
328
|
+
logger.debug("[updateTrendChart] Starting update...");
|
|
328
329
|
|
|
329
330
|
const canvas = document.getElementById("usageTrendChart");
|
|
330
331
|
|
|
@@ -335,7 +336,7 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
|
|
335
336
|
try {
|
|
336
337
|
canvas._chartInstance.stop();
|
|
337
338
|
canvas._chartInstance.destroy();
|
|
338
|
-
} catch(e) {
|
|
339
|
+
} catch(e) { if (window.UILogger) window.UILogger.debug(e); }
|
|
339
340
|
canvas._chartInstance = null;
|
|
340
341
|
}
|
|
341
342
|
|
|
@@ -359,17 +360,17 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
|
|
359
360
|
|
|
360
361
|
// Safety checks
|
|
361
362
|
if (!canvas) {
|
|
362
|
-
|
|
363
|
-
_trendChartUpdateLock = false;
|
|
363
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas not found in DOM");
|
|
364
|
+
_trendChartUpdateLock = false;
|
|
364
365
|
return;
|
|
365
366
|
}
|
|
366
367
|
if (typeof Chart === "undefined") {
|
|
367
|
-
|
|
368
|
-
_trendChartUpdateLock = false;
|
|
368
|
+
if (window.UILogger) window.UILogger.warn("[updateTrendChart] Chart.js not loaded");
|
|
369
|
+
_trendChartUpdateLock = false;
|
|
369
370
|
return;
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
|
|
373
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas element:", {
|
|
373
374
|
exists: !!canvas,
|
|
374
375
|
isConnected: canvas.isConnected,
|
|
375
376
|
width: canvas.offsetWidth,
|
|
@@ -378,7 +379,7 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
|
|
378
379
|
});
|
|
379
380
|
|
|
380
381
|
if (!isCanvasReady(canvas)) {
|
|
381
|
-
|
|
382
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Canvas not ready", {
|
|
382
383
|
isConnected: canvas.isConnected,
|
|
383
384
|
width: canvas.offsetWidth,
|
|
384
385
|
height: canvas.offsetHeight,
|
|
@@ -394,17 +395,17 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
|
|
394
395
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
395
396
|
}
|
|
396
397
|
} catch (e) {
|
|
397
|
-
|
|
398
|
+
if (window.UILogger) window.UILogger.debug("[updateTrendChart] Failed to clear canvas:", e.message);
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
|
|
401
|
+
if (window.UILogger) window.UILogger.debug(
|
|
401
402
|
"[updateTrendChart] Canvas is ready, proceeding with chart creation"
|
|
402
403
|
);
|
|
403
404
|
|
|
404
405
|
// Use filtered history data based on time range
|
|
405
406
|
const history = window.DashboardFilters.getFilteredHistoryData(component);
|
|
406
407
|
if (!history || Object.keys(history).length === 0) {
|
|
407
|
-
|
|
408
|
+
if (window.UILogger) window.UILogger.debug("No history data available for trend chart (after filtering)");
|
|
408
409
|
component.hasFilteredTrendData = false;
|
|
409
410
|
_trendChartUpdateLock = false;
|
|
410
411
|
return;
|
|
@@ -50,7 +50,7 @@ window.DashboardFilters.loadPreferences = function(component) {
|
|
|
50
50
|
component.selectedModels = prefs.selectedModels || {};
|
|
51
51
|
}
|
|
52
52
|
} catch (e) {
|
|
53
|
-
|
|
53
|
+
if (window.UILogger) window.UILogger.debug('Failed to load dashboard preferences:', e.message);
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
|
|
@@ -67,7 +67,7 @@ window.DashboardFilters.savePreferences = function(component) {
|
|
|
67
67
|
selectedModels: component.selectedModels
|
|
68
68
|
}));
|
|
69
69
|
} catch (e) {
|
|
70
|
-
|
|
70
|
+
if (window.UILogger) window.UILogger.debug('Failed to save dashboard preferences:', e.message);
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
|
|
@@ -79,12 +79,12 @@ window.Components.logsViewer = () => ({
|
|
|
79
79
|
this.$nextTick(() => this.scrollToBottom());
|
|
80
80
|
}
|
|
81
81
|
} catch (e) {
|
|
82
|
-
|
|
82
|
+
if (window.UILogger) window.UILogger.debug('Log parse error:', e.message);
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
this.eventSource.onerror = () => {
|
|
87
|
-
|
|
87
|
+
if (window.UILogger) window.UILogger.debug('Log stream disconnected, reconnecting...');
|
|
88
88
|
setTimeout(() => this.startLogStream(), 3000);
|
|
89
89
|
};
|
|
90
90
|
},
|
package/public/js/data-store.js
CHANGED
|
@@ -55,7 +55,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
55
55
|
|
|
56
56
|
// Check TTL
|
|
57
57
|
if (data.timestamp && (Date.now() - data.timestamp > CACHE_TTL)) {
|
|
58
|
-
|
|
58
|
+
if (window.UILogger) window.UILogger.debug('Cache expired, skipping restoration');
|
|
59
59
|
localStorage.removeItem('ag_data_cache');
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
@@ -70,11 +70,11 @@ document.addEventListener('alpine:init', () => {
|
|
|
70
70
|
// Don't show loading on initial load if we have cache
|
|
71
71
|
this.initialLoad = false;
|
|
72
72
|
this.computeQuotaRows();
|
|
73
|
-
|
|
73
|
+
if (window.UILogger) window.UILogger.debug('Restored data from cache');
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
} catch (e) {
|
|
77
|
-
|
|
77
|
+
if (window.UILogger) window.UILogger.debug('Failed to load cache', e.message);
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
|
|
@@ -89,7 +89,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
89
89
|
};
|
|
90
90
|
localStorage.setItem('ag_data_cache', JSON.stringify(cacheData));
|
|
91
91
|
} catch (e) {
|
|
92
|
-
|
|
92
|
+
if (window.UILogger) window.UILogger.debug('Failed to save cache', e.message);
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
95
|
|
|
@@ -127,6 +127,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
127
127
|
|
|
128
128
|
this.lastUpdated = new Date().toLocaleTimeString();
|
|
129
129
|
} catch (error) {
|
|
130
|
+
// Keep error logging for actual fetch failures
|
|
130
131
|
console.error('Fetch error:', error);
|
|
131
132
|
const store = Alpine.store('global');
|
|
132
133
|
store.showToast(store.t('connectionLost'), 'error');
|
|
@@ -237,6 +238,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
237
238
|
let minResetTime = null;
|
|
238
239
|
|
|
239
240
|
this.accounts.forEach(acc => {
|
|
241
|
+
if (acc.enabled === false) return;
|
|
240
242
|
if (this.filters.account !== 'all' && acc.email !== this.filters.account) return;
|
|
241
243
|
|
|
242
244
|
const limit = acc.limits?.[modelId];
|
|
@@ -352,6 +354,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
352
354
|
const quotaInfo = [];
|
|
353
355
|
// Use ALL accounts (no account filter)
|
|
354
356
|
this.accounts.forEach(acc => {
|
|
357
|
+
if (acc.enabled === false) return;
|
|
355
358
|
const limit = acc.limits?.[modelId];
|
|
356
359
|
if (!limit) return;
|
|
357
360
|
const pct = limit.remainingFraction !== null ? Math.round(limit.remainingFraction * 100) : 0;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Logger Utility
|
|
3
|
+
* Provides conditional logging for the web UI to reduce console spam in production.
|
|
4
|
+
* Wraps console methods and only outputs when debug mode is enabled.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* window.UILogger.debug('message') - Only logs if debug mode enabled
|
|
8
|
+
* window.UILogger.info('message') - Only logs if debug mode enabled
|
|
9
|
+
* window.UILogger.warn('message') - Always logs (important warnings)
|
|
10
|
+
* window.UILogger.error('message') - Always logs (errors should always be visible)
|
|
11
|
+
*
|
|
12
|
+
* Enable debug mode:
|
|
13
|
+
* - Set localStorage.setItem('ag_debug', 'true') in browser console
|
|
14
|
+
* - Or pass ?debug=true in URL
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
(function() {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// Check if debug mode is enabled
|
|
21
|
+
function isDebugEnabled() {
|
|
22
|
+
// Check URL parameter
|
|
23
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
24
|
+
if (urlParams.get('debug') === 'true') {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check localStorage
|
|
29
|
+
try {
|
|
30
|
+
return localStorage.getItem('ag_debug') === 'true';
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cache debug state (can be refreshed)
|
|
37
|
+
let debugEnabled = isDebugEnabled();
|
|
38
|
+
|
|
39
|
+
window.UILogger = {
|
|
40
|
+
/**
|
|
41
|
+
* Refresh debug state (call after changing localStorage)
|
|
42
|
+
*/
|
|
43
|
+
refresh() {
|
|
44
|
+
debugEnabled = isDebugEnabled();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Enable debug mode
|
|
49
|
+
*/
|
|
50
|
+
enableDebug() {
|
|
51
|
+
try {
|
|
52
|
+
localStorage.setItem('ag_debug', 'true');
|
|
53
|
+
debugEnabled = true;
|
|
54
|
+
console.info('[UILogger] Debug mode enabled. Refresh page to see all logs.');
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.warn('[UILogger] Could not save debug preference');
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Disable debug mode
|
|
62
|
+
*/
|
|
63
|
+
disableDebug() {
|
|
64
|
+
try {
|
|
65
|
+
localStorage.removeItem('ag_debug');
|
|
66
|
+
debugEnabled = false;
|
|
67
|
+
console.info('[UILogger] Debug mode disabled.');
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if debug mode is enabled
|
|
75
|
+
* @returns {boolean}
|
|
76
|
+
*/
|
|
77
|
+
isDebug() {
|
|
78
|
+
return debugEnabled;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Debug level - only logs if debug mode enabled
|
|
83
|
+
* Use for verbose debugging info (chart updates, cache operations, etc.)
|
|
84
|
+
*/
|
|
85
|
+
debug(...args) {
|
|
86
|
+
if (debugEnabled) {
|
|
87
|
+
console.log('[DEBUG]', ...args);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Info level - only logs if debug mode enabled
|
|
93
|
+
* Use for informational messages that aren't errors
|
|
94
|
+
*/
|
|
95
|
+
info(...args) {
|
|
96
|
+
if (debugEnabled) {
|
|
97
|
+
console.info('[INFO]', ...args);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Log level - alias for debug
|
|
103
|
+
*/
|
|
104
|
+
log(...args) {
|
|
105
|
+
if (debugEnabled) {
|
|
106
|
+
console.log(...args);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Warn level - always logs
|
|
112
|
+
* Use for important warnings that users should see
|
|
113
|
+
* But suppress noisy/expected warnings unless in debug mode
|
|
114
|
+
*/
|
|
115
|
+
warn(...args) {
|
|
116
|
+
// In production, only show critical warnings
|
|
117
|
+
// In debug mode, show all warnings
|
|
118
|
+
if (debugEnabled) {
|
|
119
|
+
console.warn(...args);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Warn level that always shows (for critical warnings)
|
|
125
|
+
*/
|
|
126
|
+
warnAlways(...args) {
|
|
127
|
+
console.warn(...args);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Error level - always logs
|
|
132
|
+
* Errors should always be visible for debugging
|
|
133
|
+
*/
|
|
134
|
+
error(...args) {
|
|
135
|
+
console.error(...args);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Log initial state (only in debug mode)
|
|
140
|
+
if (debugEnabled) {
|
|
141
|
+
console.info('[UILogger] Debug mode is ON. Set localStorage ag_debug=false to disable.');
|
|
142
|
+
}
|
|
143
|
+
})();
|
package/src/auth/oauth.js
CHANGED
|
@@ -137,22 +137,50 @@ export function extractCodeFromInput(input) {
|
|
|
137
137
|
return { code: trimmed, state: null };
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Attempt to bind server to a specific port
|
|
142
|
+
* @param {http.Server} server - HTTP server instance
|
|
143
|
+
* @param {number} port - Port to bind to
|
|
144
|
+
* @returns {Promise<number>} Resolves with port on success, rejects on error
|
|
145
|
+
*/
|
|
146
|
+
function tryBindPort(server, port) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const onError = (err) => {
|
|
149
|
+
server.removeListener('listening', onSuccess);
|
|
150
|
+
reject(err);
|
|
151
|
+
};
|
|
152
|
+
const onSuccess = () => {
|
|
153
|
+
server.removeListener('error', onError);
|
|
154
|
+
resolve(port);
|
|
155
|
+
};
|
|
156
|
+
server.once('error', onError);
|
|
157
|
+
server.once('listening', onSuccess);
|
|
158
|
+
server.listen(port);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
/**
|
|
141
163
|
* Start a local server to receive the OAuth callback
|
|
164
|
+
* Implements automatic port fallback for Windows compatibility (issue #176)
|
|
142
165
|
* Returns an object with a promise and an abort function
|
|
143
166
|
*
|
|
144
167
|
* @param {string} expectedState - Expected state parameter for CSRF protection
|
|
145
168
|
* @param {number} timeoutMs - Timeout in milliseconds (default 120000)
|
|
146
|
-
* @returns {{promise: Promise<string>, abort: Function}} Object with promise and
|
|
169
|
+
* @returns {{promise: Promise<string>, abort: Function, getPort: Function}} Object with promise, abort, and getPort functions
|
|
147
170
|
*/
|
|
148
171
|
export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
|
149
172
|
let server = null;
|
|
150
173
|
let timeoutId = null;
|
|
151
174
|
let isAborted = false;
|
|
175
|
+
let actualPort = OAUTH_CONFIG.callbackPort;
|
|
176
|
+
|
|
177
|
+
const promise = new Promise(async (resolve, reject) => {
|
|
178
|
+
// Build list of ports to try: primary + fallbacks
|
|
179
|
+
const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
|
|
180
|
+
const errors = [];
|
|
152
181
|
|
|
153
|
-
const promise = new Promise((resolve, reject) => {
|
|
154
182
|
server = http.createServer((req, res) => {
|
|
155
|
-
const url = new URL(req.url, `http://localhost:${
|
|
183
|
+
const url = new URL(req.url, `http://localhost:${actualPort}`);
|
|
156
184
|
|
|
157
185
|
if (url.pathname !== '/oauth-callback') {
|
|
158
186
|
res.writeHead(404);
|
|
@@ -232,17 +260,60 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
|
|
232
260
|
resolve(code);
|
|
233
261
|
});
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
263
|
+
// Try ports with fallback logic (issue #176 - Windows EACCES fix)
|
|
264
|
+
let boundSuccessfully = false;
|
|
265
|
+
for (const port of portsToTry) {
|
|
266
|
+
try {
|
|
267
|
+
await tryBindPort(server, port);
|
|
268
|
+
actualPort = port;
|
|
269
|
+
boundSuccessfully = true;
|
|
270
|
+
|
|
271
|
+
if (port !== OAUTH_CONFIG.callbackPort) {
|
|
272
|
+
logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
|
|
273
|
+
} else {
|
|
274
|
+
logger.info(`[OAuth] Callback server listening on port ${port}`);
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const errMsg = err.code === 'EACCES'
|
|
279
|
+
? `Permission denied on port ${port}`
|
|
280
|
+
: err.code === 'EADDRINUSE'
|
|
281
|
+
? `Port ${port} already in use`
|
|
282
|
+
: `Failed to bind port ${port}: ${err.message}`;
|
|
283
|
+
errors.push(errMsg);
|
|
284
|
+
logger.warn(`[OAuth] ${errMsg}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!boundSuccessfully) {
|
|
289
|
+
// All ports failed - provide helpful error message
|
|
290
|
+
const isWindows = process.platform === 'win32';
|
|
291
|
+
let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
|
|
292
|
+
|
|
293
|
+
if (isWindows) {
|
|
294
|
+
errorMsg += `\n
|
|
295
|
+
================== WINDOWS TROUBLESHOOTING ==================
|
|
296
|
+
The default port range may be reserved by Hyper-V/WSL2/Docker.
|
|
297
|
+
|
|
298
|
+
Option 1: Use a custom port
|
|
299
|
+
Set OAUTH_CALLBACK_PORT=3456 in your environment or .env file
|
|
300
|
+
|
|
301
|
+
Option 2: Reset Windows NAT (run as Administrator)
|
|
302
|
+
net stop winnat && net start winnat
|
|
303
|
+
|
|
304
|
+
Option 3: Check reserved port ranges
|
|
305
|
+
netsh interface ipv4 show excludedportrange protocol=tcp
|
|
306
|
+
|
|
307
|
+
Option 4: Exclude port from reservation (run as Administrator)
|
|
308
|
+
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
|
|
309
|
+
==============================================================`;
|
|
238
310
|
} else {
|
|
239
|
-
|
|
311
|
+
errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
|
|
240
312
|
}
|
|
241
|
-
});
|
|
242
313
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
314
|
+
reject(new Error(errorMsg));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
246
317
|
|
|
247
318
|
// Timeout after specified duration
|
|
248
319
|
timeoutId = setTimeout(() => {
|
|
@@ -266,7 +337,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
|
|
|
266
337
|
}
|
|
267
338
|
};
|
|
268
339
|
|
|
269
|
-
|
|
340
|
+
// Get actual port (useful when fallback is used)
|
|
341
|
+
const getPort = () => actualPort;
|
|
342
|
+
|
|
343
|
+
return { promise, abort, getPort };
|
|
270
344
|
}
|
|
271
345
|
|
|
272
346
|
/**
|
package/src/constants.js
CHANGED
|
@@ -188,13 +188,19 @@ export function isThinkingModel(modelName) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// Google OAuth configuration (from opencode-antigravity-auth)
|
|
191
|
+
// OAuth callback port - configurable via environment variable for Windows compatibility (issue #176)
|
|
192
|
+
// Windows may reserve ports in range 49152-65535 for Hyper-V/WSL2/Docker, causing EACCES errors
|
|
193
|
+
const OAUTH_CALLBACK_PORT = parseInt(process.env.OAUTH_CALLBACK_PORT || '51121', 10);
|
|
194
|
+
const OAUTH_CALLBACK_FALLBACK_PORTS = [51122, 51123, 51124, 51125, 51126];
|
|
195
|
+
|
|
191
196
|
export const OAUTH_CONFIG = {
|
|
192
197
|
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
|
|
193
198
|
clientSecret: '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',
|
|
194
199
|
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
195
200
|
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
196
201
|
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
|
|
197
|
-
callbackPort:
|
|
202
|
+
callbackPort: OAUTH_CALLBACK_PORT,
|
|
203
|
+
callbackFallbackPorts: OAUTH_CALLBACK_FALLBACK_PORTS,
|
|
198
204
|
scopes: [
|
|
199
205
|
'https://www.googleapis.com/auth/cloud-platform',
|
|
200
206
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
@@ -236,7 +242,7 @@ export const DEFAULT_PRESETS = [
|
|
|
236
242
|
ANTHROPIC_MODEL: 'claude-opus-4-5-thinking',
|
|
237
243
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-thinking',
|
|
238
244
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
|
|
239
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: '
|
|
245
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-sonnet-4-5',
|
|
240
246
|
CLAUDE_CODE_SUBAGENT_MODEL: 'claude-sonnet-4-5-thinking',
|
|
241
247
|
ENABLE_EXPERIMENTAL_MCP_CLI: 'true'
|
|
242
248
|
}
|
|
@@ -249,7 +255,7 @@ export const DEFAULT_PRESETS = [
|
|
|
249
255
|
ANTHROPIC_MODEL: 'gemini-3-pro-high[1m]',
|
|
250
256
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-high[1m]',
|
|
251
257
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3-flash[1m]',
|
|
252
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-
|
|
258
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-flash[1m]',
|
|
253
259
|
CLAUDE_CODE_SUBAGENT_MODEL: 'gemini-3-flash[1m]',
|
|
254
260
|
ENABLE_EXPERIMENTAL_MCP_CLI: 'true'
|
|
255
261
|
}
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
hasGeminiHistory,
|
|
19
19
|
hasUnsignedThinkingBlocks,
|
|
20
20
|
needsThinkingRecovery,
|
|
21
|
-
closeToolLoopForThinking
|
|
21
|
+
closeToolLoopForThinking,
|
|
22
|
+
cleanCacheControl
|
|
22
23
|
} from './thinking-utils.js';
|
|
23
24
|
import { logger } from '../utils/logger.js';
|
|
24
25
|
|
|
@@ -32,7 +33,13 @@ import { logger } from '../utils/logger.js';
|
|
|
32
33
|
* @returns {Object} Request body for Cloud Code API
|
|
33
34
|
*/
|
|
34
35
|
export function convertAnthropicToGoogle(anthropicRequest) {
|
|
35
|
-
|
|
36
|
+
// [CRITICAL FIX] Pre-clean all cache_control fields from messages (Issue #189)
|
|
37
|
+
// Claude Code CLI sends cache_control on various content blocks, but Cloud Code API
|
|
38
|
+
// rejects them with "Extra inputs are not permitted". Clean them proactively here
|
|
39
|
+
// before any other processing, following the pattern from Antigravity-Manager.
|
|
40
|
+
const messages = cleanCacheControl(anthropicRequest.messages || []);
|
|
41
|
+
|
|
42
|
+
const { system, max_tokens, temperature, top_p, top_k, stop_sequences, tools, tool_choice, thinking } = anthropicRequest;
|
|
36
43
|
const modelName = anthropicRequest.model || '';
|
|
37
44
|
const modelFamily = getModelFamily(modelName);
|
|
38
45
|
const isClaudeModel = modelFamily === 'claude';
|
|
@@ -7,6 +7,62 @@ 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
|
+
// Cache Control Cleaning (Issue #189)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Remove cache_control fields from all content blocks in messages.
|
|
16
|
+
* This is a critical fix for Issue #189 where Claude Code CLI sends cache_control
|
|
17
|
+
* fields that the Cloud Code API rejects with "Extra inputs are not permitted".
|
|
18
|
+
*
|
|
19
|
+
* Inspired by Antigravity-Manager's clean_cache_control_from_messages() approach,
|
|
20
|
+
* this function proactively strips cache_control from ALL block types at the
|
|
21
|
+
* entry point of the conversion pipeline.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array<Object>} messages - Array of messages in Anthropic format
|
|
24
|
+
* @returns {Array<Object>} Messages with cache_control fields removed
|
|
25
|
+
*/
|
|
26
|
+
export function cleanCacheControl(messages) {
|
|
27
|
+
if (!Array.isArray(messages)) return messages;
|
|
28
|
+
|
|
29
|
+
let removedCount = 0;
|
|
30
|
+
|
|
31
|
+
const cleaned = messages.map(message => {
|
|
32
|
+
if (!message || typeof message !== 'object') return message;
|
|
33
|
+
|
|
34
|
+
// Handle string content (no cache_control possible)
|
|
35
|
+
if (typeof message.content === 'string') return message;
|
|
36
|
+
|
|
37
|
+
// Handle array content
|
|
38
|
+
if (!Array.isArray(message.content)) return message;
|
|
39
|
+
|
|
40
|
+
const cleanedContent = message.content.map(block => {
|
|
41
|
+
if (!block || typeof block !== 'object') return block;
|
|
42
|
+
|
|
43
|
+
// Check if cache_control exists before destructuring
|
|
44
|
+
if (block.cache_control === undefined) return block;
|
|
45
|
+
|
|
46
|
+
// Create a shallow copy without cache_control
|
|
47
|
+
const { cache_control, ...cleanBlock } = block;
|
|
48
|
+
removedCount++;
|
|
49
|
+
|
|
50
|
+
return cleanBlock;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...message,
|
|
55
|
+
content: cleanedContent
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (removedCount > 0) {
|
|
60
|
+
logger.debug(`[ThinkingUtils] Removed cache_control from ${removedCount} block(s)`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cleaned;
|
|
64
|
+
}
|
|
65
|
+
|
|
10
66
|
/**
|
|
11
67
|
* Check if a part is a thinking block
|
|
12
68
|
* @param {Object} part - Content part to check
|
|
@@ -104,6 +160,38 @@ function sanitizeAnthropicThinkingBlock(block) {
|
|
|
104
160
|
return block;
|
|
105
161
|
}
|
|
106
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Sanitize a text block by removing extra fields like cache_control.
|
|
165
|
+
* Only keeps: type, text
|
|
166
|
+
* @param {Object} block - Text block to sanitize
|
|
167
|
+
* @returns {Object} Sanitized text block
|
|
168
|
+
*/
|
|
169
|
+
function sanitizeTextBlock(block) {
|
|
170
|
+
if (!block || block.type !== 'text') return block;
|
|
171
|
+
|
|
172
|
+
const sanitized = { type: 'text' };
|
|
173
|
+
if (block.text !== undefined) sanitized.text = block.text;
|
|
174
|
+
return sanitized;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sanitize a tool_use block by removing extra fields like cache_control.
|
|
179
|
+
* Only keeps: type, id, name, input, thoughtSignature (for Gemini)
|
|
180
|
+
* @param {Object} block - Tool_use block to sanitize
|
|
181
|
+
* @returns {Object} Sanitized tool_use block
|
|
182
|
+
*/
|
|
183
|
+
function sanitizeToolUseBlock(block) {
|
|
184
|
+
if (!block || block.type !== 'tool_use') return block;
|
|
185
|
+
|
|
186
|
+
const sanitized = { type: 'tool_use' };
|
|
187
|
+
if (block.id !== undefined) sanitized.id = block.id;
|
|
188
|
+
if (block.name !== undefined) sanitized.name = block.name;
|
|
189
|
+
if (block.input !== undefined) sanitized.input = block.input;
|
|
190
|
+
// Preserve thoughtSignature for Gemini models
|
|
191
|
+
if (block.thoughtSignature !== undefined) sanitized.thoughtSignature = block.thoughtSignature;
|
|
192
|
+
return sanitized;
|
|
193
|
+
}
|
|
194
|
+
|
|
107
195
|
/**
|
|
108
196
|
* Filter content array, keeping only thinking blocks with valid signatures.
|
|
109
197
|
*/
|
|
@@ -259,11 +347,13 @@ export function reorderAssistantContent(content) {
|
|
|
259
347
|
// Sanitize thinking blocks to remove cache_control and other extra fields
|
|
260
348
|
thinkingBlocks.push(sanitizeAnthropicThinkingBlock(block));
|
|
261
349
|
} else if (block.type === 'tool_use') {
|
|
262
|
-
|
|
350
|
+
// Sanitize tool_use blocks to remove cache_control and other extra fields
|
|
351
|
+
toolUseBlocks.push(sanitizeToolUseBlock(block));
|
|
263
352
|
} else if (block.type === 'text') {
|
|
264
353
|
// Only keep text blocks with meaningful content
|
|
265
354
|
if (block.text && block.text.trim().length > 0) {
|
|
266
|
-
|
|
355
|
+
// Sanitize text blocks to remove cache_control and other extra fields
|
|
356
|
+
textBlocks.push(sanitizeTextBlock(block));
|
|
267
357
|
} else {
|
|
268
358
|
droppedEmptyBlocks++;
|
|
269
359
|
}
|