@jsonstudio/llms 0.6.1172 → 0.6.1354

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 (160) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +48 -4
  95. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  96. package/dist/router/virtual-router/engine-health.js +11 -110
  97. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  98. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  99. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  100. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  101. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  102. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  103. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  104. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  105. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  106. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  107. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  108. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  109. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  110. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  111. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  112. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  113. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  114. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  115. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  116. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  117. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  118. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  119. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  120. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  121. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  122. package/dist/router/virtual-router/engine-selection.js +10 -962
  123. package/dist/router/virtual-router/engine.d.ts +1 -0
  124. package/dist/router/virtual-router/engine.js +55 -10
  125. package/dist/router/virtual-router/routing-instructions.js +6 -1
  126. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  127. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  128. package/dist/router/virtual-router/types.d.ts +25 -1
  129. package/dist/servertool/clock/config.d.ts +8 -0
  130. package/dist/servertool/clock/config.js +22 -0
  131. package/dist/servertool/clock/log.d.ts +3 -0
  132. package/dist/servertool/clock/log.js +13 -0
  133. package/dist/servertool/clock/task-store.d.ts +1 -1
  134. package/dist/servertool/clock/task-store.js +1 -1
  135. package/dist/servertool/clock/tasks.js +1 -1
  136. package/dist/servertool/engine.js +146 -21
  137. package/dist/servertool/handlers/clock-auto.js +11 -6
  138. package/dist/servertool/handlers/clock.js +36 -10
  139. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  140. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  141. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  142. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  143. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  144. package/dist/servertool/handlers/vision.js +4 -1
  145. package/dist/servertool/handlers/web-search.js +3 -1
  146. package/dist/servertool/pending-session.d.ts +19 -0
  147. package/dist/servertool/pending-session.js +97 -0
  148. package/dist/servertool/reenter-backend.js +5 -3
  149. package/dist/servertool/server-side-tools.js +235 -6
  150. package/dist/servertool/types.d.ts +13 -0
  151. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  152. package/dist/sse/shared/chat-serializer.js +2 -2
  153. package/dist/sse/shared/constants.js +1 -1
  154. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  155. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  156. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  157. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  158. package/dist/tools/exec-command/normalize.js +4 -0
  159. package/dist/tools/exec-command/regression-capturer.js +1 -1
  160. package/package.json +10 -5
@@ -2,90 +2,545 @@ 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
- 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;
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;
33
72
  }
73
+ return base;
34
74
  }
35
- return best;
75
+ const out = {};
76
+ for (const [key, value] of Object.entries(record)) {
77
+ out[key] = convertRefsToHints(value);
78
+ }
79
+ return out;
36
80
  }
37
- function cloneParameters(value) {
38
- if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
39
- return value;
81
+ function convertConstToEnum(schema) {
82
+ if (!schema || typeof schema !== 'object') {
83
+ return schema;
84
+ }
85
+ if (Array.isArray(schema)) {
86
+ return schema.map((entry) => convertConstToEnum(entry));
87
+ }
88
+ const record = schema;
89
+ const out = {};
90
+ for (const [key, value] of Object.entries(record)) {
91
+ if (key === 'const' && record.enum === undefined) {
92
+ out.enum = [convertConstToEnum(value)];
93
+ continue;
94
+ }
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')
114
+ continue;
115
+ if (value && typeof value === 'object') {
116
+ out[key] = addEnumHints(value);
117
+ }
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')
135
+ continue;
136
+ if (value && typeof value === 'object') {
137
+ out[key] = addAdditionalPropertiesHints(value);
138
+ }
139
+ }
140
+ return out;
141
+ }
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;
40
147
  }
