@jsonstudio/llms 0.6.1397 → 0.6.1402

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 (72) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +1 -3
  2. package/dist/conversion/codecs/gemini-openai-codec.js +4 -10
  3. package/dist/conversion/compat/actions/gemini-cli-request.d.ts +2 -0
  4. package/dist/conversion/compat/actions/gemini-cli-request.js +490 -0
  5. package/dist/conversion/compat/profiles/chat-gemini-cli.json +27 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +76 -348
  7. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  8. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  9. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +95 -3
  10. package/dist/conversion/hub/pipeline/hub-pipeline.js +1365 -19
  11. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +22 -0
  12. package/dist/conversion/hub/policy/policy-engine.js +50 -3
  13. package/dist/conversion/hub/process/chat-process.js +5 -146
  14. package/dist/conversion/hub/response/provider-response.js +11 -10
  15. package/dist/conversion/hub/response/response-mappers.d.ts +1 -3
  16. package/dist/conversion/hub/response/response-mappers.js +2 -20
  17. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +2 -1
  18. package/dist/conversion/responses/responses-openai-bridge.js +4 -3
  19. package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -6
  20. package/dist/conversion/shared/gemini-tool-utils.js +164 -542
  21. package/dist/conversion/shared/mcp-injection.js +29 -0
  22. package/dist/conversion/shared/openai-message-normalize.js +3 -17
  23. package/dist/filters/special/request-tool-list-filter.js +21 -13
  24. package/dist/filters/special/tool-filter-hooks.js +60 -3
  25. package/dist/router/virtual-router/bootstrap.js +8 -6
  26. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  27. package/dist/router/virtual-router/engine-health.js +110 -11
  28. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +0 -15
  29. package/dist/router/virtual-router/engine-selection/alias-selection.js +4 -85
  30. package/dist/router/virtual-router/engine-selection/route-utils.js +6 -12
  31. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +17 -40
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -5
  33. package/dist/router/virtual-router/engine.js +6 -21
  34. package/dist/router/virtual-router/types.d.ts +1 -14
  35. package/dist/servertool/clock/config.d.ts +1 -1
  36. package/dist/servertool/clock/config.js +5 -9
  37. package/dist/servertool/engine.js +6 -83
  38. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -2
  39. package/dist/sse/sse-to-json/builders/response-builder.js +0 -16
  40. package/package.json +1 -1
  41. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +0 -10
  42. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +0 -142
  43. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +0 -6
  44. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +0 -79
  45. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +0 -3
  46. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +0 -46
  47. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +0 -8
  48. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +0 -366
  49. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +0 -9
  50. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +0 -390
  51. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +0 -3
  52. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +0 -14
  53. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +0 -2
  54. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +0 -144
  55. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +0 -4
  56. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +0 -32
  57. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +0 -8
  58. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +0 -63
  59. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +0 -2
  60. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +0 -43
  61. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +0 -1
  62. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +0 -29
  63. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +0 -2
  64. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +0 -16
  65. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +0 -116
  66. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +0 -1
  67. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +0 -10
  68. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +0 -172
  69. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +0 -10
  70. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +0 -71
  71. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +0 -14
  72. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +0 -289
@@ -2,545 +2,145 @@ import { jsonClone } from '../hub/types/json.js';
2
2
  function isPlainRecord(value) {
3
3
  return !!value && typeof value === 'object' && !Array.isArray(value);
4
4
  }
