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,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
+ }