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
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Sanitizer
|
|
3
|
+
* Cleans and transforms JSON schemas for Gemini/Antigravity API compatibility
|
|
4
|
+
*
|
|
5
|
+
* Uses a multi-phase pipeline matching opencode-cloudcode-auth approach:
|
|
6
|
+
* - Phase 1: Convert $refs to description hints
|
|
7
|
+
* - Phase 2a: Merge allOf schemas
|
|
8
|
+
* - Phase 2b: Flatten anyOf/oneOf (select best option)
|
|
9
|
+
* - Phase 2c: Flatten type arrays + update required for nullable
|
|
10
|
+
* - Phase 3: Remove unsupported keywords
|
|
11
|
+
* - Phase 4: Final cleanup (required array validation)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Append a hint to a schema's description field.
|
|
16
|
+
* Format: "existing (hint)" or just "hint" if no existing description.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} schema - Schema object to modify
|
|
19
|
+
* @param {string} hint - Hint text to append
|
|
20
|
+
* @returns {Object} Modified schema with appended description
|
|
21
|
+
*/
|
|
22
|
+
function appendDescriptionHint(schema, hint) {
|
|
23
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
24
|
+
const result = { ...schema };
|
|
25
|
+
result.description = result.description
|
|
26
|
+
? `${result.description} (${hint})`
|
|
27
|
+
: hint;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Score a schema option for anyOf/oneOf selection.
|
|
33
|
+
* Higher scores = more preferred schemas.
|
|
34
|
+
*
|
|
35
|
+
* @param {Object} schema - Schema option to score
|
|
36
|
+
* @returns {number} Score (0-3)
|
|
37
|
+
*/
|
|
38
|
+
function scoreSchemaOption(schema) {
|
|
39
|
+
if (!schema || typeof schema !== 'object') return 0;
|
|
40
|
+
|
|
41
|
+
// Score 3: Object types with properties (most informative)
|
|
42
|
+
if (schema.type === 'object' || schema.properties) return 3;
|
|
43
|
+
|
|
44
|
+
// Score 2: Array types with items
|
|
45
|
+
if (schema.type === 'array' || schema.items) return 2;
|
|
46
|
+
|
|
47
|
+
// Score 1: Any other non-null type
|
|
48
|
+
if (schema.type && schema.type !== 'null') return 1;
|
|
49
|
+
|
|
50
|
+
// Score 0: Null or no type
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert $ref references to description hints.
|
|
56
|
+
* Replaces { $ref: "#/$defs/Foo" } with { type: "object", description: "See: Foo" }
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} schema - Schema to process
|
|
59
|
+
* @returns {Object} Schema with refs converted to hints
|
|
60
|
+
*/
|
|
61
|
+
function convertRefsToHints(schema) {
|
|
62
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
63
|
+
if (Array.isArray(schema)) return schema.map(convertRefsToHints);
|
|
64
|
+
|
|
65
|
+
const result = { ...schema };
|
|
66
|
+
|
|
67
|
+
// Handle $ref at this level
|
|
68
|
+
if (result.$ref && typeof result.$ref === 'string') {
|
|
69
|
+
// Extract definition name from ref path (e.g., "#/$defs/Foo" -> "Foo")
|
|
70
|
+
const parts = result.$ref.split('/');
|
|
71
|
+
const defName = parts[parts.length - 1] || 'unknown';
|
|
72
|
+
const hint = `See: ${defName}`;
|
|
73
|
+
|
|
74
|
+
// Merge with existing description if present
|
|
75
|
+
const description = result.description
|
|
76
|
+
? `${result.description} (${hint})`
|
|
77
|
+
: hint;
|
|
78
|
+
|
|
79
|
+
// Replace with object type and hint
|
|
80
|
+
return { type: 'object', description };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Recursively process properties
|
|
84
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
85
|
+
result.properties = {};
|
|
86
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
87
|
+
result.properties[key] = convertRefsToHints(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Recursively process items
|
|
92
|
+
if (result.items) {
|
|
93
|
+
if (Array.isArray(result.items)) {
|
|
94
|
+
result.items = result.items.map(convertRefsToHints);
|
|
95
|
+
} else if (typeof result.items === 'object') {
|
|
96
|
+
result.items = convertRefsToHints(result.items);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Recursively process anyOf/oneOf/allOf
|
|
101
|
+
for (const key of ['anyOf', 'oneOf', 'allOf']) {
|
|
102
|
+
if (Array.isArray(result[key])) {
|
|
103
|
+
result[key] = result[key].map(convertRefsToHints);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Merge all schemas in an allOf array into a single schema.
|
|
112
|
+
* Properties and required arrays are merged; other fields use first occurrence.
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} schema - Schema with potential allOf to merge
|
|
115
|
+
* @returns {Object} Schema with allOf merged
|
|
116
|
+
*/
|
|
117
|
+
function mergeAllOf(schema) {
|
|
118
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
119
|
+
if (Array.isArray(schema)) return schema.map(mergeAllOf);
|
|
120
|
+
|
|
121
|
+
let result = { ...schema };
|
|
122
|
+
|
|
123
|
+
// Process allOf if present
|
|
124
|
+
if (Array.isArray(result.allOf) && result.allOf.length > 0) {
|
|
125
|
+
const mergedProperties = {};
|
|
126
|
+
const mergedRequired = new Set();
|
|
127
|
+
const otherFields = {};
|
|
128
|
+
|
|
129
|
+
for (const subSchema of result.allOf) {
|
|
130
|
+
if (!subSchema || typeof subSchema !== 'object') continue;
|
|
131
|
+
|
|
132
|
+
// Merge properties (later overrides earlier)
|
|
133
|
+
if (subSchema.properties) {
|
|
134
|
+
for (const [key, value] of Object.entries(subSchema.properties)) {
|
|
135
|
+
mergedProperties[key] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Union required arrays
|
|
140
|
+
if (Array.isArray(subSchema.required)) {
|
|
141
|
+
for (const req of subSchema.required) {
|
|
142
|
+
mergedRequired.add(req);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Copy other fields (first occurrence wins)
|
|
147
|
+
for (const [key, value] of Object.entries(subSchema)) {
|
|
148
|
+
if (key !== 'properties' && key !== 'required' && !(key in otherFields)) {
|
|
149
|
+
otherFields[key] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Apply merged content
|
|
155
|
+
delete result.allOf;
|
|
156
|
+
|
|
157
|
+
// Merge other fields first (parent takes precedence)
|
|
158
|
+
for (const [key, value] of Object.entries(otherFields)) {
|
|
159
|
+
if (!(key in result)) {
|
|
160
|
+
result[key] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Merge properties (allOf properties override parent for same keys)
|
|
165
|
+
if (Object.keys(mergedProperties).length > 0) {
|
|
166
|
+
result.properties = { ...mergedProperties, ...(result.properties || {}) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Merge required
|
|
170
|
+
if (mergedRequired.size > 0) {
|
|
171
|
+
const parentRequired = Array.isArray(result.required) ? result.required : [];
|
|
172
|
+
result.required = [...new Set([...mergedRequired, ...parentRequired])];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Recursively process properties
|
|
177
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
178
|
+
const newProps = {};
|
|
179
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
180
|
+
newProps[key] = mergeAllOf(value);
|
|
181
|
+
}
|
|
182
|
+
result.properties = newProps;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Recursively process items
|
|
186
|
+
if (result.items) {
|
|
187
|
+
if (Array.isArray(result.items)) {
|
|
188
|
+
result.items = result.items.map(mergeAllOf);
|
|
189
|
+
} else if (typeof result.items === 'object') {
|
|
190
|
+
result.items = mergeAllOf(result.items);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Flatten anyOf/oneOf by selecting the best option based on scoring.
|
|
199
|
+
* Adds type hints to description when multiple types existed.
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} schema - Schema with potential anyOf/oneOf
|
|
202
|
+
* @returns {Object} Flattened schema
|
|
203
|
+
*/
|
|
204
|
+
function flattenAnyOfOneOf(schema) {
|
|
205
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
206
|
+
if (Array.isArray(schema)) return schema.map(flattenAnyOfOneOf);
|
|
207
|
+
|
|
208
|
+
let result = { ...schema };
|
|
209
|
+
|
|
210
|
+
// Handle anyOf or oneOf
|
|
211
|
+
for (const unionKey of ['anyOf', 'oneOf']) {
|
|
212
|
+
if (Array.isArray(result[unionKey]) && result[unionKey].length > 0) {
|
|
213
|
+
const options = result[unionKey];
|
|
214
|
+
|
|
215
|
+
// Collect type names for hint
|
|
216
|
+
const typeNames = [];
|
|
217
|
+
let bestOption = null;
|
|
218
|
+
let bestScore = -1;
|
|
219
|
+
|
|
220
|
+
for (const option of options) {
|
|
221
|
+
if (!option || typeof option !== 'object') continue;
|
|
222
|
+
|
|
223
|
+
// Collect type name
|
|
224
|
+
const typeName = option.type || (option.properties ? 'object' : null);
|
|
225
|
+
if (typeName && typeName !== 'null') {
|
|
226
|
+
typeNames.push(typeName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Score and track best option
|
|
230
|
+
const score = scoreSchemaOption(option);
|
|
231
|
+
if (score > bestScore) {
|
|
232
|
+
bestScore = score;
|
|
233
|
+
bestOption = option;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Remove the union key
|
|
238
|
+
delete result[unionKey];
|
|
239
|
+
|
|
240
|
+
// Merge best option into result
|
|
241
|
+
if (bestOption) {
|
|
242
|
+
// Preserve parent description
|
|
243
|
+
const parentDescription = result.description;
|
|
244
|
+
|
|
245
|
+
// Recursively flatten the best option
|
|
246
|
+
const flattenedOption = flattenAnyOfOneOf(bestOption);
|
|
247
|
+
|
|
248
|
+
// Merge fields from selected option
|
|
249
|
+
for (const [key, value] of Object.entries(flattenedOption)) {
|
|
250
|
+
if (key === 'description') {
|
|
251
|
+
// Merge descriptions if different
|
|
252
|
+
if (value && value !== parentDescription) {
|
|
253
|
+
result.description = parentDescription
|
|
254
|
+
? `${parentDescription} (${value})`
|
|
255
|
+
: value;
|
|
256
|
+
}
|
|
257
|
+
} else if (!(key in result) || key === 'type' || key === 'properties' || key === 'items') {
|
|
258
|
+
result[key] = value;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Add type hint if multiple types existed
|
|
263
|
+
if (typeNames.length > 1) {
|
|
264
|
+
const uniqueTypes = [...new Set(typeNames)];
|
|
265
|
+
result = appendDescriptionHint(result, `Accepts: ${uniqueTypes.join(' | ')}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Recursively process properties
|
|
272
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
273
|
+
const newProps = {};
|
|
274
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
275
|
+
newProps[key] = flattenAnyOfOneOf(value);
|
|
276
|
+
}
|
|
277
|
+
result.properties = newProps;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Recursively process items
|
|
281
|
+
if (result.items) {
|
|
282
|
+
if (Array.isArray(result.items)) {
|
|
283
|
+
result.items = result.items.map(flattenAnyOfOneOf);
|
|
284
|
+
} else if (typeof result.items === 'object') {
|
|
285
|
+
result.items = flattenAnyOfOneOf(result.items);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Enhanced Schema Hints (for preserving semantic information)
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Add hints for enum values (if ≤10 values).
|
|
298
|
+
* This preserves enum information in the description since Gemini
|
|
299
|
+
* may not fully support enums in all cases.
|
|
300
|
+
*
|
|
301
|
+
* @param {Object} schema - Schema to process
|
|
302
|
+
* @returns {Object} Schema with enum hints added to description
|
|
303
|
+
*/
|
|
304
|
+
function addEnumHints(schema) {
|
|
305
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
306
|
+
if (Array.isArray(schema)) return schema.map(addEnumHints);
|
|
307
|
+
|
|
308
|
+
let result = { ...schema };
|
|
309
|
+
|
|
310
|
+
// Add enum hint if present and reasonable size
|
|
311
|
+
if (Array.isArray(result.enum) && result.enum.length > 1 && result.enum.length <= 10) {
|
|
312
|
+
const vals = result.enum.map(v => String(v)).join(', ');
|
|
313
|
+
result = appendDescriptionHint(result, `Allowed: ${vals}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Recursively process properties
|
|
317
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
318
|
+
const newProps = {};
|
|
319
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
320
|
+
newProps[key] = addEnumHints(value);
|
|
321
|
+
}
|
|
322
|
+
result.properties = newProps;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Recursively process items
|
|
326
|
+
if (result.items) {
|
|
327
|
+
result.items = Array.isArray(result.items)
|
|
328
|
+
? result.items.map(addEnumHints)
|
|
329
|
+
: addEnumHints(result.items);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Add hints for additionalProperties: false.
|
|
337
|
+
* This informs the model that extra properties are not allowed.
|
|
338
|
+
*
|
|
339
|
+
* @param {Object} schema - Schema to process
|
|
340
|
+
* @returns {Object} Schema with additionalProperties hints added
|
|
341
|
+
*/
|
|
342
|
+
function addAdditionalPropertiesHints(schema) {
|
|
343
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
344
|
+
if (Array.isArray(schema)) return schema.map(addAdditionalPropertiesHints);
|
|
345
|
+
|
|
346
|
+
let result = { ...schema };
|
|
347
|
+
|
|
348
|
+
if (result.additionalProperties === false) {
|
|
349
|
+
result = appendDescriptionHint(result, 'No extra properties allowed');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Recursively process properties
|
|
353
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
354
|
+
const newProps = {};
|
|
355
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
356
|
+
newProps[key] = addAdditionalPropertiesHints(value);
|
|
357
|
+
}
|
|
358
|
+
result.properties = newProps;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Recursively process items
|
|
362
|
+
if (result.items) {
|
|
363
|
+
result.items = Array.isArray(result.items)
|
|
364
|
+
? result.items.map(addAdditionalPropertiesHints)
|
|
365
|
+
: addAdditionalPropertiesHints(result.items);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Move unsupported constraints to description hints.
|
|
373
|
+
* This preserves constraint information that would otherwise be lost
|
|
374
|
+
* when we strip unsupported keywords.
|
|
375
|
+
*
|
|
376
|
+
* @param {Object} schema - Schema to process
|
|
377
|
+
* @returns {Object} Schema with constraint hints added to description
|
|
378
|
+
*/
|
|
379
|
+
function moveConstraintsToDescription(schema) {
|
|
380
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
381
|
+
if (Array.isArray(schema)) return schema.map(moveConstraintsToDescription);
|
|
382
|
+
|
|
383
|
+
const CONSTRAINTS = ['minLength', 'maxLength', 'pattern', 'minimum', 'maximum',
|
|
384
|
+
'minItems', 'maxItems', 'format'];
|
|
385
|
+
|
|
386
|
+
let result = { ...schema };
|
|
387
|
+
|
|
388
|
+
for (const constraint of CONSTRAINTS) {
|
|
389
|
+
if (result[constraint] !== undefined && typeof result[constraint] !== 'object') {
|
|
390
|
+
result = appendDescriptionHint(result, `${constraint}: ${result[constraint]}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Recursively process properties
|
|
395
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
396
|
+
const newProps = {};
|
|
397
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
398
|
+
newProps[key] = moveConstraintsToDescription(value);
|
|
399
|
+
}
|
|
400
|
+
result.properties = newProps;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Recursively process items
|
|
404
|
+
if (result.items) {
|
|
405
|
+
result.items = Array.isArray(result.items)
|
|
406
|
+
? result.items.map(moveConstraintsToDescription)
|
|
407
|
+
: moveConstraintsToDescription(result.items);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Flatten array type fields and track nullable properties.
|
|
415
|
+
* Converts { type: ["string", "null"] } to { type: "string" } with nullable hint.
|
|
416
|
+
*
|
|
417
|
+
* @param {Object} schema - Schema to process
|
|
418
|
+
* @param {Set<string>} nullableProps - Set to collect nullable property names (mutated)
|
|
419
|
+
* @param {string} currentPropName - Current property name (for tracking)
|
|
420
|
+
* @returns {Object} Flattened schema
|
|
421
|
+
*/
|
|
422
|
+
function flattenTypeArrays(schema, nullableProps = null, currentPropName = null) {
|
|
423
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
424
|
+
if (Array.isArray(schema)) return schema.map(s => flattenTypeArrays(s, nullableProps));
|
|
425
|
+
|
|
426
|
+
let result = { ...schema };
|
|
427
|
+
|
|
428
|
+
// Handle array type fields
|
|
429
|
+
if (Array.isArray(result.type)) {
|
|
430
|
+
const types = result.type;
|
|
431
|
+
const hasNull = types.includes('null');
|
|
432
|
+
const nonNullTypes = types.filter(t => t !== 'null' && t);
|
|
433
|
+
|
|
434
|
+
// Select first non-null type, or 'string' as fallback
|
|
435
|
+
const firstType = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
|
|
436
|
+
result.type = firstType;
|
|
437
|
+
|
|
438
|
+
// Add hint for multiple types
|
|
439
|
+
if (nonNullTypes.length > 1) {
|
|
440
|
+
result = appendDescriptionHint(result, `Accepts: ${nonNullTypes.join(' | ')}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Track nullable and add hint
|
|
444
|
+
if (hasNull) {
|
|
445
|
+
result = appendDescriptionHint(result, 'nullable');
|
|
446
|
+
// Track this property as nullable for required array update
|
|
447
|
+
if (nullableProps && currentPropName) {
|
|
448
|
+
nullableProps.add(currentPropName);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Recursively process properties, tracking nullable ones
|
|
454
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
455
|
+
const childNullableProps = new Set();
|
|
456
|
+
const newProps = {};
|
|
457
|
+
|
|
458
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
459
|
+
newProps[key] = flattenTypeArrays(value, childNullableProps, key);
|
|
460
|
+
}
|
|
461
|
+
result.properties = newProps;
|
|
462
|
+
|
|
463
|
+
// Remove nullable properties from required array
|
|
464
|
+
if (Array.isArray(result.required) && childNullableProps.size > 0) {
|
|
465
|
+
result.required = result.required.filter(prop => !childNullableProps.has(prop));
|
|
466
|
+
if (result.required.length === 0) {
|
|
467
|
+
delete result.required;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Recursively process items
|
|
473
|
+
if (result.items) {
|
|
474
|
+
if (Array.isArray(result.items)) {
|
|
475
|
+
result.items = result.items.map(item => flattenTypeArrays(item, nullableProps));
|
|
476
|
+
} else if (typeof result.items === 'object') {
|
|
477
|
+
result.items = flattenTypeArrays(result.items, nullableProps);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return result;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Sanitize JSON Schema for Antigravity API compatibility.
|
|
486
|
+
* Uses allowlist approach - only permit known-safe JSON Schema features.
|
|
487
|
+
* Converts "const" to equivalent "enum" for compatibility.
|
|
488
|
+
* Generates placeholder schema for empty tool schemas.
|
|
489
|
+
*/
|
|
490
|
+
export function sanitizeSchema(schema) {
|
|
491
|
+
if (!schema || typeof schema !== 'object') {
|
|
492
|
+
// Empty/missing schema - generate placeholder with reason property
|
|
493
|
+
return {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {
|
|
496
|
+
reason: {
|
|
497
|
+
type: 'string',
|
|
498
|
+
description: 'Reason for calling this tool'
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
required: ['reason']
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Allowlist of permitted JSON Schema fields
|
|
506
|
+
const ALLOWED_FIELDS = new Set([
|
|
507
|
+
'type',
|
|
508
|
+
'description',
|
|
509
|
+
'properties',
|
|
510
|
+
'required',
|
|
511
|
+
'items',
|
|
512
|
+
'enum',
|
|
513
|
+
'title'
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
const sanitized = {};
|
|
517
|
+
|
|
518
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
519
|
+
// Convert "const" to "enum" for compatibility
|
|
520
|
+
if (key === 'const') {
|
|
521
|
+
sanitized.enum = [value];
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Skip fields not in allowlist
|
|
526
|
+
if (!ALLOWED_FIELDS.has(key)) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (key === 'properties' && value && typeof value === 'object') {
|
|
531
|
+
sanitized.properties = {};
|
|
532
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
533
|
+
sanitized.properties[propKey] = sanitizeSchema(propValue);
|
|
534
|
+
}
|
|
535
|
+
} else if (key === 'items' && value && typeof value === 'object') {
|
|
536
|
+
if (Array.isArray(value)) {
|
|
537
|
+
sanitized.items = value.map(item => sanitizeSchema(item));
|
|
538
|
+
} else {
|
|
539
|
+
sanitized.items = sanitizeSchema(value);
|
|
540
|
+
}
|
|
541
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
542
|
+
sanitized[key] = sanitizeSchema(value);
|
|
543
|
+
} else {
|
|
544
|
+
sanitized[key] = value;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Ensure we have at least a type
|
|
549
|
+
if (!sanitized.type) {
|
|
550
|
+
sanitized.type = 'object';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// If object type with no properties, add placeholder
|
|
554
|
+
if (sanitized.type === 'object' && (!sanitized.properties || Object.keys(sanitized.properties).length === 0)) {
|
|
555
|
+
sanitized.properties = {
|
|
556
|
+
reason: {
|
|
557
|
+
type: 'string',
|
|
558
|
+
description: 'Reason for calling this tool'
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
sanitized.required = ['reason'];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return sanitized;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Convert JSON Schema type names to Google's Protobuf-style uppercase type names.
|
|
569
|
+
* Google's Generative AI API expects uppercase types: STRING, OBJECT, ARRAY, etc.
|
|
570
|
+
*
|
|
571
|
+
* @param {string} type - JSON Schema type name (lowercase)
|
|
572
|
+
* @returns {string} Google-format type name (uppercase)
|
|
573
|
+
*/
|
|
574
|
+
function toGoogleType(type) {
|
|
575
|
+
if (!type || typeof type !== 'string') return type;
|
|
576
|
+
const typeMap = {
|
|
577
|
+
'string': 'STRING',
|
|
578
|
+
'number': 'NUMBER',
|
|
579
|
+
'integer': 'INTEGER',
|
|
580
|
+
'boolean': 'BOOLEAN',
|
|
581
|
+
'array': 'ARRAY',
|
|
582
|
+
'object': 'OBJECT',
|
|
583
|
+
'null': 'STRING' // Fallback for null type
|
|
584
|
+
};
|
|
585
|
+
return typeMap[type.toLowerCase()] || type.toUpperCase();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Cleans JSON schema for Gemini API compatibility.
|
|
590
|
+
* Uses a multi-phase pipeline matching opencode-cloudcode-auth approach.
|
|
591
|
+
*
|
|
592
|
+
* @param {Object} schema - The JSON schema to clean
|
|
593
|
+
* @returns {Object} Cleaned schema safe for Gemini API
|
|
594
|
+
*/
|
|
595
|
+
export function cleanSchema(schema) {
|
|
596
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
597
|
+
if (Array.isArray(schema)) return schema.map(cleanSchema);
|
|
598
|
+
|
|
599
|
+
// Phase 1: Convert $refs to hints
|
|
600
|
+
let result = convertRefsToHints(schema);
|
|
601
|
+
|
|
602
|
+
// Phase 1b: Add enum hints (preserves enum info in description)
|
|
603
|
+
result = addEnumHints(result);
|
|
604
|
+
|
|
605
|
+
// Phase 1c: Add additionalProperties hints
|
|
606
|
+
result = addAdditionalPropertiesHints(result);
|
|
607
|
+
|
|
608
|
+
// Phase 1d: Move constraints to description (before they get stripped)
|
|
609
|
+
result = moveConstraintsToDescription(result);
|
|
610
|
+
|
|
611
|
+
// Phase 2a: Merge allOf schemas
|
|
612
|
+
result = mergeAllOf(result);
|
|
613
|
+
|
|
614
|
+
// Phase 2b: Flatten anyOf/oneOf
|
|
615
|
+
result = flattenAnyOfOneOf(result);
|
|
616
|
+
|
|
617
|
+
// Phase 2c: Flatten type arrays and update required for nullable
|
|
618
|
+
result = flattenTypeArrays(result);
|
|
619
|
+
|
|
620
|
+
// Phase 3: Remove unsupported keywords
|
|
621
|
+
const unsupported = [
|
|
622
|
+
'additionalProperties', 'default', '$schema', '$defs',
|
|
623
|
+
'definitions', '$ref', '$id', '$comment', 'title',
|
|
624
|
+
'minLength', 'maxLength', 'pattern', 'format',
|
|
625
|
+
'minItems', 'maxItems', 'examples', 'allOf', 'anyOf', 'oneOf'
|
|
626
|
+
];
|
|
627
|
+
|
|
628
|
+
for (const key of unsupported) {
|
|
629
|
+
delete result[key];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Check for unsupported 'format' in string types
|
|
633
|
+
if (result.type === 'string' && result.format) {
|
|
634
|
+
const allowed = ['enum', 'date-time'];
|
|
635
|
+
if (!allowed.includes(result.format)) {
|
|
636
|
+
delete result.format;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Phase 4: Final cleanup - recursively clean nested schemas and validate required
|
|
641
|
+
if (result.properties && typeof result.properties === 'object') {
|
|
642
|
+
const newProps = {};
|
|
643
|
+
for (const [key, value] of Object.entries(result.properties)) {
|
|
644
|
+
newProps[key] = cleanSchema(value);
|
|
645
|
+
}
|
|
646
|
+
result.properties = newProps;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (result.items) {
|
|
650
|
+
if (Array.isArray(result.items)) {
|
|
651
|
+
result.items = result.items.map(cleanSchema);
|
|
652
|
+
} else if (typeof result.items === 'object') {
|
|
653
|
+
result.items = cleanSchema(result.items);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Validate that required array only contains properties that exist
|
|
658
|
+
if (result.required && Array.isArray(result.required) && result.properties) {
|
|
659
|
+
const definedProps = new Set(Object.keys(result.properties));
|
|
660
|
+
result.required = result.required.filter(prop => definedProps.has(prop));
|
|
661
|
+
if (result.required.length === 0) {
|
|
662
|
+
delete result.required;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Phase 5: Convert type to Google's uppercase format (STRING, OBJECT, ARRAY, etc.)
|
|
667
|
+
// Only convert at current level - nested types already converted by recursive cleanSchema calls
|
|
668
|
+
if (result.type && typeof result.type === 'string') {
|
|
669
|
+
result.type = toGoogleType(result.type);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return result;
|
|
673
|
+
}
|