5
- const GEMINI_FUNCTION_NAME_SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
- export function isGeminiFunctionNameSafe(name) {
7
- return typeof name === 'string' && GEMINI_FUNCTION_NAME_SAFE.test(name);
8
- }
9
- export function sanitizeGeminiFunctionName(name, options) {
10
- const raw = typeof name === 'string' ? name : '';
11
- const maxLen = typeof options?.maxLen === 'number' && Number.isFinite(options.maxLen) && options.maxLen > 8
12
- ? Math.floor(options.maxLen)
13
- : 64;
14
- // Preserve existing underscores (many tool namespaces use double-underscores as separators).
15
- // Only replace invalid characters.
16
- const replaced = raw.replace(/[^A-Za-z0-9_]/g, '_');
17
- const prefixed = /^[A-Za-z_]/.test(replaced) ? replaced : `_${replaced}`;
18
- const sliced = prefixed.length > maxLen ? prefixed.slice(0, maxLen) : prefixed;
19
- return sliced || '_tool';
20
- }
21
- // Align with opencode-antigravity-auth gcli2api-style schema transform (toGeminiSchema):
22
- // Gemini/Antigravity strict protobuf-backed validation rejects these JSON Schema keywords.
23
- const UNSUPPORTED_KEYWORDS = new Set([
24
- 'additionalProperties',
25
- '$schema',
26
- '$id',
27
- '$comment',
28
- '$ref',
29
- '$defs',
30
- 'definitions',
31
- 'const',
32
- 'contentMediaType',
33
- 'contentEncoding',
34
- 'if',
35
- 'then',
36
- 'else',
37
- 'not',
38
- 'patternProperties',
39
- 'unevaluatedProperties',
40
- 'unevaluatedItems',
41
- 'dependentRequired',
42
- 'dependentSchemas',
43
- 'propertyNames',
44
- 'minContains',
45
- 'maxContains'
46
- ]);
47
- const EMPTY_SCHEMA_PLACEHOLDER_NAME = '_placeholder';
48
- const EMPTY_SCHEMA_PLACEHOLDER_DESCRIPTION = 'Placeholder. Always pass true.';
49
- function appendDescriptionHint(schema, hint) {
50
- const existing = typeof schema.description === 'string' ? schema.description : '';
51
- const description = existing ? `${existing} (${hint})` : hint;
52
- return { ...schema, description };
53
- }
54
- function convertRefsToHints(schema) {
55
- if (!schema || typeof schema !== 'object') {
56
- return schema;
57
- }
58
- if (Array.isArray(schema)) {
59
- return schema.map((entry) => convertRefsToHints(entry));
60
- }
61
- const record = schema;
62
- if (typeof record.$ref === 'string') {
63
- const refVal = record.$ref;
64
- const defName = refVal.includes('/') ? refVal.split('/').pop() : refVal;
65
- const hint = `See: ${defName || refVal}`;
66
- const base = { type: 'object' };
67
- if (typeof record.description === 'string' && record.description.trim()) {
68
- base.description = `${record.description} (${hint})`;
69
- }
70
- else {
71
- base.description = hint;
5
+ function pickBestSchemaVariant(variants) {
6
+ if (!Array.isArray(variants) || variants.length === 0) {
7
+ return { type: 'object', properties: {} };
8
+ }
9
+ const score = (value) => {
10
+ if (!isPlainRecord(value)) {
11
+ return 0;
12
+ }
13
+ const type = typeof value.type === 'string' ? value.type.trim().toLowerCase() : '';
14
+ // Prefer 'string' to maximize compatibility (can carry arbitrary serialized content),
15
+ // then object/array, then primitives.
16
+ if (type === 'string')
17
+ return 100;
18
+ if (type === 'object')
19
+ return 80;
20
+ if (type === 'array')
21
+ return 60;
22
+ if (type === 'integer' || type === 'number' || type === 'boolean')
23
+ return 50;
24
+ return 10;
25
+ };
26
+ let best = variants[0];
27
+ let bestScore = score(best);
28
+ for (const candidate of variants.slice(1)) {
29
+ const candidateScore = score(candidate);
30
+ if (candidateScore > bestScore) {
31
+ best = candidate;
32
+ bestScore = candidateScore;
72
33
  }
73
- return base;
74
- }
75
- const out = {};
76
- for (const [key, value] of Object.entries(record)) {
77
- out[key] = convertRefsToHints(value);
78
34
  }
79
- return out;
35
+ return best;
80
36
  }
81
- function convertConstToEnum(schema) {
82
- if (!schema || typeof schema !== 'object') {
83
- return schema;
37
+ function normalizeSchemaTypes(value) {
38
+ if (!value || typeof value !== 'object') {
39
+ return value;
84
40
  }
85
- if (Array.isArray(schema)) {
86
- return schema.map((entry) => convertConstToEnum(entry));
41
+ if (Array.isArray(value)) {
42
+ return value.map((entry) => normalizeSchemaTypes(entry));
87
43
  }
88
- const record = schema;
44
+ const record = value;
89
45
  const out = {};
90
- for (const [key, value] of Object.entries(record)) {
91
- if (key === 'const' && record.enum === undefined) {
92
- out.enum = [convertConstToEnum(value)];
46
+ for (const [key, val] of Object.entries(record)) {
47
+ if (key === 'type' && typeof val === 'string') {
48
+ out[key] = val.toUpperCase();
93
49
  continue;
94
50
  }
95
- out[key] = convertConstToEnum(value);
96
- }
97
- return out;
98
- }
99
- function addEnumHints(schema) {
100
- if (!schema || typeof schema !== 'object') {
101
- return schema;
102
- }
103
- if (Array.isArray(schema)) {
104
- return schema.map((entry) => addEnumHints(entry));
105
- }
106
- const record = schema;
107
- let out = { ...record };
108
- if (Array.isArray(record.enum) && record.enum.length > 1 && record.enum.length <= 10) {
109
- const vals = record.enum.map((v) => String(v)).join(', ');
110
- out = appendDescriptionHint(out, `Allowed: ${vals}`);
111
- }
112
- for (const [key, value] of Object.entries(out)) {
113
- if (key === 'enum')
51
+ if ((key === 'properties' || key === 'items') && val && typeof val === 'object') {
52
+ out[key] = normalizeSchemaTypes(val);
114
53
  continue;
115
- if (value && typeof value === 'object') {
116
- out[key] = addEnumHints(value);
117
54
  }
118
- }
119
- return out;
120
- }
121
- function addAdditionalPropertiesHints(schema) {
122
- if (!schema || typeof schema !== 'object') {
123
- return schema;
124
- }
125
- if (Array.isArray(schema)) {
126
- return schema.map((entry) => addAdditionalPropertiesHints(entry));
127
- }
128
- const record = schema;
129
- let out = { ...record };
130
- if (record.additionalProperties === false) {
131
- out = appendDescriptionHint(out, 'No extra properties allowed');
132
- }
133
- for (const [key, value] of Object.entries(out)) {
134
- if (key === 'additionalProperties')
55
+ if ((key === 'anyOf' || key === 'oneOf' || key === 'allOf') && Array.isArray(val)) {
56
+ out[key] = val.map((entry) => normalizeSchemaTypes(entry));
135
57
  continue;
136
- if (value && typeof value === 'object') {
137
- out[key] = addAdditionalPropertiesHints(value);
138
58
  }
59
+ out[key] = normalizeSchemaTypes(val);
139
60
  }
140
61
  return out;
141
62
  }
142
- // Note: we intentionally do not rewrite schemas into description hints (unlike earlier variants),
143
- // because Gemini/Cloud Code may compare tool schemas across turns; keep transformation minimal and deterministic.
144
- function mergeAllOf(schema) {
145
- if (!schema || typeof schema !== 'object') {
146
- return schema;
147
- }
148
- if (Array.isArray(schema)) {
149
- return schema.map((entry) => mergeAllOf(entry));
63
+ function cloneParameters(value) {
64
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
65
+ return value;
150
66
  }
151
- const record = schema;
152
- let out = { ...record };
153
- const allOf = out.allOf;
154
- if (Array.isArray(allOf) && allOf.length > 0) {
155
- const merged = {};
156
- const mergedRequired = [];
157
- for (const item of allOf) {
158
- if (!isPlainRecord(item))
159
- continue;
160
- if (isPlainRecord(item.properties)) {
161
- merged.properties = { ...merged.properties, ...item.properties };
162
- }
163
- if (Array.isArray(item.required)) {
164
- for (const req of item.required) {
165
- if (typeof req === 'string' && !mergedRequired.includes(req)) {
166
- mergedRequired.push(req);
167
- }
168
- }
169
- }
170
- for (const [key, value] of Object.entries(item)) {
171
- if (key === 'properties' || key === 'required')
67
+ if (Array.isArray(value)) {
68
+ return value.map((entry) => cloneParameters(entry));
69
+ }
70
+ if (isPlainRecord(value)) {
71
+ // Normalize JSON Schema const enum to improve compatibility with functionDeclarations.parameters.
72
+ // Some upstreams reject `const` but accept `enum`.
73
+ if (Object.prototype.hasOwnProperty.call(value, 'const') && !Object.prototype.hasOwnProperty.call(value, 'enum')) {
74
+ const cloned = {};
75
+ for (const [key, entry] of Object.entries(value)) {
76
+ if (key === 'const') {
77
+ cloned.enum = [cloneParameters(entry)];
172
78
  continue;
173
- if (merged[key] === undefined)
174
- merged[key] = value;
175
- }
176
- }
177
- delete out.allOf;
178
- if (merged.properties) {
179
- out.properties = { ...(isPlainRecord(out.properties) ? out.properties : {}), ...merged.properties };
180
- }
181
- if (mergedRequired.length > 0) {
182
- const existingRequired = Array.isArray(out.required) ? out.required.filter((r) => typeof r === 'string') : [];
183
- out.required = Array.from(new Set([...existingRequired, ...mergedRequired]));
184
- }
185
- for (const [key, value] of Object.entries(merged)) {
186
- if (key === 'properties' || key === 'required')
187
- continue;
188
- if (out[key] === undefined)
189
- out[key] = value;
190
- }
191
- }
192
- for (const [key, value] of Object.entries(out)) {
193
- if (value && typeof value === 'object') {
194
- out[key] = mergeAllOf(value);
195
- }
196
- }
197
- return out;
198
- }
199
- function tryMergeEnumFromUnion(options) {
200
- if (!Array.isArray(options) || options.length === 0)
201
- return null;
202
- const enumValues = [];
203
- for (const option of options) {
204
- if (!isPlainRecord(option))
205
- return null;
206
- if (option.const !== undefined) {
207
- enumValues.push(option.const);
208
- continue;
209
- }
210
- if (Array.isArray(option.enum) && option.enum.length > 0) {
211
- enumValues.push(...option.enum);
212
- continue;
213
- }
214
- if (option.properties || option.items || option.anyOf || option.oneOf || option.allOf) {
215
- return null;
216
- }
217
- if (typeof option.type === 'string' && option.type) {
218
- return null;
219
- }
220
- return null;
221
- }
222
- return enumValues.length > 0 ? enumValues : null;
223
- }
224
- function scoreSchemaOption(schema) {
225
- if (!isPlainRecord(schema))
226
- return { score: 0, typeName: 'unknown' };
227
- const type = typeof schema.type === 'string' ? schema.type.toLowerCase() : '';
228
- // Prefer 'string' to maximize compatibility (can carry arbitrary serialized content),
229
- // then object/array, then primitives. This mirrors our historical Gemini tool behavior
230
- // and avoids breaking tools expecting string args (e.g. cmd/command).
231
- if (type === 'string')
232
- return { score: 4, typeName: 'string' };
233
- if (type === 'object' || schema.properties)
234
- return { score: 3, typeName: 'object' };
235
- if (type === 'array' || schema.items)
236
- return { score: 2, typeName: 'array' };
237
- if (type && type !== 'null')
238
- return { score: 1, typeName: type };
239
- return { score: 0, typeName: type || 'null' };
240
- }
241
- function flattenAnyOfOneOf(schema) {
242
- if (!schema || typeof schema !== 'object') {
243
- return schema;
244
- }
245
- if (Array.isArray(schema)) {
246
- return schema.map((entry) => flattenAnyOfOneOf(entry));
247
- }
248
- let out = { ...schema };
249
- for (const unionKey of ['anyOf', 'oneOf']) {
250
- const options = out[unionKey];
251
- if (Array.isArray(options) && options.length > 0) {
252
- const parentDesc = typeof out.description === 'string' ? out.description : '';
253
- const mergedEnum = tryMergeEnumFromUnion(options);
254
- if (mergedEnum) {
255
- const next = { ...out };
256
- delete next[unionKey];
257
- next.type = 'string';
258
- next.enum = mergedEnum;
259
- if (parentDesc)
260
- next.description = parentDesc;
261
- out = next;
262
- continue;
263
- }
264
- let bestIdx = 0;
265
- let bestScore = -1;
266
- const allTypes = [];
267
- for (let i = 0; i < options.length; i++) {
268
- const { score, typeName } = scoreSchemaOption(options[i]);
269
- allTypes.push(typeName);
270
- if (score > bestScore) {
271
- bestScore = score;
272
- bestIdx = i;
273
- }
274
- }
275
- let selected = flattenAnyOfOneOf(options[bestIdx]) || { type: 'string' };
276
- if (isPlainRecord(selected)) {
277
- let selectedRecord = selected;
278
- if (parentDesc) {
279
- const childDesc = typeof selectedRecord.description === 'string' ? selectedRecord.description : '';
280
- selectedRecord = childDesc && childDesc !== parentDesc
281
- ? { ...selectedRecord, description: `${parentDesc} (${childDesc})` }
282
- : childDesc
283
- ? selectedRecord
284
- : { ...selectedRecord, description: parentDesc };
285
- }
286
- const uniqueTypes = Array.from(new Set(allTypes.filter(Boolean)));
287
- if (uniqueTypes.length > 1) {
288
- selectedRecord = appendDescriptionHint(selectedRecord, `Accepts: ${uniqueTypes.join(' | ')}`);
289
- }
290
- const next = { ...out };
291
- delete next[unionKey];
292
- delete next.description;
293
- out = { ...next, ...selectedRecord };
294
- }
295
- else {
296
- delete out[unionKey];
297
- }
298
- }
299
- }
300
- for (const [key, value] of Object.entries(out)) {
301
- if (value && typeof value === 'object') {
302
- out[key] = flattenAnyOfOneOf(value);
303
- }
304
- }
305
- return out;
306
- }
307
- function flattenTypeArrays(schema, nullableFields, currentPath) {
308
- if (!schema || typeof schema !== 'object') {
309
- return schema;
310
- }
311
- if (Array.isArray(schema)) {
312
- return schema.map((entry, idx) => flattenTypeArrays(entry, nullableFields, `${currentPath || ''}[${idx}]`));
313
- }
314
- let out = { ...schema };
315
- const localNullableFields = nullableFields || new Map();
316
- if (Array.isArray(out.type)) {
317
- const types = out.type.filter((t) => typeof t === 'string' && t.trim().length > 0);
318
- const hasNull = types.some((t) => t === 'null');
319
- const nonNull = types.filter((t) => t !== 'null');
320
- const first = nonNull.length > 0 ? nonNull[0] : 'string';
321
- out.type = first;
322
- if (nonNull.length > 1) {
323
- out = appendDescriptionHint(out, `Accepts: ${Array.from(new Set(nonNull)).join(' | ')}`);
324
- }
325
- if (hasNull) {
326
- out = appendDescriptionHint(out, 'nullable');
327
- }
328
- }
329
- if (isPlainRecord(out.properties)) {
330
- const nextProps = {};
331
- for (const [propKey, propValue] of Object.entries(out.properties)) {
332
- const propPath = currentPath ? `${currentPath}.properties.${propKey}` : `properties.${propKey}`;
333
- const processed = flattenTypeArrays(propValue, localNullableFields, propPath);
334
- nextProps[propKey] = processed;
335
- if (isPlainRecord(processed) &&
336
- typeof processed.description === 'string' &&
337
- processed.description.includes('nullable')) {
338
- const objectPath = currentPath || '';
339
- const existing = localNullableFields.get(objectPath) || [];
340
- existing.push(propKey);
341
- localNullableFields.set(objectPath, existing);
342
- }
343
- }
344
- out.properties = nextProps;
345
- }
346
- // Remove nullable fields from required array (root-level cleanup, aligned with opencode).
347
- if (Array.isArray(out.required) && !nullableFields) {
348
- const nullableAtRoot = localNullableFields.get(currentPath || '') || [];
349
- if (nullableAtRoot.length > 0) {
350
- const nextRequired = out.required.filter((r) => typeof r === 'string' && !nullableAtRoot.includes(r));
351
- out.required = nextRequired;
352
- if (nextRequired.length === 0) {
353
- delete out.required;
354
- }
355
- }
356
- }
357
- for (const [key, value] of Object.entries(out)) {
358
- if (key === 'properties')
359
- continue;
360
- if (value && typeof value === 'object') {
361
- out[key] = flattenTypeArrays(value, localNullableFields, `${currentPath || ''}.${key}`);
362
- }
363
- }
364
- return out;
365
- }
366
- function removeUnsupportedKeywords(schema, insideProperties = false) {
367
- if (!schema || typeof schema !== 'object') {
368
- return schema;
369
- }
370
- if (Array.isArray(schema)) {
371
- return schema.map((entry) => removeUnsupportedKeywords(entry, false));
372
- }
373
- const record = schema;
374
- const out = {};
375
- for (const [key, value] of Object.entries(record)) {
376
- if (!insideProperties && UNSUPPORTED_KEYWORDS.has(key)) {
377
- continue;
378
- }
379
- if (value && typeof value === 'object') {
380
- if (key === 'properties' && isPlainRecord(value)) {
381
- const propsOut = {};
382
- for (const [propName, propSchema] of Object.entries(value)) {
383
- propsOut[propName] = removeUnsupportedKeywords(propSchema, false);
384
- }
385
- out[key] = propsOut;
386
- }
387
- else {
388
- out[key] = removeUnsupportedKeywords(value, false);
389
- }
390
- }
391
- else {
392
- out[key] = value;
393
- }
394
- }
395
- return out;
396
- }
397
- function cleanupRequiredFields(schema) {
398
- if (!schema || typeof schema !== 'object') {
399
- return schema;
400
- }
401
- if (Array.isArray(schema)) {
402
- return schema.map((entry) => cleanupRequiredFields(entry));
403
- }
404
- let out = { ...schema };
405
- if (Array.isArray(out.required) && isPlainRecord(out.properties)) {
406
- const props = out.properties;
407
- const valid = out.required.filter((req) => typeof req === 'string' && Object.prototype.hasOwnProperty.call(props, req));
408
- if (valid.length === 0) {
409
- delete out.required;
410
- }
411
- else if (valid.length !== out.required.length) {
412
- out.required = valid;
413
- }
414
- }
415
- for (const [key, value] of Object.entries(out)) {
416
- if (value && typeof value === 'object') {
417
- out[key] = cleanupRequiredFields(value);
418
- }
419
- }
420
- return out;
421
- }
422
- function addEmptySchemaPlaceholder(schema) {
423
- if (!schema || typeof schema !== 'object') {
424
- return schema;
425
- }
426
- if (Array.isArray(schema)) {
427
- return schema.map((entry) => addEmptySchemaPlaceholder(entry));
428
- }
429
- let out = { ...schema };
430
- const typeRaw = typeof out.type === 'string' ? out.type.toLowerCase() : '';
431
- const isObject = typeRaw === 'object' || isPlainRecord(out.properties);
432
- if (isObject) {
433
- const hasProps = isPlainRecord(out.properties) && Object.keys(out.properties).length > 0;
434
- if (!hasProps) {
435
- out.type = 'object';
436
- out.properties = {
437
- [EMPTY_SCHEMA_PLACEHOLDER_NAME]: {
438
- type: 'boolean',
439
- description: EMPTY_SCHEMA_PLACEHOLDER_DESCRIPTION
440
79
  }
441
- };
442
- out.required = [EMPTY_SCHEMA_PLACEHOLDER_NAME];
443
- }
444
- }
445
- for (const [key, value] of Object.entries(out)) {
446
- if (value && typeof value === 'object') {
447
- out[key] = addEmptySchemaPlaceholder(value);
448
- }
449
- }
450
- return out;
451
- }
452
- function cleanJSONSchemaForAntigravity(schema) {
453
- if (!schema || typeof schema !== 'object') {
454
- return schema;
455
- }
456
- let out = schema;
457
- // Keep this helper for backward compatibility in other call sites, but avoid
458
- // aggressive rewriting that can destabilize schema comparison across turns.
459
- out = removeUnsupportedKeywords(out);
460
- out = cleanupRequiredFields(out);
461
- return out;
462
- }
463
- function toGeminiSchema(schema) {
464
- if (schema === null || typeof schema === 'string' || typeof schema === 'number' || typeof schema === 'boolean') {
465
- return schema;
466
- }
467
- if (Array.isArray(schema)) {
468
- return schema.map((entry) => toGeminiSchema(entry));
469
- }
470
- if (!isPlainRecord(schema)) {
471
- return schema;
472
- }
473
- const input = schema;
474
- const out = {};
475
- const propertyNames = new Set();
476
- if (isPlainRecord(input.properties)) {
477
- for (const key of Object.keys(input.properties)) {
478
- propertyNames.add(key);
479
- }
480
- }
481
- for (const [key, value] of Object.entries(input)) {
482
- if (UNSUPPORTED_KEYWORDS.has(key)) {
483
- continue;
484
- }
485
- if (key === 'type' && typeof value === 'string') {
486
- out[key] = value.toUpperCase();
487
- continue;
488
- }
489
- if (key === 'properties' && isPlainRecord(value)) {
490
- const props = {};
491
- for (const [propName, propSchema] of Object.entries(value)) {
492
- props[propName] = toGeminiSchema(propSchema);
80
+ cloned[key] = cloneParameters(entry);
493
81
  }
494
- out[key] = props;
495
- continue;
496
- }
497
- if (key === 'items' && value && typeof value === 'object') {
498
- out[key] = toGeminiSchema(value);
499
- continue;
500
- }
501
- if ((key === 'anyOf' || key === 'oneOf' || key === 'allOf') && Array.isArray(value)) {
502
- out[key] = value.map((item) => toGeminiSchema(item));
503
- continue;
504
- }
505
- if (key === 'enum' && Array.isArray(value)) {
506
- out[key] = value;
507
- continue;
508
- }
509
- if ((key === 'default' || key === 'examples') && value !== undefined) {
510
- out[key] = value;
511
- continue;
512
- }
513
- if (key === 'required' && Array.isArray(value)) {
514
- if (propertyNames.size > 0) {
515
- const validRequired = value.filter((entry) => typeof entry === 'string' && propertyNames.has(entry));
516
- // Filter required array to only include properties that exist.
517
- if (validRequired.length > 0) {
518
- out[key] = validRequired;
519
- }
82
+ return cloned;
83
+ }
84
+ // Gemini function_declarations.parameters only support a subset of JSON Schema.
85
+ // Additionally, recent Antigravity/Gemini backends may produce MALFORMED_FUNCTION_CALL
86
+ // when schemas include combinators (oneOf/anyOf/allOf). Prefer a safe single-variant schema.
87
+ const combinator = Array.isArray(value.oneOf)
88
+ ? 'oneOf'
89
+ : Array.isArray(value.anyOf)
90
+ ? 'anyOf'
91
+ : Array.isArray(value.allOf)
92
+ ? 'allOf'
93
+ : undefined;
94
+ if (combinator) {
95
+ const variants = value[combinator];
96
+ const chosen = pickBestSchemaVariant(Array.isArray(variants) ? variants : []);
97
+ const simplified = cloneParameters(chosen);
98
+ // Preserve description if present on the wrapper node.
99
+ if (isPlainRecord(simplified) &&
100
+ typeof value.description === 'string' &&
101
+ typeof simplified.description !== 'string') {
102
+ simplified.description = String(value.description);
520
103
  }
521
- else {
522
- out[key] = value;
104
+ return simplified;
105
+ }
106
+ const cloned = {};
107
+ for (const [key, entry] of Object.entries(value)) {
108
+ // Gemini function_declarations.parameters only support a subset of JSON Schema.
109
+ // Drop meta/unsupported fields that cause INVALID_ARGUMENT, such as $schema/exclusiveMinimum/propertyNames.
110
+ if (typeof key === 'string') {
111
+ if (key.startsWith('$'))
112
+ continue;
113
+ if (key === 'const')
114
+ continue;
115
+ if (key === 'default' || key === 'examples' || key === 'example' || key === 'title')
116
+ continue;
117
+ if (key === 'deprecated' || key === 'readOnly' || key === 'writeOnly')
118
+ continue;
119
+ if (key === 'patternProperties' || key === 'dependentRequired' || key === 'dependentSchemas')
120
+ continue;
121
+ if (key === 'unevaluatedProperties' || key === 'unevaluatedItems')
122
+ continue;
123
+ if (key === 'contains' || key === 'minContains' || key === 'maxContains')
124
+ continue;
125
+ if (key === 'contentEncoding' || key === 'contentMediaType' || key === 'contentSchema')
126
+ continue;
127
+ if (key === 'if' || key === 'then' || key === 'else' || key === 'not')
128
+ continue;
129
+ if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum' || key === 'propertyNames')
130
+ continue;
131
+ // Keep Gemini tool schemas mostly permissive to avoid upstream MALFORMED_FUNCTION_CALL on strict validation.
132
+ // Preserve `required` for Gemini validation (align with gcli2api), but still drop additionalProperties.
133
+ if (key === 'additionalProperties')
134
+ continue;
135
+ // Combinators are handled at the node level above.
136
+ if (key === 'oneOf' || key === 'anyOf' || key === 'allOf')
137
+ continue;
523
138
  }
524
- continue;
139
+ cloned[key] = cloneParameters(entry);
525
140
  }
526
- out[key] = value;
527
- }
528
- if (out.type === 'ARRAY' && !('items' in out)) {
529
- out.items = { type: 'STRING' };
141
+ return cloned;
530
142
  }
531
- return out;
532
- }
533
- function cloneParameters(value, mode = 'default') {
534
- if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
535
- return value;
536
- }
537
- const cloned = jsonClone(value);
538
- if (mode === 'antigravity') {
539
- // gcli2api-style: minimal, deterministic transform (no schema hint rewriting).
540
- return toGeminiSchema(cloned);
541
- }
542
- // Default Gemini: avoid aggressive schema rewriting (keep original shape where possible).
543
- return convertConstToEnum(cloned);
143
+ return { type: 'object', properties: {} };
544
144
  }
545
145
  function declarationToBridge(declaration) {
546
146
  if (!declaration || typeof declaration !== 'object') {
@@ -552,7 +152,7 @@ function declarationToBridge(declaration) {
552
152
  return null;
553
153
  }
554
154
  const description = typeof def.description === 'string' ? def.description : undefined;
555
- const parameters = cloneParameters(def.parameters ?? { type: 'object', properties: {} }, 'default');
155
+ const parameters = cloneParameters(def.parameters ?? { type: 'object', properties: {} });
556
156
  return {
557
157
  type: 'function',
558
158
  function: {
@@ -568,7 +168,7 @@ function legacyToolToBridge(entry) {
568
168
  return null;
569
169
  }
570
170
  const description = typeof entry.description === 'string' ? entry.description : undefined;
571
- const parameters = cloneParameters(entry.parameters ?? { type: 'object', properties: {} }, 'default');
171
+ const parameters = cloneParameters(entry.parameters ?? { type: 'object', properties: {} });
572
172
  return {
573
173
  type: 'function',
574
174
  function: {
@@ -625,16 +225,15 @@ export function buildGeminiToolsFromBridge(defs, options) {
625
225
  // Keep all function declarations in a single tool group to match gcli2api snapshots
626
226
  // and avoid backend-specific limits on the number of tool groups.
627
227
  const functionDeclarations = [];
228
+ const normalizeToolName = (name) => mode === 'antigravity' ? String(name || '').replace(/-/g, '_') : String(name || '');
628
229
  const applyFixups = (name, parameters) => {
629
230
  if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) {
630
231
  return parameters;
631
232
  }
632
- if (mode !== 'antigravity') {
633
- return parameters;
634
- }
635
233
  const params = parameters;
636
234
  const propsRaw = params.properties;
637
235
  const props = isPlainRecord(propsRaw) ? propsRaw : {};
236
+ const isAntigravity = mode === 'antigravity';
638
237
  const lowered = String(name || '').trim().toLowerCase();
639
238
  if (lowered === 'exec_command') {
640
239
  // Keep Gemini tool schema aligned with both historical Codex and our runtime tool signature:
@@ -646,20 +245,39 @@ export function buildGeminiToolsFromBridge(defs, options) {
646
245
  // is disabled (toolConfig.mode=NONE). Force these fields to simple STRING.
647
246
  // For Gemini wire, prefer `command` as the canonical key (gcli2api-style).
648
247
  // The internal tool governor normalizes `command` → `cmd` before execution.
649
- try {
650
- if (Object.prototype.hasOwnProperty.call(props, 'cmd')) {
651
- delete props.cmd;
248
+ if (isAntigravity) {
249
+ try {
250
+ if (Object.prototype.hasOwnProperty.call(props, 'cmd')) {
251
+ delete props.cmd;
252
+ }
652
253
  }
254
+ catch {
255
+ // ignore
256
+ }
257
+ props.command = {
258
+ type: 'STRING',
259
+ description: 'Shell command to execute.'
260
+ };
261
+ // Keep workdir simple (avoid anyOf string|null patterns).
262
+ props.workdir = { type: 'STRING', description: 'Working directory.' };
653
263
  }
654
- catch {
655
- // ignore
264
+ else {
265
+ if (!Object.prototype.hasOwnProperty.call(props, 'cmd') && Object.prototype.hasOwnProperty.call(props, 'command')) {
266
+ props.cmd = props.command;
267
+ }
268
+ if (!Object.prototype.hasOwnProperty.call(props, 'command') && Object.prototype.hasOwnProperty.call(props, 'cmd')) {
269
+ props.command = props.cmd;
270
+ }
271
+ if (!Object.prototype.hasOwnProperty.call(props, 'cmd')) {
272
+ props.cmd = { type: 'string' };
273
+ }
274
+ if (!Object.prototype.hasOwnProperty.call(props, 'command')) {
275
+ props.command = { type: 'string' };
276
+ }
277
+ if (!Object.prototype.hasOwnProperty.call(props, 'workdir')) {
278
+ props.workdir = { type: 'string' };
279
+ }
656
280
  }
657
- props.command = {
658
- type: 'STRING',
659
- description: 'Shell command to execute.'
660
- };
661
- // Keep workdir simple (avoid anyOf string|null patterns).
662
- props.workdir = { type: 'STRING', description: 'Working directory.' };
663
281
  params.properties = props;
664
282
  // Avoid hard required keys for Gemini: the model may emit either alias (cmd/command),
665
283
  // and "required" mismatch surfaces as MALFORMED_FUNCTION_CALL (empty reply) upstream.
@@ -680,10 +298,10 @@ export function buildGeminiToolsFromBridge(defs, options) {
680
298
  props.text = props.chars;
681
299
  }
682
300
  if (!Object.prototype.hasOwnProperty.call(props, 'session_id')) {
683
- props.session_id = { type: 'NUMBER' };
301
+ props.session_id = { type: isAntigravity ? 'NUMBER' : 'number' };
684
302
  }
685
303
  if (!Object.prototype.hasOwnProperty.call(props, 'chars')) {
686
- props.chars = { type: 'STRING' };
304
+ props.chars = { type: isAntigravity ? 'STRING' : 'string' };
687
305
  }
688
306
  params.properties = props;
689
307
  try {
@@ -701,25 +319,25 @@ export function buildGeminiToolsFromBridge(defs, options) {
701
319
  // - input/instructions/text: historical aliases containing patch text
702
320
  if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
703
321
  props.patch = {
704
- type: 'STRING',
322
+ type: isAntigravity ? 'STRING' : 'string',
705
323
  description: 'Patch text (*** Begin Patch / *** End Patch or GNU unified diff).'
706
324
  };
707
325
  }
708
326
  if (!Object.prototype.hasOwnProperty.call(props, 'input')) {
709
327
  props.input = {
710
- type: 'STRING',
328
+ type: isAntigravity ? 'STRING' : 'string',
711
329
  description: 'Alias of patch (patch text). Prefer patch.'
712
330
  };
713
331
  }
714
332
  if (!Object.prototype.hasOwnProperty.call(props, 'instructions')) {
715
333
  props.instructions = {
716
- type: 'STRING',
334
+ type: isAntigravity ? 'STRING' : 'string',
717
335
  description: 'Alias of patch (patch text). Prefer patch.'
718
336
  };
719
337
  }
720
338
  if (!Object.prototype.hasOwnProperty.call(props, 'text')) {
721
339
  props.text = {
722
- type: 'STRING',
340
+ type: isAntigravity ? 'STRING' : 'string',
723
341
  description: 'Alias of patch (patch text). Prefer patch.'
724
342
  };
725
343
  }
@@ -765,16 +383,20 @@ export function buildGeminiToolsFromBridge(defs, options) {
765
383
  if (!name) {
766
384
  return;
767
385
  }
386
+ const finalName = normalizeToolName(name);
768
387
  const description = typeof fnNode?.description === 'string'
769
388
  ? fnNode.description
770
389
  : typeof def.description === 'string'
771
390
  ? def.description
772
391
  : undefined;
773
- const parameters = applyFixups(name, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }, mode));
392
+ const parameters = applyFixups(finalName, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }));
393
+ const normalizedParameters = mode === 'antigravity'
394
+ ? normalizeSchemaTypes(parameters)
395
+ : parameters;
774
396
  functionDeclarations.push({
775
- name,
776
- description: rewriteDescription(name, description),
777
- parameters
397
+ name: finalName,
398
+ description: rewriteDescription(finalName, description),
399
+ parameters: normalizedParameters
778
400
  });
779
401
  });
780
402
  if (!functionDeclarations.length) {