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.
- package/LICENSE +21 -0
- package/README.md +757 -0
- package/bin/cli.js +146 -0
- package/package.json +97 -0
- package/public/Complaint Details.pdf +0 -0
- package/public/Cyber Crime Portal.pdf +0 -0
- package/public/app.js +229 -0
- package/public/css/src/input.css +523 -0
- package/public/css/style.css +1 -0
- package/public/favicon.png +0 -0
- package/public/index.html +549 -0
- package/public/js/components/account-manager.js +356 -0
- package/public/js/components/add-account-modal.js +414 -0
- package/public/js/components/claude-config.js +420 -0
- package/public/js/components/dashboard/charts.js +605 -0
- package/public/js/components/dashboard/filters.js +362 -0
- package/public/js/components/dashboard/stats.js +110 -0
- package/public/js/components/dashboard.js +236 -0
- package/public/js/components/logs-viewer.js +100 -0
- package/public/js/components/models.js +36 -0
- package/public/js/components/server-config.js +349 -0
- package/public/js/config/constants.js +102 -0
- package/public/js/data-store.js +375 -0
- package/public/js/settings-store.js +58 -0
- package/public/js/store.js +99 -0
- package/public/js/translations/en.js +367 -0
- package/public/js/translations/id.js +412 -0
- package/public/js/translations/pt.js +308 -0
- package/public/js/translations/tr.js +358 -0
- package/public/js/translations/zh.js +373 -0
- package/public/js/utils/account-actions.js +189 -0
- package/public/js/utils/error-handler.js +96 -0
- package/public/js/utils/model-config.js +42 -0
- package/public/js/utils/ui-logger.js +143 -0
- package/public/js/utils/validators.js +77 -0
- package/public/js/utils.js +69 -0
- package/public/proxy-server-64.png +0 -0
- package/public/views/accounts.html +361 -0
- package/public/views/dashboard.html +484 -0
- package/public/views/logs.html +97 -0
- package/public/views/models.html +331 -0
- package/public/views/settings.html +1327 -0
- package/src/account-manager/credentials.js +378 -0
- package/src/account-manager/index.js +462 -0
- package/src/account-manager/onboarding.js +112 -0
- package/src/account-manager/rate-limits.js +369 -0
- package/src/account-manager/storage.js +160 -0
- package/src/account-manager/strategies/base-strategy.js +109 -0
- package/src/account-manager/strategies/hybrid-strategy.js +339 -0
- package/src/account-manager/strategies/index.js +79 -0
- package/src/account-manager/strategies/round-robin-strategy.js +76 -0
- package/src/account-manager/strategies/sticky-strategy.js +138 -0
- package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
- package/src/account-manager/strategies/trackers/index.js +9 -0
- package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
- package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
- package/src/auth/database.js +169 -0
- package/src/auth/oauth.js +548 -0
- package/src/auth/token-extractor.js +117 -0
- package/src/cli/accounts.js +648 -0
- package/src/cloudcode/index.js +29 -0
- package/src/cloudcode/message-handler.js +510 -0
- package/src/cloudcode/model-api.js +248 -0
- package/src/cloudcode/rate-limit-parser.js +235 -0
- package/src/cloudcode/request-builder.js +93 -0
- package/src/cloudcode/session-manager.js +47 -0
- package/src/cloudcode/sse-parser.js +121 -0
- package/src/cloudcode/sse-streamer.js +293 -0
- package/src/cloudcode/streaming-handler.js +615 -0
- package/src/config.js +125 -0
- package/src/constants.js +407 -0
- package/src/errors.js +242 -0
- package/src/fallback-config.js +29 -0
- package/src/format/content-converter.js +193 -0
- package/src/format/index.js +20 -0
- package/src/format/request-converter.js +255 -0
- package/src/format/response-converter.js +120 -0
- package/src/format/schema-sanitizer.js +673 -0
- package/src/format/signature-cache.js +88 -0
- package/src/format/thinking-utils.js +648 -0
- package/src/index.js +148 -0
- package/src/modules/usage-stats.js +205 -0
- package/src/providers/anthropic-provider.js +258 -0
- package/src/providers/base-provider.js +157 -0
- package/src/providers/cloudcode.js +94 -0
- package/src/providers/copilot.js +399 -0
- package/src/providers/github-provider.js +287 -0
- package/src/providers/google-provider.js +192 -0
- package/src/providers/index.js +211 -0
- package/src/providers/openai-compatible.js +265 -0
- package/src/providers/openai-provider.js +271 -0
- package/src/providers/openrouter-provider.js +325 -0
- package/src/providers/setup.js +83 -0
- package/src/server.js +870 -0
- package/src/utils/claude-config.js +245 -0
- package/src/utils/helpers.js +51 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/native-module-helper.js +162 -0
- package/src/webui/index.js +1134 -0
package/src/errors.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Provides structured error types for better error handling and classification.
|
|
5
|
+
* Replaces string-based error detection with proper error class checking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base error class for CommonsProxy errors
|
|
10
|
+
*/
|
|
11
|
+
export class CommonsProxyError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} message - Error message
|
|
14
|
+
* @param {string} code - Error code for programmatic handling
|
|
15
|
+
* @param {boolean} retryable - Whether the error is retryable
|
|
16
|
+
* @param {Object} metadata - Additional error metadata
|
|
17
|
+
*/
|
|
18
|
+
constructor(message, code, retryable = false, metadata = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CommonsProxyError';
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.retryable = retryable;
|
|
23
|
+
this.metadata = metadata;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert to JSON for API responses
|
|
28
|
+
*/
|
|
29
|
+
toJSON() {
|
|
30
|
+
return {
|
|
31
|
+
name: this.name,
|
|
32
|
+
code: this.code,
|
|
33
|
+
message: this.message,
|
|
34
|
+
retryable: this.retryable,
|
|
35
|
+
...this.metadata
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Rate limit error (429 / RESOURCE_EXHAUSTED)
|
|
42
|
+
*/
|
|
43
|
+
export class RateLimitError extends CommonsProxyError {
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} message - Error message
|
|
46
|
+
* @param {number|null} resetMs - Time in ms until rate limit resets
|
|
47
|
+
* @param {string} accountEmail - Email of the rate-limited account
|
|
48
|
+
*/
|
|
49
|
+
constructor(message, resetMs = null, accountEmail = null) {
|
|
50
|
+
super(message, 'RATE_LIMITED', true, { resetMs, accountEmail });
|
|
51
|
+
this.name = 'RateLimitError';
|
|
52
|
+
this.resetMs = resetMs;
|
|
53
|
+
this.accountEmail = accountEmail;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Authentication error (invalid credentials, token expired, etc.)
|
|
59
|
+
*/
|
|
60
|
+
export class AuthError extends CommonsProxyError {
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} message - Error message
|
|
63
|
+
* @param {string} accountEmail - Email of the account with auth issues
|
|
64
|
+
* @param {string} reason - Specific reason for auth failure
|
|
65
|
+
*/
|
|
66
|
+
constructor(message, accountEmail = null, reason = null) {
|
|
67
|
+
super(message, 'AUTH_INVALID', false, { accountEmail, reason });
|
|
68
|
+
this.name = 'AuthError';
|
|
69
|
+
this.accountEmail = accountEmail;
|
|
70
|
+
this.reason = reason;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* No accounts available error
|
|
76
|
+
*/
|
|
77
|
+
export class NoAccountsError extends CommonsProxyError {
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} message - Error message
|
|
80
|
+
* @param {boolean} allRateLimited - Whether all accounts are rate limited
|
|
81
|
+
*/
|
|
82
|
+
constructor(message = 'No accounts available', allRateLimited = false) {
|
|
83
|
+
super(message, 'NO_ACCOUNTS', allRateLimited, { allRateLimited });
|
|
84
|
+
this.name = 'NoAccountsError';
|
|
85
|
+
this.allRateLimited = allRateLimited;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Max retries exceeded error
|
|
91
|
+
*/
|
|
92
|
+
export class MaxRetriesError extends CommonsProxyError {
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} message - Error message
|
|
95
|
+
* @param {number} attempts - Number of attempts made
|
|
96
|
+
*/
|
|
97
|
+
constructor(message = 'Max retries exceeded', attempts = 0) {
|
|
98
|
+
super(message, 'MAX_RETRIES', false, { attempts });
|
|
99
|
+
this.name = 'MaxRetriesError';
|
|
100
|
+
this.attempts = attempts;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* API error from upstream service
|
|
106
|
+
*/
|
|
107
|
+
export class ApiError extends CommonsProxyError {
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} message - Error message
|
|
110
|
+
* @param {number} statusCode - HTTP status code
|
|
111
|
+
* @param {string} errorType - Type of API error
|
|
112
|
+
*/
|
|
113
|
+
constructor(message, statusCode = 500, errorType = 'api_error') {
|
|
114
|
+
super(message, errorType.toUpperCase(), statusCode >= 500, { statusCode, errorType });
|
|
115
|
+
this.name = 'ApiError';
|
|
116
|
+
this.statusCode = statusCode;
|
|
117
|
+
this.errorType = errorType;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Native module error (version mismatch, rebuild required)
|
|
123
|
+
*/
|
|
124
|
+
export class NativeModuleError extends CommonsProxyError {
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} message - Error message
|
|
127
|
+
* @param {boolean} rebuildSucceeded - Whether auto-rebuild succeeded
|
|
128
|
+
* @param {boolean} restartRequired - Whether server restart is needed
|
|
129
|
+
*/
|
|
130
|
+
constructor(message, rebuildSucceeded = false, restartRequired = false) {
|
|
131
|
+
super(message, 'NATIVE_MODULE_ERROR', false, { rebuildSucceeded, restartRequired });
|
|
132
|
+
this.name = 'NativeModuleError';
|
|
133
|
+
this.rebuildSucceeded = rebuildSucceeded;
|
|
134
|
+
this.restartRequired = restartRequired;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Empty response error - thrown when API returns no content
|
|
140
|
+
* Used to trigger retry logic in streaming handler
|
|
141
|
+
*/
|
|
142
|
+
export class EmptyResponseError extends CommonsProxyError {
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} message - Error message
|
|
145
|
+
*/
|
|
146
|
+
constructor(message = 'No content received from API') {
|
|
147
|
+
super(message, 'EMPTY_RESPONSE', true, {});
|
|
148
|
+
this.name = 'EmptyResponseError';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Capacity exhausted error - Google's model is at capacity (not user quota)
|
|
154
|
+
* Should retry on same account with shorter delay, not switch accounts immediately
|
|
155
|
+
* Different from QUOTA_EXHAUSTED which indicates user's daily/hourly limit
|
|
156
|
+
*/
|
|
157
|
+
export class CapacityExhaustedError extends CommonsProxyError {
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} message - Error message
|
|
160
|
+
* @param {number|null} retryAfterMs - Suggested retry delay in ms
|
|
161
|
+
*/
|
|
162
|
+
constructor(message = 'Model capacity exhausted', retryAfterMs = null) {
|
|
163
|
+
super(message, 'CAPACITY_EXHAUSTED', true, { retryAfterMs });
|
|
164
|
+
this.name = 'CapacityExhaustedError';
|
|
165
|
+
this.retryAfterMs = retryAfterMs;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if an error is a rate limit error
|
|
171
|
+
* Works with both custom error classes and legacy string-based errors
|
|
172
|
+
* @param {Error} error - Error to check
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
export function isRateLimitError(error) {
|
|
176
|
+
if (error instanceof RateLimitError) return true;
|
|
177
|
+
const msg = (error.message || '').toLowerCase();
|
|
178
|
+
return msg.includes('429') ||
|
|
179
|
+
msg.includes('resource_exhausted') ||
|
|
180
|
+
msg.includes('quota_exhausted') ||
|
|
181
|
+
msg.includes('rate limit');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if an error is an authentication error
|
|
186
|
+
* Works with both custom error classes and legacy string-based errors
|
|
187
|
+
* @param {Error} error - Error to check
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
export function isAuthError(error) {
|
|
191
|
+
if (error instanceof AuthError) return true;
|
|
192
|
+
const msg = (error.message || '').toUpperCase();
|
|
193
|
+
return msg.includes('AUTH_INVALID') ||
|
|
194
|
+
msg.includes('INVALID_GRANT') ||
|
|
195
|
+
msg.includes('TOKEN REFRESH FAILED');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if an error is an empty response error
|
|
200
|
+
* @param {Error} error - Error to check
|
|
201
|
+
* @returns {boolean}
|
|
202
|
+
*/
|
|
203
|
+
export function isEmptyResponseError(error) {
|
|
204
|
+
return error instanceof EmptyResponseError ||
|
|
205
|
+
error?.name === 'EmptyResponseError';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if an error is a capacity exhausted error (model overload, not user quota)
|
|
210
|
+
* This is different from quota exhaustion - capacity issues are temporary infrastructure
|
|
211
|
+
* limits that should be retried on the SAME account with shorter delays
|
|
212
|
+
* @param {Error} error - Error to check
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
export function isCapacityExhaustedError(error) {
|
|
216
|
+
if (error instanceof CapacityExhaustedError) return true;
|
|
217
|
+
const msg = (error.message || '').toLowerCase();
|
|
218
|
+
return msg.includes('model_capacity_exhausted') ||
|
|
219
|
+
msg.includes('capacity_exhausted') ||
|
|
220
|
+
msg.includes('model is currently overloaded') ||
|
|
221
|
+
msg.includes('service temporarily unavailable');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Legacy alias for backward compatibility
|
|
225
|
+
export const AntigravityError = CommonsProxyError;
|
|
226
|
+
|
|
227
|
+
export default {
|
|
228
|
+
CommonsProxyError,
|
|
229
|
+
AntigravityError, // Legacy alias
|
|
230
|
+
RateLimitError,
|
|
231
|
+
AuthError,
|
|
232
|
+
NoAccountsError,
|
|
233
|
+
MaxRetriesError,
|
|
234
|
+
ApiError,
|
|
235
|
+
NativeModuleError,
|
|
236
|
+
EmptyResponseError,
|
|
237
|
+
CapacityExhaustedError,
|
|
238
|
+
isRateLimitError,
|
|
239
|
+
isAuthError,
|
|
240
|
+
isEmptyResponseError,
|
|
241
|
+
isCapacityExhaustedError
|
|
242
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Fallback Configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines fallback mappings for when a model's quota is exhausted across all accounts.
|
|
5
|
+
* Enables graceful degradation to alternative models with similar capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MODEL_FALLBACK_MAP } from './constants.js';
|
|
9
|
+
|
|
10
|
+
// Re-export for convenience
|
|
11
|
+
export { MODEL_FALLBACK_MAP };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get fallback model for a given model ID
|
|
15
|
+
* @param {string} model - Primary model ID
|
|
16
|
+
* @returns {string|null} Fallback model ID or null if no fallback exists
|
|
17
|
+
*/
|
|
18
|
+
export function getFallbackModel(model) {
|
|
19
|
+
return MODEL_FALLBACK_MAP[model] || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a model has a fallback configured
|
|
24
|
+
* @param {string} model - Model ID to check
|
|
25
|
+
* @returns {boolean} True if fallback exists
|
|
26
|
+
*/
|
|
27
|
+
export function hasFallback(model) {
|
|
28
|
+
return model in MODEL_FALLBACK_MAP;
|
|
29
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Converter
|
|
3
|
+
* Converts Anthropic message content to Google Generative AI parts format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MIN_SIGNATURE_LENGTH, GEMINI_SKIP_SIGNATURE } from '../constants.js';
|
|
7
|
+
import { getCachedSignature, getCachedSignatureFamily } from './signature-cache.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert Anthropic role to Google role
|
|
12
|
+
* @param {string} role - Anthropic role ('user', 'assistant')
|
|
13
|
+
* @returns {string} Google role ('user', 'model')
|
|
14
|
+
*/
|
|
15
|
+
export function convertRole(role) {
|
|
16
|
+
if (role === 'assistant') return 'model';
|
|
17
|
+
if (role === 'user') return 'user';
|
|
18
|
+
return 'user'; // Default to user
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert Anthropic message content to Google Generative AI parts
|
|
23
|
+
* @param {string|Array} content - Anthropic message content
|
|
24
|
+
* @param {boolean} isClaudeModel - Whether the model is a Claude model
|
|
25
|
+
* @param {boolean} isGeminiModel - Whether the model is a Gemini model
|
|
26
|
+
* @returns {Array} Google Generative AI parts array
|
|
27
|
+
*/
|
|
28
|
+
export function convertContentToParts(content, isClaudeModel = false, isGeminiModel = false) {
|
|
29
|
+
if (typeof content === 'string') {
|
|
30
|
+
return [{ text: content }];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(content)) {
|
|
34
|
+
return [{ text: String(content) }];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parts = [];
|
|
38
|
+
const deferredInlineData = []; // Collect inlineData to add at the end (Issue #91)
|
|
39
|
+
|
|
40
|
+
for (const block of content) {
|
|
41
|
+
if (!block) continue;
|
|
42
|
+
|
|
43
|
+
if (block.type === 'text') {
|
|
44
|
+
// Skip empty text blocks - they cause API errors
|
|
45
|
+
if (block.text && block.text.trim()) {
|
|
46
|
+
parts.push({ text: block.text });
|
|
47
|
+
}
|
|
48
|
+
} else if (block.type === 'image') {
|
|
49
|
+
// Handle image content
|
|
50
|
+
if (block.source?.type === 'base64') {
|
|
51
|
+
// Base64-encoded image
|
|
52
|
+
parts.push({
|
|
53
|
+
inlineData: {
|
|
54
|
+
mimeType: block.source.media_type,
|
|
55
|
+
data: block.source.data
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
} else if (block.source?.type === 'url') {
|
|
59
|
+
// URL-referenced image
|
|
60
|
+
parts.push({
|
|
61
|
+
fileData: {
|
|
62
|
+
mimeType: block.source.media_type || 'image/jpeg',
|
|
63
|
+
fileUri: block.source.url
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} else if (block.type === 'document') {
|
|
68
|
+
// Handle document content (e.g. PDF)
|
|
69
|
+
if (block.source?.type === 'base64') {
|
|
70
|
+
parts.push({
|
|
71
|
+
inlineData: {
|
|
72
|
+
mimeType: block.source.media_type,
|
|
73
|
+
data: block.source.data
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
} else if (block.source?.type === 'url') {
|
|
77
|
+
parts.push({
|
|
78
|
+
fileData: {
|
|
79
|
+
mimeType: block.source.media_type || 'application/pdf',
|
|
80
|
+
fileUri: block.source.url
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} else if (block.type === 'tool_use') {
|
|
85
|
+
// Convert tool_use to functionCall (Google format)
|
|
86
|
+
// For Claude models, include the id field
|
|
87
|
+
const functionCall = {
|
|
88
|
+
name: block.name,
|
|
89
|
+
args: block.input || {}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (isClaudeModel && block.id) {
|
|
93
|
+
functionCall.id = block.id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build the part with functionCall
|
|
97
|
+
const part = { functionCall };
|
|
98
|
+
|
|
99
|
+
// For Gemini models, include thoughtSignature at the part level
|
|
100
|
+
// This is required by Gemini 3+ for tool calls to work correctly
|
|
101
|
+
if (isGeminiModel) {
|
|
102
|
+
// Priority: block.thoughtSignature > cache > GEMINI_SKIP_SIGNATURE
|
|
103
|
+
let signature = block.thoughtSignature;
|
|
104
|
+
|
|
105
|
+
if (!signature && block.id) {
|
|
106
|
+
signature = getCachedSignature(block.id);
|
|
107
|
+
if (signature) {
|
|
108
|
+
logger.debug(`[ContentConverter] Restored signature from cache for: ${block.id}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
part.thoughtSignature = signature || GEMINI_SKIP_SIGNATURE;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
parts.push(part);
|
|
116
|
+
} else if (block.type === 'tool_result') {
|
|
117
|
+
// Convert tool_result to functionResponse (Google format)
|
|
118
|
+
let responseContent = block.content;
|
|
119
|
+
let imageParts = [];
|
|
120
|
+
|
|
121
|
+
if (typeof responseContent === 'string') {
|
|
122
|
+
responseContent = { result: responseContent };
|
|
123
|
+
} else if (Array.isArray(responseContent)) {
|
|
124
|
+
// Extract images from tool results first (e.g., from Read tool reading image files)
|
|
125
|
+
for (const item of responseContent) {
|
|
126
|
+
if (item.type === 'image' && item.source?.type === 'base64') {
|
|
127
|
+
imageParts.push({
|
|
128
|
+
inlineData: {
|
|
129
|
+
mimeType: item.source.media_type,
|
|
130
|
+
data: item.source.data
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract text content
|
|
137
|
+
const texts = responseContent
|
|
138
|
+
.filter(c => c.type === 'text')
|
|
139
|
+
.map(c => c.text)
|
|
140
|
+
.join('\n');
|
|
141
|
+
responseContent = { result: texts || (imageParts.length > 0 ? 'Image attached' : '') };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const functionResponse = {
|
|
145
|
+
name: block.tool_use_id || 'unknown',
|
|
146
|
+
response: responseContent
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// For Claude models, the id field must match the tool_use_id
|
|
150
|
+
if (isClaudeModel && block.tool_use_id) {
|
|
151
|
+
functionResponse.id = block.tool_use_id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
parts.push({ functionResponse });
|
|
155
|
+
|
|
156
|
+
// Defer images from the tool result to end of parts array (Issue #91)
|
|
157
|
+
// This ensures all functionResponse parts are consecutive
|
|
158
|
+
deferredInlineData.push(...imageParts);
|
|
159
|
+
} else if (block.type === 'thinking') {
|
|
160
|
+
// Handle thinking blocks with signature compatibility check
|
|
161
|
+
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
|
|
162
|
+
const signatureFamily = getCachedSignatureFamily(block.signature);
|
|
163
|
+
const targetFamily = isClaudeModel ? 'claude' : isGeminiModel ? 'gemini' : null;
|
|
164
|
+
|
|
165
|
+
// Drop blocks with incompatible signatures for Gemini (cross-model switch)
|
|
166
|
+
if (isGeminiModel && signatureFamily && targetFamily && signatureFamily !== targetFamily) {
|
|
167
|
+
logger.debug(`[ContentConverter] Dropping incompatible ${signatureFamily} thinking for ${targetFamily} model`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Drop blocks with unknown signature origin for Gemini (cold cache - safe default)
|
|
172
|
+
if (isGeminiModel && !signatureFamily && targetFamily) {
|
|
173
|
+
logger.debug(`[ContentConverter] Dropping thinking with unknown signature origin`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Compatible - convert to Gemini format with signature
|
|
178
|
+
parts.push({
|
|
179
|
+
text: block.thinking,
|
|
180
|
+
thought: true,
|
|
181
|
+
thoughtSignature: block.signature
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Unsigned thinking blocks are dropped (existing behavior)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add deferred inlineData at the end (Issue #91)
|
|
189
|
+
// This ensures functionResponse parts are consecutive, which Claude's API requires
|
|
190
|
+
parts.push(...deferredInlineData);
|
|
191
|
+
|
|
192
|
+
return parts;
|
|
193
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Converter Module
|
|
3
|
+
* Converts between Anthropic Messages API format and Google Generative AI format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Re-export all from each module
|
|
7
|
+
export * from './request-converter.js';
|
|
8
|
+
export * from './response-converter.js';
|
|
9
|
+
export * from './content-converter.js';
|
|
10
|
+
export * from './schema-sanitizer.js';
|
|
11
|
+
export * from './thinking-utils.js';
|
|
12
|
+
|
|
13
|
+
// Default export for backward compatibility
|
|
14
|
+
import { convertAnthropicToGoogle } from './request-converter.js';
|
|
15
|
+
import { convertGoogleToAnthropic } from './response-converter.js';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
convertAnthropicToGoogle,
|
|
19
|
+
convertGoogleToAnthropic
|
|
20
|
+
};
|