@kamel-ahmed/proxy-claude 1.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.js +124 -0
  4. package/package.json +80 -0
  5. package/public/app.js +228 -0
  6. package/public/css/src/input.css +523 -0
  7. package/public/css/style.css +1 -0
  8. package/public/favicon.svg +10 -0
  9. package/public/index.html +381 -0
  10. package/public/js/components/account-manager.js +245 -0
  11. package/public/js/components/claude-config.js +420 -0
  12. package/public/js/components/dashboard/charts.js +589 -0
  13. package/public/js/components/dashboard/filters.js +362 -0
  14. package/public/js/components/dashboard/stats.js +110 -0
  15. package/public/js/components/dashboard.js +236 -0
  16. package/public/js/components/logs-viewer.js +100 -0
  17. package/public/js/components/models.js +36 -0
  18. package/public/js/components/server-config.js +349 -0
  19. package/public/js/config/constants.js +102 -0
  20. package/public/js/data-store.js +386 -0
  21. package/public/js/settings-store.js +58 -0
  22. package/public/js/store.js +78 -0
  23. package/public/js/translations/en.js +351 -0
  24. package/public/js/translations/id.js +396 -0
  25. package/public/js/translations/pt.js +287 -0
  26. package/public/js/translations/tr.js +342 -0
  27. package/public/js/translations/zh.js +357 -0
  28. package/public/js/utils/account-actions.js +189 -0
  29. package/public/js/utils/error-handler.js +96 -0
  30. package/public/js/utils/model-config.js +42 -0
  31. package/public/js/utils/validators.js +77 -0
  32. package/public/js/utils.js +69 -0
  33. package/public/views/accounts.html +329 -0
  34. package/public/views/dashboard.html +484 -0
  35. package/public/views/logs.html +97 -0
  36. package/public/views/models.html +331 -0
  37. package/public/views/settings.html +1329 -0
  38. package/src/account-manager/credentials.js +243 -0
  39. package/src/account-manager/index.js +380 -0
  40. package/src/account-manager/onboarding.js +117 -0
  41. package/src/account-manager/rate-limits.js +237 -0
  42. package/src/account-manager/storage.js +136 -0
  43. package/src/account-manager/strategies/base-strategy.js +104 -0
  44. package/src/account-manager/strategies/hybrid-strategy.js +195 -0
  45. package/src/account-manager/strategies/index.js +79 -0
  46. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  47. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  48. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  49. package/src/account-manager/strategies/trackers/index.js +8 -0
  50. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +121 -0
  51. package/src/auth/database.js +169 -0
  52. package/src/auth/oauth.js +419 -0
  53. package/src/auth/token-extractor.js +117 -0
  54. package/src/cli/accounts.js +512 -0
  55. package/src/cli/refresh.js +201 -0
  56. package/src/cli/setup.js +338 -0
  57. package/src/cloudcode/index.js +29 -0
  58. package/src/cloudcode/message-handler.js +386 -0
  59. package/src/cloudcode/model-api.js +248 -0
  60. package/src/cloudcode/rate-limit-parser.js +181 -0
  61. package/src/cloudcode/request-builder.js +93 -0
  62. package/src/cloudcode/session-manager.js +47 -0
  63. package/src/cloudcode/sse-parser.js +121 -0
  64. package/src/cloudcode/sse-streamer.js +293 -0
  65. package/src/cloudcode/streaming-handler.js +492 -0
  66. package/src/config.js +107 -0
  67. package/src/constants.js +278 -0
  68. package/src/errors.js +238 -0
  69. package/src/fallback-config.js +29 -0
  70. package/src/format/content-converter.js +193 -0
  71. package/src/format/index.js +20 -0
  72. package/src/format/request-converter.js +248 -0
  73. package/src/format/response-converter.js +120 -0
  74. package/src/format/schema-sanitizer.js +673 -0
  75. package/src/format/signature-cache.js +88 -0
  76. package/src/format/thinking-utils.js +558 -0
  77. package/src/index.js +146 -0
  78. package/src/modules/usage-stats.js +205 -0
  79. package/src/server.js +861 -0
  80. package/src/utils/claude-config.js +245 -0
  81. package/src/utils/helpers.js +51 -0
  82. package/src/utils/logger.js +142 -0
  83. package/src/utils/native-module-helper.js +162 -0
  84. package/src/webui/index.js +707 -0