41
- if (Array.isArray(value)) {
42
- return value.map((entry) => cloneParameters(entry));
43
- }
44
- if (isPlainRecord(value)) {
45
- // Gemini function_declarations.parameters only support a subset of JSON Schema.
46
- // Additionally, recent Antigravity/Gemini backends may produce MALFORMED_FUNCTION_CALL
47
- // when schemas include combinators (oneOf/anyOf/allOf). Prefer a safe single-variant schema.
48
- const combinator = Array.isArray(value.oneOf)
49
- ? 'oneOf'
50
- : Array.isArray(value.anyOf)
51
- ? 'anyOf'
52
- : Array.isArray(value.allOf)
53
- ? 'allOf'
54
- : undefined;
55
- if (combinator) {
56
- const variants = value[combinator];
57
- const chosen = pickBestSchemaVariant(Array.isArray(variants) ? variants : []);
58
- const simplified = cloneParameters(chosen);
59
- // Preserve description if present on the wrapper node.
60
- if (isPlainRecord(simplified) &&
61
- typeof value.description === 'string' &&
62
- typeof simplified.description !== 'string') {
63
- simplified.description = String(value.description);
148
+ if (Array.isArray(schema)) {
149
+ return schema.map((entry) => mergeAllOf(entry));
150
+ }
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 };
64
162
  }
65
- return simplified;
66
- }
67
- const cloned = {};
68
- for (const [key, entry] of Object.entries(value)) {
69
- // Gemini function_declarations.parameters only support a subset of JSON Schema.
70
- // Drop meta/unsupported fields that cause INVALID_ARGUMENT, such as $schema/exclusiveMinimum/propertyNames.
71
- if (typeof key === 'string') {
72
- if (key.startsWith('$'))
73
- continue;
74
- if (key === 'exclusiveMinimum' || key === 'exclusiveMaximum' || key === 'propertyNames')
75
- continue;
76
- // Keep Gemini tool schemas mostly permissive to avoid upstream MALFORMED_FUNCTION_CALL on strict validation.
77
- // We selectively re-introduce safe required fields for critical tools in `buildGeminiToolsFromBridge`.
78
- if (key === 'required' || key === 'additionalProperties')
79
- continue;
80
- // Combinators are handled at the node level above.
81
- if (key === 'oneOf' || key === 'anyOf' || key === 'allOf')
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')
82
172
  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];
83
297
  }
84
- cloned[key] = cloneParameters(entry);
85
298
  }
86
- return cloned;
87
299
  }
88
- return { type: 'object', properties: {} };
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
+ }
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);
493
+ }
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
+ }
520
+ }
521
+ else {
522
+ out[key] = value;
523
+ }
524
+ continue;
525
+ }
526
+ out[key] = value;
527
+ }
528
+ if (out.type === 'ARRAY' && !('items' in out)) {
529
+ out.items = { type: 'STRING' };
530
+ }
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);
89
544
  }
