commons-proxy 2.0.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Model API for Cloud Code
3
+ *
4
+ * Handles model listing and quota retrieval from the Cloud Code API.
5
+ */
6
+
7
+ import {
8
+ ANTIGRAVITY_ENDPOINT_FALLBACKS,
9
+ ANTIGRAVITY_HEADERS,
10
+ LOAD_CODE_ASSIST_ENDPOINTS,
11
+ LOAD_CODE_ASSIST_HEADERS,
12
+ getModelFamily
13
+ } from '../constants.js';
14
+ import { logger } from '../utils/logger.js';
15
+
16
+ /**
17
+ * Check if a model is supported (Claude or Gemini)
18
+ * @param {string} modelId - Model ID to check
19
+ * @returns {boolean} True if model is supported
20
+ */
21
+ function isSupportedModel(modelId) {
22
+ const family = getModelFamily(modelId);
23
+ return family === 'claude' || family === 'gemini';
24
+ }
25
+
26
+ /**
27
+ * List available models in Anthropic API format
28
+ * Fetches models dynamically from the Cloud Code API
29
+ *
30
+ * @param {string} token - OAuth access token
31
+ * @returns {Promise<{object: string, data: Array<{id: string, object: string, created: number, owned_by: string, description: string}>}>} List of available models
32
+ */
33
+ export async function listModels(token) {
34
+ const data = await fetchAvailableModels(token);
35
+ if (!data || !data.models) {
36
+ return { object: 'list', data: [] };
37
+ }
38
+
39
+ const modelList = Object.entries(data.models)
40
+ .filter(([modelId]) => isSupportedModel(modelId))
41
+ .map(([modelId, modelData]) => ({
42
+ id: modelId,
43
+ object: 'model',
44
+ created: Math.floor(Date.now() / 1000),
45
+ owned_by: 'anthropic',
46
+ description: modelData.displayName || modelId
47
+ }));
48
+
49
+ return {
50
+ object: 'list',
51
+ data: modelList
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Fetch available models with quota info from Cloud Code API
57
+ * Returns model quotas including remaining fraction and reset time
58
+ *
59
+ * @param {string} token - OAuth access token
60
+ * @param {string} [projectId] - Optional project ID for accurate quota info
61
+ * @returns {Promise<Object>} Raw response from fetchAvailableModels API
62
+ */
63
+ export async function fetchAvailableModels(token, projectId = null) {
64
+ const headers = {
65
+ 'Authorization': `Bearer ${token}`,
66
+ 'Content-Type': 'application/json',
67
+ ...ANTIGRAVITY_HEADERS
68
+ };
69
+
70
+ // Include project ID in body for accurate quota info (per Quotio implementation)
71
+ const body = projectId ? { project: projectId } : {};
72
+
73
+ for (const endpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
74
+ try {
75
+ const url = `${endpoint}/v1internal:fetchAvailableModels`;
76
+ const response = await fetch(url, {
77
+ method: 'POST',
78
+ headers,
79
+ body: JSON.stringify(body)
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const errorText = await response.text();
84
+ logger.warn(`[CloudCode] fetchAvailableModels error at ${endpoint}: ${response.status}`);
85
+ continue;
86
+ }
87
+
88
+ return await response.json();
89
+ } catch (error) {
90
+ logger.warn(`[CloudCode] fetchAvailableModels failed at ${endpoint}:`, error.message);
91
+ }
92
+ }
93
+
94
+ throw new Error('Failed to fetch available models from all endpoints');
95
+ }
96
+
97
+ /**
98
+ * Get model quotas for an account
99
+ * Extracts quota info (remaining fraction and reset time) for each model
100
+ *
101
+ * @param {string} token - OAuth access token
102
+ * @param {string} [projectId] - Optional project ID for accurate quota info
103
+ * @returns {Promise<Object>} Map of modelId -> { remainingFraction, resetTime }
104
+ */
105
+ export async function getModelQuotas(token, projectId = null) {
106
+ const data = await fetchAvailableModels(token, projectId);
107
+ if (!data || !data.models) return {};
108
+
109
+ const quotas = {};
110
+ for (const [modelId, modelData] of Object.entries(data.models)) {
111
+ // Only include Claude and Gemini models
112
+ if (!isSupportedModel(modelId)) continue;
113
+
114
+ if (modelData.quotaInfo) {
115
+ quotas[modelId] = {
116
+ // When remainingFraction is missing but resetTime is present, quota is exhausted (0%)
117
+ remainingFraction: modelData.quotaInfo.remainingFraction ?? (modelData.quotaInfo.resetTime ? 0 : null),
118
+ resetTime: modelData.quotaInfo.resetTime ?? null
119
+ };
120
+ }
121
+ }
122
+
123
+ return quotas;
124
+ }
125
+
126
+ /**
127
+ * Parse tier ID string to determine subscription level
128
+ * @param {string} tierId - The tier ID from the API
129
+ * @returns {'free' | 'pro' | 'ultra' | 'unknown'} The subscription tier
130
+ */
131
+ export function parseTierId(tierId) {
132
+ if (!tierId) return 'unknown';
133
+ const lower = tierId.toLowerCase();
134
+
135
+ if (lower.includes('ultra')) {
136
+ return 'ultra';
137
+ }
138
+ if (lower === 'standard-tier') {
139
+ // standard-tier = "Gemini Code Assist" (paid, project-based)
140
+ return 'pro';
141
+ }
142
+ if (lower.includes('pro') || lower.includes('premium')) {
143
+ return 'pro';
144
+ }
145
+ if (lower === 'free-tier' || lower.includes('free')) {
146
+ return 'free';
147
+ }
148
+ return 'unknown';
149
+ }
150
+
151
+ /**
152
+ * Get subscription tier for an account
153
+ * Calls loadCodeAssist API to discover project ID and subscription tier
154
+ *
155
+ * @param {string} token - OAuth access token
156
+ * @returns {Promise<{tier: string, projectId: string|null}>} Subscription tier (free/pro/ultra) and project ID
157
+ */
158
+ export async function getSubscriptionTier(token) {
159
+ const headers = {
160
+ 'Authorization': `Bearer ${token}`,
161
+ 'Content-Type': 'application/json',
162
+ ...LOAD_CODE_ASSIST_HEADERS
163
+ };
164
+
165
+ for (const endpoint of LOAD_CODE_ASSIST_ENDPOINTS) {
166
+ try {
167
+ const url = `${endpoint}/v1internal:loadCodeAssist`;
168
+ const response = await fetch(url, {
169
+ method: 'POST',
170
+ headers,
171
+ body: JSON.stringify({
172
+ metadata: {
173
+ ideType: 'IDE_UNSPECIFIED',
174
+ platform: 'PLATFORM_UNSPECIFIED',
175
+ pluginType: 'GEMINI',
176
+ duetProject: 'rising-fact-p41fc'
177
+ }
178
+ })
179
+ });
180
+
181
+ if (!response.ok) {
182
+ logger.warn(`[CloudCode] loadCodeAssist error at ${endpoint}: ${response.status}`);
183
+ continue;
184
+ }
185
+
186
+ const data = await response.json();
187
+
188
+ // Debug: Log all tier-related fields from the response
189
+ logger.debug(`[CloudCode] loadCodeAssist tier data: paidTier=${JSON.stringify(data.paidTier)}, currentTier=${JSON.stringify(data.currentTier)}, allowedTiers=${JSON.stringify(data.allowedTiers?.map(t => ({ id: t?.id, isDefault: t?.isDefault })))}`);
190
+
191
+ // Extract project ID
192
+ let projectId = null;
193
+ if (typeof data.cloudaicompanionProject === 'string') {
194
+ projectId = data.cloudaicompanionProject;
195
+ } else if (data.cloudaicompanionProject?.id) {
196
+ projectId = data.cloudaicompanionProject.id;
197
+ }
198
+
199
+ // Extract subscription tier
200
+ // Priority: paidTier > currentTier > allowedTiers
201
+ // - paidTier.id: "g1-pro-tier", "g1-ultra-tier" (Google One subscription)
202
+ // - currentTier.id: "standard-tier" (pro), "free-tier" (free)
203
+ // - allowedTiers: fallback when currentTier is missing
204
+ // Note: paidTier is sometimes missing from the response even for Pro accounts
205
+ let tier = 'unknown';
206
+ let tierId = null;
207
+ let tierSource = null;
208
+
209
+ // 1. Check paidTier first (Google One AI subscription - most reliable)
210
+ if (data.paidTier?.id) {
211
+ tierId = data.paidTier.id;
212
+ tier = parseTierId(tierId);
213
+ tierSource = 'paidTier';
214
+ }
215
+
216
+ // 2. Fall back to currentTier if paidTier didn't give us a tier
217
+ if (tier === 'unknown' && data.currentTier?.id) {
218
+ tierId = data.currentTier.id;
219
+ tier = parseTierId(tierId);
220
+ tierSource = 'currentTier';
221
+ }
222
+
223
+ // 3. Fall back to allowedTiers (find the default or first non-free tier)
224
+ if (tier === 'unknown' && Array.isArray(data.allowedTiers) && data.allowedTiers.length > 0) {
225
+ // First look for the default tier
226
+ let defaultTier = data.allowedTiers.find(t => t?.isDefault);
227
+ if (!defaultTier) {
228
+ defaultTier = data.allowedTiers[0];
229
+ }
230
+ if (defaultTier?.id) {
231
+ tierId = defaultTier.id;
232
+ tier = parseTierId(tierId);
233
+ tierSource = 'allowedTiers';
234
+ }
235
+ }
236
+
237
+ logger.debug(`[CloudCode] Subscription detected: ${tier} (tierId: ${tierId}, source: ${tierSource}), Project: ${projectId}`);
238
+
239
+ return { tier, projectId };
240
+ } catch (error) {
241
+ logger.warn(`[CloudCode] loadCodeAssist failed at ${endpoint}:`, error.message);
242
+ }
243
+ }
244
+
245
+ // Fallback: return default values if all endpoints fail
246
+ logger.warn('[CloudCode] Failed to detect subscription tier from all endpoints. Defaulting to free.');
247
+ return { tier: 'free', projectId: null };
248
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Rate Limit Parser for Cloud Code
3
+ *
4
+ * Parses reset times from HTTP headers and error messages.
5
+ * Supports various formats: Retry-After, x-ratelimit-reset,
6
+ * quotaResetDelay, quotaResetTimeStamp, and duration strings.
7
+ */
8
+
9
+ import { formatDuration } from '../utils/helpers.js';
10
+ import { logger } from '../utils/logger.js';
11
+
12
+ /**
13
+ * Parse reset time from HTTP response or error
14
+ * Checks headers first, then error message body
15
+ * Returns milliseconds or null if not found
16
+ *
17
+ * @param {Response|Error} responseOrError - HTTP Response object or Error
18
+ * @param {string} errorText - Optional error body text
19
+ */
20
+ export function parseResetTime(responseOrError, errorText = '') {
21
+ let resetMs = null;
22
+
23
+ // If it's a Response object, check headers first
24
+ if (responseOrError && typeof responseOrError.headers?.get === 'function') {
25
+ const headers = responseOrError.headers;
26
+
27
+ // Standard Retry-After header (seconds or HTTP date)
28
+ const retryAfter = headers.get('retry-after');
29
+ if (retryAfter) {
30
+ const seconds = parseInt(retryAfter, 10);
31
+ if (!isNaN(seconds)) {
32
+ resetMs = seconds * 1000;
33
+ logger.debug(`[CloudCode] Retry-After header: ${seconds}s`);
34
+ } else {
35
+ // Try parsing as HTTP date
36
+ const date = new Date(retryAfter);
37
+ if (!isNaN(date.getTime())) {
38
+ resetMs = date.getTime() - Date.now();
39
+ if (resetMs > 0) {
40
+ logger.debug(`[CloudCode] Retry-After date: ${retryAfter}`);
41
+ } else {
42
+ resetMs = null;
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ // x-ratelimit-reset (Unix timestamp in seconds)
49
+ if (!resetMs) {
50
+ const ratelimitReset = headers.get('x-ratelimit-reset');
51
+ if (ratelimitReset) {
52
+ const resetTimestamp = parseInt(ratelimitReset, 10) * 1000;
53
+ resetMs = resetTimestamp - Date.now();
54
+ if (resetMs > 0) {
55
+ logger.debug(`[CloudCode] x-ratelimit-reset: ${new Date(resetTimestamp).toISOString()}`);
56
+ } else {
57
+ resetMs = null;
58
+ }
59
+ }
60
+ }
61
+
62
+ // x-ratelimit-reset-after (seconds)
63
+ if (!resetMs) {
64
+ const resetAfter = headers.get('x-ratelimit-reset-after');
65
+ if (resetAfter) {
66
+ const seconds = parseInt(resetAfter, 10);
67
+ if (!isNaN(seconds) && seconds > 0) {
68
+ resetMs = seconds * 1000;
69
+ logger.debug(`[CloudCode] x-ratelimit-reset-after: ${seconds}s`);
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ // If no header found, try parsing from error message/body
76
+ if (!resetMs) {
77
+ const msg = (responseOrError instanceof Error ? responseOrError.message : errorText) || '';
78
+
79
+ // Try to extract "quotaResetDelay" first (e.g. "754.431528ms" or "1.5s")
80
+ // This is Google's preferred format for rate limit reset delay
81
+ const quotaDelayMatch = msg.match(/quotaResetDelay[:\s"]+(\d+(?:\.\d+)?)(ms|s)/i);
82
+ if (quotaDelayMatch) {
83
+ const value = parseFloat(quotaDelayMatch[1]);
84
+ const unit = quotaDelayMatch[2].toLowerCase();
85
+ resetMs = unit === 's' ? Math.ceil(value * 1000) : Math.ceil(value);
86
+ logger.debug(`[CloudCode] Parsed quotaResetDelay from body: ${resetMs}ms`);
87
+ }
88
+
89
+ // Try to extract "quotaResetTimeStamp" (ISO format like "2025-12-31T07:00:47Z")
90
+ if (!resetMs) {
91
+ const quotaTimestampMatch = msg.match(/quotaResetTimeStamp[:\s"]+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i);
92
+ if (quotaTimestampMatch) {
93
+ const resetTime = new Date(quotaTimestampMatch[1]).getTime();
94
+ if (!isNaN(resetTime)) {
95
+ resetMs = resetTime - Date.now();
96
+ // Even if expired or 0, we found a timestamp, so rely on it.
97
+ // But if it's negative, it means "now", so treat as small wait.
98
+ logger.debug(`[CloudCode] Parsed quotaResetTimeStamp: ${quotaTimestampMatch[1]} (Delta: ${resetMs}ms)`);
99
+ }
100
+ }
101
+ }
102
+
103
+ // Try to extract "retry-after-ms" or "retryDelay" - check seconds format first (e.g. "7739.23s")
104
+ // Added stricter regex to avoid partial matches
105
+ if (!resetMs) {
106
+ const secMatch = msg.match(/(?:retry[-_]?after[-_]?ms|retryDelay)[:\s"]+([\d.]+)(?:s\b|s")/i);
107
+ if (secMatch) {
108
+ resetMs = Math.ceil(parseFloat(secMatch[1]) * 1000);
109
+ logger.debug(`[CloudCode] Parsed retry seconds from body (precise): ${resetMs}ms`);
110
+ }
111
+ }
112
+
113
+ if (!resetMs) {
114
+ // Check for ms (explicit "ms" suffix or implicit if no suffix)
115
+ const msMatch = msg.match(/(?:retry[-_]?after[-_]?ms|retryDelay)[:\s"]+(\d+)(?:\s*ms)?(?![\w.])/i);
116
+ if (msMatch) {
117
+ resetMs = parseInt(msMatch[1], 10);
118
+ logger.debug(`[CloudCode] Parsed retry-after-ms from body: ${resetMs}ms`);
119
+ }
120
+ }
121
+
122
+ // Try to extract seconds value like "retry after 60 seconds"
123
+ if (!resetMs) {
124
+ const secMatch = msg.match(/retry\s+(?:after\s+)?(\d+)\s*(?:sec|s\b)/i);
125
+ if (secMatch) {
126
+ resetMs = parseInt(secMatch[1], 10) * 1000;
127
+ logger.debug(`[CloudCode] Parsed retry seconds from body: ${secMatch[1]}s`);
128
+ }
129
+ }
130
+
131
+ // Try to extract duration like "1h23m45s" or "23m45s" or "45s"
132
+ if (!resetMs) {
133
+ const durationMatch = msg.match(/(\d+)h(\d+)m(\d+)s|(\d+)m(\d+)s|(\d+)s/i);
134
+ if (durationMatch) {
135
+ if (durationMatch[1]) {
136
+ const hours = parseInt(durationMatch[1], 10);
137
+ const minutes = parseInt(durationMatch[2], 10);
138
+ const seconds = parseInt(durationMatch[3], 10);
139
+ resetMs = (hours * 3600 + minutes * 60 + seconds) * 1000;
140
+ } else if (durationMatch[4]) {
141
+ const minutes = parseInt(durationMatch[4], 10);
142
+ const seconds = parseInt(durationMatch[5], 10);
143
+ resetMs = (minutes * 60 + seconds) * 1000;
144
+ } else if (durationMatch[6]) {
145
+ resetMs = parseInt(durationMatch[6], 10) * 1000;
146
+ }
147
+ if (resetMs) {
148
+ logger.debug(`[CloudCode] Parsed duration from body: ${formatDuration(resetMs)}`);
149
+ }
150
+ }
151
+ }
152
+
153
+ // Try to extract ISO timestamp or Unix timestamp
154
+ if (!resetMs) {
155
+ const isoMatch = msg.match(/reset[:\s"]+(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)/i);
156
+ if (isoMatch) {
157
+ const resetTime = new Date(isoMatch[1]).getTime();
158
+ if (!isNaN(resetTime)) {
159
+ resetMs = resetTime - Date.now();
160
+ if (resetMs > 0) {
161
+ logger.debug(`[CloudCode] Parsed ISO reset time: ${isoMatch[1]}`);
162
+ } else {
163
+ resetMs = null;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // SANITY CHECK: Handle very small or negative reset times
171
+ // For sub-second rate limits (common with per-second quotas), add a small buffer
172
+ // For negative or zero, use a reasonable minimum
173
+ if (resetMs !== null) {
174
+ if (resetMs <= 0) {
175
+ logger.debug(`[CloudCode] Reset time invalid (${resetMs}ms), using 500ms default`);
176
+ resetMs = 500;
177
+ } else if (resetMs < 500) {
178
+ // Very short reset - add 200ms buffer for network latency
179
+ logger.debug(`[CloudCode] Short reset time (${resetMs}ms), adding 200ms buffer`);
180
+ resetMs = resetMs + 200;
181
+ }
182
+ // Note: No longer enforcing 2s minimum - this was causing cascading failures
183
+ // when all accounts had short rate limits simultaneously
184
+ }
185
+
186
+ return resetMs;
187
+ }
188
+
189
+ /**
190
+ * Parse the rate limit reason from error text
191
+ * Used for smart backoff by error type (matches opencode-cloudcode-auth)
192
+ *
193
+ * @param {string} errorText - Error message/body text
194
+ * @returns {'RATE_LIMIT_EXCEEDED' | 'QUOTA_EXHAUSTED' | 'MODEL_CAPACITY_EXHAUSTED' | 'SERVER_ERROR' | 'UNKNOWN'} Error reason
195
+ */
196
+ export function parseRateLimitReason(errorText) {
197
+ const lower = (errorText || '').toLowerCase();
198
+
199
+ // Check for quota exhaustion (daily/hourly limits)
200
+ if (lower.includes('quota_exhausted') ||
201
+ lower.includes('quotaresetdelay') ||
202
+ lower.includes('quotaresettimestamp') ||
203
+ lower.includes('resource_exhausted') ||
204
+ lower.includes('daily limit') ||
205
+ lower.includes('quota exceeded')) {
206
+ return 'QUOTA_EXHAUSTED';
207
+ }
208
+
209
+ // Check for model capacity issues (temporary, retry quickly)
210
+ if (lower.includes('model_capacity_exhausted') ||
211
+ lower.includes('capacity_exhausted') ||
212
+ lower.includes('model is currently overloaded') ||
213
+ lower.includes('service temporarily unavailable')) {
214
+ return 'MODEL_CAPACITY_EXHAUSTED';
215
+ }
216
+
217
+ // Check for rate limiting (per-minute limits)
218
+ if (lower.includes('rate_limit_exceeded') ||
219
+ lower.includes('rate limit') ||
220
+ lower.includes('too many requests') ||
221
+ lower.includes('throttl')) {
222
+ return 'RATE_LIMIT_EXCEEDED';
223
+ }
224
+
225
+ // Check for server errors
226
+ if (lower.includes('internal server error') ||
227
+ lower.includes('server error') ||
228
+ lower.includes('503') ||
229
+ lower.includes('502') ||
230
+ lower.includes('504')) {
231
+ return 'SERVER_ERROR';
232
+ }
233
+
234
+ return 'UNKNOWN';
235
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Request Builder for Cloud Code
3
+ *
4
+ * Builds request payloads and headers for the Cloud Code API.
5
+ */
6
+
7
+ import crypto from 'crypto';
8
+ import {
9
+ ANTIGRAVITY_HEADERS,
10
+ ANTIGRAVITY_SYSTEM_INSTRUCTION,
11
+ getModelFamily,
12
+ isThinkingModel
13
+ } from '../constants.js';
14
+ import { convertAnthropicToGoogle } from '../format/index.js';
15
+ import { deriveSessionId } from './session-manager.js';
16
+
17
+ /**
18
+ * Build the wrapped request body for Cloud Code API
19
+ *
20
+ * @param {Object} anthropicRequest - The Anthropic-format request
21
+ * @param {string} projectId - The project ID to use
22
+ * @returns {Object} The Cloud Code API request payload
23
+ */
24
+ export function buildCloudCodeRequest(anthropicRequest, projectId) {
25
+ const model = anthropicRequest.model;
26
+ const googleRequest = convertAnthropicToGoogle(anthropicRequest);
27
+
28
+ // Use stable session ID derived from first user message for cache continuity
29
+ googleRequest.sessionId = deriveSessionId(anthropicRequest);
30
+
31
+ // Build system instruction parts array with [ignore] tags to prevent model from
32
+ // identifying as "Antigravity" (fixes GitHub issue #76)
33
+ // Reference: CLIProxyAPI, gcli2api, AIClient-2-API all use this approach
34
+ const systemParts = [
35
+ { text: ANTIGRAVITY_SYSTEM_INSTRUCTION },
36
+ { text: `Please ignore the following [ignore]${ANTIGRAVITY_SYSTEM_INSTRUCTION}[/ignore]` }
37
+ ];
38
+
39
+ // Append any existing system instructions from the request
40
+ if (googleRequest.systemInstruction && googleRequest.systemInstruction.parts) {
41
+ for (const part of googleRequest.systemInstruction.parts) {
42
+ if (part.text) {
43
+ systemParts.push({ text: part.text });
44
+ }
45
+ }
46
+ }
47
+
48
+ const payload = {
49
+ project: projectId,
50
+ model: model,
51
+ request: googleRequest,
52
+ userAgent: 'commons-proxy',
53
+ requestType: 'agent', // CLIProxyAPI v6.6.89 compatibility
54
+ requestId: 'agent-' + crypto.randomUUID()
55
+ };
56
+
57
+ // Inject systemInstruction with role: "user" at the top level (CLIProxyAPI v6.6.89 behavior)
58
+ payload.request.systemInstruction = {
59
+ role: 'user',
60
+ parts: systemParts
61
+ };
62
+
63
+ return payload;
64
+ }
65
+
66
+ /**
67
+ * Build headers for Cloud Code API requests
68
+ *
69
+ * @param {string} token - OAuth access token
70
+ * @param {string} model - Model name
71
+ * @param {string} accept - Accept header value (default: 'application/json')
72
+ * @returns {Object} Headers object
73
+ */
74
+ export function buildHeaders(token, model, accept = 'application/json') {
75
+ const headers = {
76
+ 'Authorization': `Bearer ${token}`,
77
+ 'Content-Type': 'application/json',
78
+ ...ANTIGRAVITY_HEADERS
79
+ };
80
+
81
+ const modelFamily = getModelFamily(model);
82
+
83
+ // Add interleaved thinking header only for Claude thinking models
84
+ if (modelFamily === 'claude' && isThinkingModel(model)) {
85
+ headers['anthropic-beta'] = 'interleaved-thinking-2025-05-14';
86
+ }
87
+
88
+ if (accept !== 'application/json') {
89
+ headers['Accept'] = accept;
90
+ }
91
+
92
+ return headers;
93
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Session Management for Cloud Code
3
+ *
4
+ * Handles session ID derivation for prompt caching continuity.
5
+ * Session IDs are derived from the first user message to ensure
6
+ * the same conversation uses the same session across turns.
7
+ */
8
+
9
+ import crypto from 'crypto';
10
+
11
+ /**
12
+ * Derive a stable session ID from the first user message in the conversation.
13
+ * This ensures the same conversation uses the same session ID across turns,
14
+ * enabling prompt caching (cache is scoped to session + organization).
15
+ *
16
+ * @param {Object} anthropicRequest - The Anthropic-format request
17
+ * @returns {string} A stable session ID (32 hex characters) or random UUID if no user message
18
+ */
19
+ export function deriveSessionId(anthropicRequest) {
20
+ const messages = anthropicRequest.messages || [];
21
+
22
+ // Find the first user message
23
+ for (const msg of messages) {
24
+ if (msg.role === 'user') {
25
+ let content = '';
26
+
27
+ if (typeof msg.content === 'string') {
28
+ content = msg.content;
29
+ } else if (Array.isArray(msg.content)) {
30
+ // Extract text from content blocks
31
+ content = msg.content
32
+ .filter(block => block.type === 'text' && block.text)
33
+ .map(block => block.text)
34
+ .join('\n');
35
+ }
36
+
37
+ if (content) {
38
+ // Hash the content with SHA256, return first 32 hex chars
39
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
40
+ return hash.substring(0, 32);
41
+ }
42
+ }
43
+ }
44
+
45
+ // Fallback to random UUID if no user message found
46
+ return crypto.randomUUID();
47
+ }