@@ -0,0 +1,181 @@
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: Enforce strict minimums for found rate limits
171
+ // If we found a reset time, but it's very small (e.g. < 1s) or negative,
172
+ // explicitly bump it up to avoid "Available in 0s" loops.
173
+ if (resetMs !== null) {
174
+ if (resetMs < 1000) {
175
+ logger.debug(`[CloudCode] Reset time too small (${resetMs}ms), enforcing 2s buffer`);
176
+ resetMs = 2000;
177
+ }
178
+ }
179
+
180
+ return resetMs;
181
+ }
@@ -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: 'antigravity',
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
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * SSE Parser for Cloud Code
3
+ *
4
+ * Parses SSE responses for non-streaming thinking models.
5
+ * Accumulates all parts and returns a single response.
6
+ */
7
+
8
+ import { convertGoogleToAnthropic } from '../format/index.js';
9
+ import { logger } from '../utils/logger.js';
10
+
11
+ /**
12
+ * Parse SSE response for thinking models and accumulate all parts
13
+ *
14
+ * @param {Response} response - The HTTP response with SSE body
15
+ * @param {string} originalModel - The original model name
16
+ * @returns {Promise<Object>} Anthropic-format response object
17
+ */
18
+ export async function parseThinkingSSEResponse(response, originalModel) {
19
+ let accumulatedThinkingText = '';
20
+ let accumulatedThinkingSignature = '';
21
+ let accumulatedText = '';
22
+ const finalParts = [];
23
+ let usageMetadata = {};
24
+ let finishReason = 'STOP';
25
+
26
+ const flushThinking = () => {
27
+ if (accumulatedThinkingText) {
28
+ finalParts.push({
29
+ thought: true,
30
+ text: accumulatedThinkingText,
31
+ thoughtSignature: accumulatedThinkingSignature
32
+ });
33
+ accumulatedThinkingText = '';
34
+ accumulatedThinkingSignature = '';
35
+ }
36
+ };
37
+
38
+ const flushText = () => {
39
+ if (accumulatedText) {
40
+ finalParts.push({ text: accumulatedText });
41
+ accumulatedText = '';
42
+ }
43
+ };
44
+
45
+ const reader = response.body.getReader();
46
+ const decoder = new TextDecoder();
47
+ let buffer = '';
48
+
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+
53
+ buffer += decoder.decode(value, { stream: true });
54
+ const lines = buffer.split('\n');
55
+ buffer = lines.pop() || '';
56
+
57
+ for (const line of lines) {
58
+ if (!line.startsWith('data:')) continue;
59
+ const jsonText = line.slice(5).trim();
60
+ if (!jsonText) continue;
61
+
62
+ try {
63
+ const data = JSON.parse(jsonText);
64
+ const innerResponse = data.response || data;
65
+
66
+ if (innerResponse.usageMetadata) {
67
+ usageMetadata = innerResponse.usageMetadata;
68
+ }
69
+
70
+ const candidates = innerResponse.candidates || [];
71
+ const firstCandidate = candidates[0] || {};
72
+ if (firstCandidate.finishReason) {
73
+ finishReason = firstCandidate.finishReason;
74
+ }
75
+
76
+ const parts = firstCandidate.content?.parts || [];
77
+ for (const part of parts) {
78
+ if (part.thought === true) {
79
+ flushText();
80
+ accumulatedThinkingText += (part.text || '');
81
+ if (part.thoughtSignature) {
82
+ accumulatedThinkingSignature = part.thoughtSignature;
83
+ }
84
+ } else if (part.functionCall) {
85
+ flushThinking();
86
+ flushText();
87
+ finalParts.push(part);
88
+ } else if (part.text !== undefined) {
89
+ if (!part.text) continue;
90
+ flushThinking();
91
+ accumulatedText += part.text;
92
+ } else if (part.inlineData) {
93
+ // Handle image content
94
+ flushThinking();
95
+ flushText();
96
+ finalParts.push(part);
97
+ }
98
+ }
99
+ } catch (e) {
100
+ logger.debug('[CloudCode] SSE parse warning:', e.message, 'Raw:', jsonText.slice(0, 100));
101
+ }
102
+ }
103
+ }
104
+
105
+ flushThinking();
106
+ flushText();
107
+
108
+ const accumulatedResponse = {
109
+ candidates: [{ content: { parts: finalParts }, finishReason }],
110
+ usageMetadata
111
+ };
112
+
113
+ const partTypes = finalParts.map(p => p.thought ? 'thought' : (p.functionCall ? 'functionCall' : (p.inlineData ? 'inlineData' : 'text')));
114
+ logger.debug('[CloudCode] Response received (SSE), part types:', partTypes);
115
+ if (finalParts.some(p => p.thought)) {
116
+ const thinkingPart = finalParts.find(p => p.thought);
117
+ logger.debug('[CloudCode] Thinking signature length:', thinkingPart?.thoughtSignature?.length || 0);
118
+ }
119
+
120
+ return convertGoogleToAnthropic(accumulatedResponse, originalModel);
121
+ }