90
545
  function declarationToBridge(declaration) {
91
546
  if (!declaration || typeof declaration !== 'object') {
@@ -97,7 +552,7 @@ function declarationToBridge(declaration) {
97
552
  return null;
98
553
  }
99
554
  const description = typeof def.description === 'string' ? def.description : undefined;
100
- const parameters = cloneParameters(def.parameters ?? { type: 'object', properties: {} });
555
+ const parameters = cloneParameters(def.parameters ?? { type: 'object', properties: {} }, 'default');
101
556
  return {
102
557
  type: 'function',
103
558
  function: {
@@ -113,7 +568,7 @@ function legacyToolToBridge(entry) {
113
568
  return null;
114
569
  }
115
570
  const description = typeof entry.description === 'string' ? entry.description : undefined;
116
- const parameters = cloneParameters(entry.parameters ?? { type: 'object', properties: {} });
571
+ const parameters = cloneParameters(entry.parameters ?? { type: 'object', properties: {} }, 'default');
117
572
  return {
118
573
  type: 'function',
119
574
  function: {
@@ -161,15 +616,22 @@ export function prepareGeminiToolsForBridge(rawTools, missing) {
161
616
  });
162
617
  return defs.length ? defs : undefined;
163
618
  }
164
- export function buildGeminiToolsFromBridge(defs) {
619
+ export function buildGeminiToolsFromBridge(defs, options) {
165
620
  if (!defs || !defs.length) {
166
621
  return undefined;
167
622
  }
168
- const tools = [];
623
+ const mode = options?.mode === 'antigravity' ? 'antigravity' : 'default';
624
+ // Cloud Code Assist / Gemini CLI is sensitive to tool array shape.
625
+ // Keep all function declarations in a single tool group to match gcli2api snapshots
626
+ // and avoid backend-specific limits on the number of tool groups.
627
+ const functionDeclarations = [];
169
628
  const applyFixups = (name, parameters) => {
170
629
  if (!parameters || typeof parameters !== 'object' || Array.isArray(parameters)) {
171
630
  return parameters;
172
631
  }
632
+ if (mode !== 'antigravity') {
633
+ return parameters;
634
+ }
173
635
  const params = parameters;
174
636
  const propsRaw = params.properties;
175
637
  const props = isPlainRecord(propsRaw) ? propsRaw : {};
@@ -179,21 +641,25 @@ export function buildGeminiToolsFromBridge(defs) {
179
641
  // - Codex CLI: { cmd, workdir }
180
642
  // - Some history: { command, workdir }
181
643
  // Strict Gemini validation (and/or tool selection) is sensitive to schema/arg drift.
182
- if (!Object.prototype.hasOwnProperty.call(props, 'cmd') && Object.prototype.hasOwnProperty.call(props, 'command')) {
183
- props.cmd = props.command;
184
- }
185
- if (!Object.prototype.hasOwnProperty.call(props, 'command') && Object.prototype.hasOwnProperty.call(props, 'cmd')) {
186
- props.command = props.cmd;
187
- }
188
- if (!Object.prototype.hasOwnProperty.call(props, 'cmd')) {
189
- props.cmd = { type: 'string' };
190
- }
191
- if (!Object.prototype.hasOwnProperty.call(props, 'command')) {
192
- props.command = { type: 'string' };
644
+ // NOTE: Some upstream tool schemas encode cmd/command as `oneOf` (string|array),
645
+ // which Antigravity/Gemini can reject with MALFORMED_FUNCTION_CALL even when tool calling
646
+ // is disabled (toolConfig.mode=NONE). Force these fields to simple STRING.
647
+ // For Gemini wire, prefer `command` as the canonical key (gcli2api-style).
648
+ // The internal tool governor normalizes `command` `cmd` before execution.
649
+ try {
650
+ if (Object.prototype.hasOwnProperty.call(props, 'cmd')) {
651
+ delete props.cmd;
652
+ }
193
653
  }
194
- if (!Object.prototype.hasOwnProperty.call(props, 'workdir')) {
195
- props.workdir = { type: 'string' };
654
+ catch {
655
+ // ignore
196
656
  }
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.' };
197
663
  params.properties = props;
198
664
  // Avoid hard required keys for Gemini: the model may emit either alias (cmd/command),
199
665
  // and "required" mismatch surfaces as MALFORMED_FUNCTION_CALL (empty reply) upstream.
@@ -214,10 +680,10 @@ export function buildGeminiToolsFromBridge(defs) {
214
680
  props.text = props.chars;
215
681
  }
216
682
  if (!Object.prototype.hasOwnProperty.call(props, 'session_id')) {
217
- props.session_id = { type: 'number' };
683
+ props.session_id = { type: 'NUMBER' };
218
684
  }
219
685
  if (!Object.prototype.hasOwnProperty.call(props, 'chars')) {
220
- props.chars = { type: 'string' };
686
+ props.chars = { type: 'STRING' };
221
687
  }
222
688
  params.properties = props;
223
689
  try {
@@ -235,25 +701,25 @@ export function buildGeminiToolsFromBridge(defs) {
235
701
  // - input/instructions/text: historical aliases containing patch text
236
702
  if (!Object.prototype.hasOwnProperty.call(props, 'patch')) {
237
703
  props.patch = {
238
- type: 'string',
704
+ type: 'STRING',
239
705
  description: 'Patch text (*** Begin Patch / *** End Patch or GNU unified diff).'
240
706
  };
241
707
  }
242
708
  if (!Object.prototype.hasOwnProperty.call(props, 'input')) {
243
709
  props.input = {
244
- type: 'string',
710
+ type: 'STRING',
245
711
  description: 'Alias of patch (patch text). Prefer patch.'
246
712
  };
247
713
  }
248
714
  if (!Object.prototype.hasOwnProperty.call(props, 'instructions')) {
249
715
  props.instructions = {
250
- type: 'string',
716
+ type: 'STRING',
251
717
  description: 'Alias of patch (patch text). Prefer patch.'
252
718
  };
253
719
  }
254
720
  if (!Object.prototype.hasOwnProperty.call(props, 'text')) {
255
721
  props.text = {
256
- type: 'string',
722
+ type: 'STRING',
257
723
  description: 'Alias of patch (patch text). Prefer patch.'
258
724
  };
259
725
  }
@@ -269,6 +735,9 @@ export function buildGeminiToolsFromBridge(defs) {
269
735
  return params;
270
736
  };
271
737
  const rewriteDescription = (name, description) => {
738
+ if (mode !== 'antigravity') {
739
+ return description;
740
+ }
272
741
  const lowered = String(name || '').trim().toLowerCase();
273
742
  if (lowered === 'apply_patch') {
274
743
  return ('Edit files by providing patch text in `patch` (string). ' +
@@ -301,16 +770,19 @@ export function buildGeminiToolsFromBridge(defs) {
301
770
  : typeof def.description === 'string'
302
771
  ? def.description
303
772
  : undefined;
304
- const parameters = applyFixups(name, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }));
305
- tools.push({
306
- functionDeclarations: [
307
- {
308
- name,
309
- description: rewriteDescription(name, description),
310
- parameters
311
- }
312
- ]
773
+ const parameters = applyFixups(name, cloneParameters(fnNode?.parameters ?? def.parameters ?? { type: 'object', properties: {} }, mode));
774
+ functionDeclarations.push({
775
+ name,
776
+ description: rewriteDescription(name, description),
777
+ parameters
313
778
  });
314
779
  });
315
- return tools.length ? tools : undefined;
780
+ if (!functionDeclarations.length) {
781
+ return undefined;
782
+ }
783
+ return [
784
+ {
785
+ functionDeclarations
786
+ }
787
+ ];
316
788
  }