@bitkyc08/opencodex 2.59.0 → 2.60.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 (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -8,7 +8,38 @@ const ALLOWED_TYPES = new Set(["string", "integer", "number", "boolean", "array"
8
8
  const MAX_SCHEMA_DEPTH = 24; // Google's documented nesting limit is 32; leave headroom for CCA.
9
9
  const MAX_DEREF_DEPTH = 16;
10
10
  const MAX_SCHEMA_NODES = 1_024;
11
+ export const GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT = 255;
11
12
  const BUDGET_EXHAUSTED = Symbol("schema-budget-exhausted");
13
+ const NUMERIC_BOUND_KEYS = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"] as const;
14
+ const SIZE_BOUND_KEYS = ["minLength", "maxLength", "minItems", "maxItems", "minProperties", "maxProperties"] as const;
15
+ const UNSUPPORTED_CONSTRAINT_KEYS = [
16
+ "allOf",
17
+ "oneOf",
18
+ "not",
19
+ "multipleOf",
20
+ "pattern",
21
+ "$dynamicRef",
22
+ "$recursiveRef",
23
+ "additionalProperties",
24
+ "additionalItems",
25
+ "uniqueItems",
26
+ "contains",
27
+ "minContains",
28
+ "maxContains",
29
+ "dependencies",
30
+ "dependentRequired",
31
+ "dependentSchemas",
32
+ "patternProperties",
33
+ "propertyNames",
34
+ "unevaluatedProperties",
35
+ "unevaluatedItems",
36
+ ] as const;
37
+ const CONDITIONAL_KEYS = ["if", "then", "else"] as const;
38
+ const BOUNDED_VALUE_KEYS = new Set(["enum", "properties", "required", "items", "anyOf"]);
39
+ const RETAINED_UNION_KEYS = ["type", "nullable", "format", "enum", "properties", "items", "required"] as const;
40
+ // Annotation-only keywords do not change the accepted value set. The sanitizer intentionally
41
+ // drops title, default, examples, $comment, deprecated, readOnly, writeOnly, contentEncoding,
42
+ // contentMediaType, contentSchema, externalDocs, and example without loss.
12
43
  const MERGED_SCHEMA_KEYS = [
13
44
  "type",
14
45
  "nullable",
@@ -24,9 +55,150 @@ const MERGED_SCHEMA_KEYS = [
24
55
 
25
56
  type SanitizeResult = Schema | typeof BUDGET_EXHAUSTED;
26
57
 
58
+ export type GoogleToolSchemaEndpointClass = "ai-studio" | "vertex" | "cloud-code-assist";
59
+ export type GoogleToolSchemaPolicy = "compatible" | "reject-lossy";
60
+
61
+ export interface GoogleToolSchemaProfile {
62
+ endpointClass: GoogleToolSchemaEndpointClass;
63
+ }
64
+
65
+ export type GoogleToolSchemaLossCategory =
66
+ | "enum-value-filtered"
67
+ | "enum-constraint-dropped"
68
+ | "const-value-filtered"
69
+ | "numeric-bound-dropped"
70
+ | "size-bound-dropped"
71
+ | "type-union-collapsed"
72
+ | "unsupported-type-widened"
73
+ | "nullability-overridden"
74
+ | "conditional-dropped"
75
+ | "tuple-prefix-dropped"
76
+ | "ref-overlay-replaced"
77
+ | "union-sibling-replaced"
78
+ | "root-object-coerced"
79
+ | "union-widened"
80
+ | "recursive-ref-widened"
81
+ | "dereference-limit-widened"
82
+ | "depth-limit-widened"
83
+ | "node-budget-widened"
84
+ | "repair-opened-schema"
85
+ | "unsupported-constraint-dropped"
86
+ | "invalid-schema-widened";
87
+
88
+ export interface GoogleToolSchemaLossReport {
89
+ version: 1;
90
+ endpointClass: GoogleToolSchemaEndpointClass;
91
+ lossy: boolean;
92
+ truncated: boolean;
93
+ /** Bounded comparisons that exhausted their allowance; uncertainty is not proven loss. */
94
+ uncertainComparisons: number;
95
+ /** Saturating proven-loss counts only; capped/unknown structural comparisons add no category. */
96
+ categories: Partial<Record<GoogleToolSchemaLossCategory, number>>;
97
+ }
98
+
99
+ export interface GoogleToolSchemaSanitizeResult {
100
+ parameters: Record<string, unknown>;
101
+ lossReport: GoogleToolSchemaLossReport;
102
+ }
103
+
27
104
  interface SanitizeState {
28
105
  activeRefs: Set<string>;
29
106
  remainingNodes: number;
107
+ budgetReported: boolean;
108
+ enumFilterLossOverrides: WeakMap<Schema, number>;
109
+ report: GoogleToolSchemaLossReport;
110
+ }
111
+
112
+ export function createGoogleToolSchemaLossReport(
113
+ profile: GoogleToolSchemaProfile,
114
+ ): GoogleToolSchemaLossReport {
115
+ return {
116
+ version: 1,
117
+ endpointClass: profile.endpointClass,
118
+ lossy: false,
119
+ truncated: false,
120
+ uncertainComparisons: 0,
121
+ categories: {},
122
+ };
123
+ }
124
+
125
+ function addGoogleToolSchemaUncertainty(report: GoogleToolSchemaLossReport): void {
126
+ if (report.uncertainComparisons >= GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT) {
127
+ report.truncated = true;
128
+ return;
129
+ }
130
+ report.uncertainComparisons++;
131
+ }
132
+
133
+ export function addGoogleToolSchemaLoss(
134
+ report: GoogleToolSchemaLossReport,
135
+ category: GoogleToolSchemaLossCategory,
136
+ amount = 1,
137
+ ): void {
138
+ if (!Number.isFinite(amount) || amount <= 0) return;
139
+ report.lossy = true;
140
+ const increment = Math.floor(amount);
141
+ const current = report.categories[category] ?? 0;
142
+ const next = current + increment;
143
+ if (next >= GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT) {
144
+ report.categories[category] = GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT;
145
+ if (next > GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT) report.truncated = true;
146
+ return;
147
+ }
148
+ report.categories[category] = next;
149
+ }
150
+
151
+ export function mergeGoogleToolSchemaLossReport(
152
+ target: GoogleToolSchemaLossReport,
153
+ source: GoogleToolSchemaLossReport,
154
+ ): void {
155
+ for (const [category, count] of Object.entries(source.categories)) {
156
+ addGoogleToolSchemaLoss(target, category as GoogleToolSchemaLossCategory, count);
157
+ }
158
+ const uncertainty = target.uncertainComparisons + source.uncertainComparisons;
159
+ target.uncertainComparisons = Math.min(uncertainty, GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT);
160
+ if (uncertainty > GOOGLE_TOOL_SCHEMA_LOSS_COUNT_LIMIT) target.truncated = true;
161
+ if (source.truncated) target.truncated = true;
162
+ }
163
+
164
+ function reportBudgetExhausted(state: SanitizeState): void {
165
+ if (state.budgetReported) return;
166
+ state.budgetReported = true;
167
+ addGoogleToolSchemaLoss(state.report, "node-budget-widened");
168
+ }
169
+
170
+ function reportDroppedConstraints(node: Schema, state: SanitizeState): void {
171
+ let numericBounds = 0;
172
+ for (const key of NUMERIC_BOUND_KEYS) {
173
+ if (Object.hasOwn(node, key)) numericBounds++;
174
+ }
175
+ addGoogleToolSchemaLoss(state.report, "numeric-bound-dropped", numericBounds);
176
+
177
+ let sizeBounds = 0;
178
+ for (const key of SIZE_BOUND_KEYS) {
179
+ if (!Object.hasOwn(node, key)) continue;
180
+ if ((key === "minLength" || key === "minItems" || key === "minProperties") && node[key] === 0) continue;
181
+ sizeBounds++;
182
+ }
183
+ addGoogleToolSchemaLoss(state.report, "size-bound-dropped", sizeBounds);
184
+
185
+ let unsupported = 0;
186
+ for (const key of UNSUPPORTED_CONSTRAINT_KEYS) {
187
+ if (!Object.hasOwn(node, key)) continue;
188
+ if (key === "additionalProperties" && node[key] === true) continue;
189
+ if (key === "uniqueItems" && node[key] === false) continue;
190
+ unsupported++;
191
+ }
192
+ addGoogleToolSchemaLoss(state.report, "unsupported-constraint-dropped", unsupported);
193
+
194
+ let conditionals = 0;
195
+ for (const key of CONDITIONAL_KEYS) {
196
+ if (Object.hasOwn(node, key)) conditionals++;
197
+ }
198
+ addGoogleToolSchemaLoss(state.report, "conditional-dropped", conditionals);
199
+ if (Object.hasOwn(node, "prefixItems") && !(Array.isArray(node.prefixItems) && node.prefixItems.length === 0)) {
200
+ addGoogleToolSchemaLoss(state.report, "tuple-prefix-dropped");
201
+ }
30
202
  }
31
203
 
32
204
  function isRecord(value: unknown): value is Schema {
@@ -55,39 +227,308 @@ function collectDefs(root: unknown, defs: Map<string, unknown>): void {
55
227
  }
56
228
  }
57
229
 
58
- function mergeRefTarget(target: Schema, overlay: Schema): Schema {
230
+ interface BoundedEqualityState {
231
+ remainingNodes: number;
232
+ }
233
+
234
+ /**
235
+ * Exact JSON-value comparison with the sanitizer's own depth/node ceilings.
236
+ * `unknown` means the cap prevented a proof; report-only mode does not count it as proven loss.
237
+ */
238
+ type BoundedEquality = "equal" | "different" | "unknown";
239
+
240
+ function boundedJsonEqual(
241
+ left: unknown,
242
+ right: unknown,
243
+ state: BoundedEqualityState = { remainingNodes: MAX_SCHEMA_NODES },
244
+ depth = 0,
245
+ ): BoundedEquality {
246
+ if (Object.is(left, right)) return "equal";
247
+ if (depth >= MAX_SCHEMA_DEPTH || state.remainingNodes <= 0) return "unknown";
248
+ state.remainingNodes--;
249
+
250
+ if (Array.isArray(left) || Array.isArray(right)) {
251
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return "different";
252
+ for (let index = 0; index < left.length; index++) {
253
+ if (state.remainingNodes <= 0) return "unknown";
254
+ state.remainingNodes--;
255
+ const comparison = boundedJsonEqual(left[index], right[index], state, depth + 1);
256
+ if (comparison !== "equal") return comparison;
257
+ }
258
+ return "equal";
259
+ }
260
+ if (!isRecord(left) || !isRecord(right)) return "different";
261
+
262
+ let leftCount = 0;
263
+ for (const key in left) {
264
+ if (!Object.hasOwn(left, key)) continue;
265
+ if (state.remainingNodes <= 0) return "unknown";
266
+ state.remainingNodes--;
267
+ leftCount++;
268
+ if (!Object.hasOwn(right, key)) return "different";
269
+ const comparison = boundedJsonEqual(left[key], right[key], state, depth + 1);
270
+ if (comparison !== "equal") return comparison;
271
+ }
272
+ let rightCount = 0;
273
+ for (const key in right) {
274
+ if (!Object.hasOwn(right, key)) continue;
275
+ if (state.remainingNodes <= 0) return "unknown";
276
+ state.remainingNodes--;
277
+ rightCount++;
278
+ if (rightCount > leftCount) return "different";
279
+ }
280
+ return leftCount === rightCount ? "equal" : "different";
281
+ }
282
+
283
+ function boundedStringSetEqual(
284
+ left: unknown,
285
+ right: unknown,
286
+ normalize: (value: string) => string,
287
+ state: BoundedEqualityState = { remainingNodes: MAX_SCHEMA_NODES },
288
+ ): BoundedEquality {
289
+ const leftValues = Array.isArray(left) ? left : [left];
290
+ const rightValues = Array.isArray(right) ? right : [right];
291
+ const collect = (values: unknown[]): Set<string> | undefined => {
292
+ const result = new Set<string>();
293
+ for (const value of values) {
294
+ if (state.remainingNodes <= 0) return undefined;
295
+ state.remainingNodes--;
296
+ if (typeof value === "string") result.add(normalize(value));
297
+ }
298
+ return result;
299
+ };
300
+ const leftSet = collect(leftValues);
301
+ const rightSet = collect(rightValues);
302
+ if (!leftSet || !rightSet) return "unknown";
303
+ if (leftSet.size !== rightSet.size) return "different";
304
+ for (const value of leftSet) {
305
+ if (!rightSet.has(value)) return "different";
306
+ }
307
+ return "equal";
308
+ }
309
+
310
+ function boundedJsonSetEqual(
311
+ left: unknown[],
312
+ right: unknown[],
313
+ state: BoundedEqualityState = { remainingNodes: MAX_SCHEMA_NODES },
314
+ ): BoundedEquality {
315
+ const unique = (values: unknown[]): { comparison: BoundedEquality; values: unknown[] } => {
316
+ const result: unknown[] = [];
317
+ for (const value of values) {
318
+ if (state.remainingNodes <= 0) return { comparison: "unknown", values: result };
319
+ state.remainingNodes--;
320
+ let duplicate = false;
321
+ for (const candidate of result) {
322
+ const comparison = boundedJsonEqual(value, candidate, state);
323
+ if (comparison === "unknown") return { comparison, values: result };
324
+ if (comparison === "equal") {
325
+ duplicate = true;
326
+ break;
327
+ }
328
+ }
329
+ if (!duplicate) result.push(value);
330
+ }
331
+ return { comparison: "equal", values: result };
332
+ };
333
+ const leftUnique = unique(left);
334
+ if (leftUnique.comparison !== "equal") return leftUnique.comparison;
335
+ const rightUnique = unique(right);
336
+ if (rightUnique.comparison !== "equal") return rightUnique.comparison;
337
+ if (leftUnique.values.length !== rightUnique.values.length) return "different";
338
+ const unmatched = [...rightUnique.values];
339
+ for (const value of leftUnique.values) {
340
+ let matched = -1;
341
+ for (let index = 0; index < unmatched.length; index++) {
342
+ const comparison = boundedJsonEqual(value, unmatched[index], state);
343
+ if (comparison === "unknown") return comparison;
344
+ if (comparison === "equal") {
345
+ matched = index;
346
+ break;
347
+ }
348
+ }
349
+ if (matched < 0) return "different";
350
+ unmatched.splice(matched, 1);
351
+ }
352
+ return "equal";
353
+ }
354
+
355
+ interface BoundedEnumOverlayAccounting {
356
+ comparison: BoundedEquality;
357
+ emittedWidensIntersection: boolean;
358
+ filteredIntersectionValues: number;
359
+ }
360
+
361
+ function boundedEnumOverlayAccounting(
362
+ target: unknown[],
363
+ overlay: unknown[],
364
+ ): BoundedEnumOverlayAccounting {
365
+ const state: BoundedEqualityState = { remainingNodes: MAX_SCHEMA_NODES };
366
+ const stringSet = (values: unknown[]): Set<string> | undefined => {
367
+ const result = new Set<string>();
368
+ for (const value of values) {
369
+ if (state.remainingNodes <= 0) return undefined;
370
+ state.remainingNodes--;
371
+ if (typeof value === "string") result.add(value);
372
+ }
373
+ return result;
374
+ };
375
+ const targetStrings = stringSet(target);
376
+ const emittedStrings = stringSet(overlay);
377
+ if (!targetStrings || !emittedStrings) {
378
+ return { comparison: "unknown", emittedWidensIntersection: false, filteredIntersectionValues: 0 };
379
+ }
380
+
381
+ let emittedWidensIntersection = false;
382
+ for (const value of emittedStrings) {
383
+ if (!targetStrings.has(value)) {
384
+ emittedWidensIntersection = true;
385
+ break;
386
+ }
387
+ }
388
+
389
+ const uniqueFiltered: unknown[] = [];
390
+ for (const value of overlay) {
391
+ if (typeof value === "string") continue;
392
+ if (state.remainingNodes <= 0) {
393
+ return { comparison: "unknown", emittedWidensIntersection: false, filteredIntersectionValues: 0 };
394
+ }
395
+ state.remainingNodes--;
396
+ let duplicate = false;
397
+ for (const candidate of uniqueFiltered) {
398
+ const comparison = boundedJsonEqual(value, candidate, state);
399
+ if (comparison === "unknown") {
400
+ return { comparison, emittedWidensIntersection: false, filteredIntersectionValues: 0 };
401
+ }
402
+ if (comparison === "equal") {
403
+ duplicate = true;
404
+ break;
405
+ }
406
+ }
407
+ if (!duplicate) uniqueFiltered.push(value);
408
+ }
409
+
410
+ let filteredIntersectionValues = 0;
411
+ for (const value of uniqueFiltered) {
412
+ for (const candidate of target) {
413
+ const comparison = boundedJsonEqual(value, candidate, state);
414
+ if (comparison === "unknown") {
415
+ return { comparison, emittedWidensIntersection: false, filteredIntersectionValues: 0 };
416
+ }
417
+ if (comparison === "equal") {
418
+ filteredIntersectionValues++;
419
+ break;
420
+ }
421
+ }
422
+ }
423
+ return { comparison: "equal", emittedWidensIntersection, filteredIntersectionValues };
424
+ }
425
+
426
+ function compareMergedValue(key: string, left: unknown, right: unknown): BoundedEquality {
427
+ if (key === "enum" && Array.isArray(left) && Array.isArray(right)) {
428
+ return boundedJsonSetEqual(left, right);
429
+ }
430
+ if (key === "required" && Array.isArray(left) && Array.isArray(right)) {
431
+ if (left.every(value => typeof value === "string") && right.every(value => typeof value === "string")) {
432
+ return boundedStringSetEqual(left, right, value => value);
433
+ }
434
+ return boundedJsonEqual(left, right);
435
+ }
436
+ if (key === "type") {
437
+ const leftValues = Array.isArray(left) ? left : [left];
438
+ const rightValues = Array.isArray(right) ? right : [right];
439
+ if (leftValues.every(value => typeof value === "string")
440
+ && rightValues.every(value => typeof value === "string")) {
441
+ return boundedStringSetEqual(left, right, value => value.toLowerCase());
442
+ }
443
+ return boundedJsonEqual(left, right);
444
+ }
445
+ if (BOUNDED_VALUE_KEYS.has(key)) return boundedJsonEqual(left, right);
446
+ return Object.is(left, right) ? "equal" : "different";
447
+ }
448
+
449
+ function mergeRefTarget(target: Schema, overlay: Schema, state: SanitizeState): Schema {
59
450
  const merged: Schema = {};
60
451
  if (Object.hasOwn(target, "$ref")) merged.$ref = target.$ref;
61
452
  for (const key of MERGED_SCHEMA_KEYS) {
62
- if (Object.hasOwn(overlay, key)) merged[key] = overlay[key];
63
- else if (Object.hasOwn(target, key)) merged[key] = target[key];
453
+ if (Object.hasOwn(overlay, key)) {
454
+ if (key !== "description" && Object.hasOwn(target, key)) {
455
+ if (key === "enum" && Array.isArray(target[key]) && Array.isArray(overlay[key])) {
456
+ const accounting = boundedEnumOverlayAccounting(target[key], overlay[key]);
457
+ if (accounting.comparison === "unknown") {
458
+ addGoogleToolSchemaUncertainty(state.report);
459
+ } else {
460
+ if (accounting.emittedWidensIntersection) {
461
+ addGoogleToolSchemaLoss(state.report, "ref-overlay-replaced");
462
+ }
463
+ addGoogleToolSchemaLoss(
464
+ state.report,
465
+ "enum-value-filtered",
466
+ accounting.filteredIntersectionValues,
467
+ );
468
+ }
469
+ // The intersection-aware accounting above owns this merged enum's filtering report.
470
+ state.enumFilterLossOverrides.set(merged, 0);
471
+ } else {
472
+ const comparison = compareMergedValue(key, overlay[key], target[key]);
473
+ if (comparison === "different") addGoogleToolSchemaLoss(state.report, "ref-overlay-replaced");
474
+ else if (comparison === "unknown") addGoogleToolSchemaUncertainty(state.report);
475
+ }
476
+ }
477
+ merged[key] = overlay[key];
478
+ } else if (Object.hasOwn(target, key)) merged[key] = target[key];
64
479
  }
65
480
  return merged;
66
481
  }
67
482
 
68
- function normalizeType(value: unknown, out: Schema, preserveNullType: boolean): void {
483
+ function normalizeType(
484
+ value: unknown,
485
+ out: Schema,
486
+ preserveNullType: boolean,
487
+ state: SanitizeState,
488
+ ): void {
69
489
  const candidates = Array.isArray(value) ? value : [value];
70
490
  let sawNull = false;
491
+ const nonNullTypes = new Set<string>();
492
+ let unsupported = 0;
493
+ if (Array.isArray(value) && value.length === 0) unsupported++;
71
494
 
72
495
  for (const candidate of candidates) {
73
- if (typeof candidate !== "string") continue;
496
+ if (typeof candidate !== "string") {
497
+ if (candidate !== undefined) unsupported++;
498
+ continue;
499
+ }
74
500
  const type = candidate.toLowerCase();
75
501
  if (type === "null") {
76
502
  sawNull = true;
77
- } else if (out.type === undefined && ALLOWED_TYPES.has(type)) {
78
- out.type = type;
503
+ } else if (ALLOWED_TYPES.has(type)) {
504
+ nonNullTypes.add(type);
505
+ if (out.type === undefined) out.type = type;
506
+ } else {
507
+ unsupported++;
79
508
  }
80
509
  }
81
510
 
511
+ addGoogleToolSchemaLoss(state.report, "unsupported-type-widened", unsupported);
512
+ if (nonNullTypes.size > 1) addGoogleToolSchemaLoss(state.report, "type-union-collapsed");
513
+
82
514
  if (!sawNull) return;
83
515
  if (out.type !== undefined) out.nullable = true;
84
516
  else if (preserveNullType) out.type = "null";
85
- else out.nullable = true;
517
+ else {
518
+ out.nullable = true;
519
+ addGoogleToolSchemaLoss(state.report, "unsupported-type-widened");
520
+ }
86
521
  }
87
522
 
88
- function sanitizeEnum(value: unknown): string[] | undefined {
523
+ function sanitizeEnum(value: unknown, state?: SanitizeState, filteredLossOverride?: number): string[] | undefined {
89
524
  if (!Array.isArray(value)) return undefined;
90
- const values = [...new Set(value.filter((item): item is string => typeof item === "string"))];
525
+ const stringValues = value.filter((item): item is string => typeof item === "string");
526
+ if (state) addGoogleToolSchemaLoss(
527
+ state.report,
528
+ "enum-value-filtered",
529
+ filteredLossOverride ?? (value.length - stringValues.length),
530
+ );
531
+ const values = [...new Set(stringValues)];
91
532
  return values.length > 0 ? values : undefined;
92
533
  }
93
534
 
@@ -98,10 +539,16 @@ function normalizeAnyOf(
98
539
  refDepth: number,
99
540
  state: SanitizeState,
100
541
  ): Schema {
101
- if (!Array.isArray(value) || value.length === 0) return {};
542
+ if (!Array.isArray(value) || value.length === 0) {
543
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
544
+ return {};
545
+ }
102
546
  const schemas: Schema[] = [];
103
547
  for (let index = 0; index < value.length; index++) {
104
- if (state.remainingNodes <= 0) return {};
548
+ if (state.remainingNodes <= 0) {
549
+ reportBudgetExhausted(state);
550
+ return {};
551
+ }
105
552
  const schema = sanitizeSchema(value[index], defs, depth + 1, refDepth, true, state);
106
553
  if (schema === BUDGET_EXHAUSTED) return {};
107
554
  schemas.push(schema);
@@ -130,6 +577,7 @@ function normalizeAnyOf(
130
577
 
131
578
  // CCA's Claude bridge turns typed anyOf branches into an invalid input_schema. Widen only this
132
579
  // node when a union cannot be collapsed losslessly; parent annotations and structure survive.
580
+ addGoogleToolSchemaLoss(state.report, "union-widened");
133
581
  return {};
134
582
  }
135
583
 
@@ -144,7 +592,10 @@ function sanitizeProperties(
144
592
  const properties: Record<string, Schema> = Object.create(null) as Record<string, Schema>;
145
593
  for (const name in value) {
146
594
  if (!Object.hasOwn(value, name)) continue;
147
- if (state.remainingNodes <= 0) break;
595
+ if (state.remainingNodes <= 0) {
596
+ reportBudgetExhausted(state);
597
+ break;
598
+ }
148
599
  // Property names form a name bag and must never be interpreted as schema keywords.
149
600
  const schema = sanitizeSchema(value[name], defs, depth + 1, refDepth, false, state);
150
601
  if (schema === BUDGET_EXHAUSTED) break;
@@ -161,78 +612,191 @@ function sanitizeSchema(
161
612
  preserveNullType: boolean,
162
613
  state: SanitizeState,
163
614
  ): SanitizeResult {
164
- if (state.remainingNodes <= 0) return BUDGET_EXHAUSTED;
615
+ if (state.remainingNodes <= 0) {
616
+ reportBudgetExhausted(state);
617
+ return BUDGET_EXHAUSTED;
618
+ }
165
619
  state.remainingNodes -= 1;
166
- if (depth >= MAX_SCHEMA_DEPTH || !isRecord(node)) return {};
620
+ if (depth >= MAX_SCHEMA_DEPTH) {
621
+ addGoogleToolSchemaLoss(state.report, "depth-limit-widened");
622
+ return {};
623
+ }
624
+ if (!isRecord(node)) {
625
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
626
+ return {};
627
+ }
628
+
629
+ reportDroppedConstraints(node, state);
167
630
 
168
- if (typeof node.$ref === "string" && refDepth < MAX_DEREF_DEPTH) {
631
+ if (Object.hasOwn(node, "$ref") && typeof node.$ref !== "string") {
632
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
633
+ } else if (typeof node.$ref === "string" && refDepth >= MAX_DEREF_DEPTH) {
634
+ addGoogleToolSchemaLoss(state.report, "dereference-limit-widened");
635
+ } else if (typeof node.$ref === "string") {
169
636
  const target = resolveRef(node.$ref, defs);
170
637
  if (isRecord(target)) {
171
- if (state.activeRefs.has(node.$ref)) return {};
638
+ if (state.activeRefs.has(node.$ref)) {
639
+ addGoogleToolSchemaLoss(state.report, "recursive-ref-widened");
640
+ return {};
641
+ }
172
642
  state.activeRefs.add(node.$ref);
643
+ // Constraints outside the merge allowlist are discarded here and must be counted before
644
+ // selecting the safe view. Copied keys are deliberately excluded to avoid double counting.
645
+ reportDroppedConstraints(target, state);
173
646
  // Select only inputs the sanitizer can consume. Spreading an untrusted definition here would
174
647
  // enumerate and allocate every unsupported annotation before the node budget can stop work.
175
- const merged = mergeRefTarget(target, node);
648
+ const merged = mergeRefTarget(target, node, state);
176
649
  try {
177
650
  return sanitizeSchema(merged, defs, depth, refDepth + 1, preserveNullType, state);
178
651
  } finally {
179
652
  state.activeRefs.delete(node.$ref);
180
653
  }
654
+ } else {
655
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
181
656
  }
182
657
  }
183
658
 
184
659
  const out: Schema = {};
185
- normalizeType(node.type, out, preserveNullType);
660
+ if (Object.hasOwn(node, "type") && node.type === undefined) {
661
+ addGoogleToolSchemaLoss(state.report, "unsupported-type-widened");
662
+ }
663
+ normalizeType(node.type, out, preserveNullType, state);
186
664
 
665
+ const typeIncludesNull = (Array.isArray(node.type) ? node.type : [node.type])
666
+ .some(candidate => typeof candidate === "string" && candidate.toLowerCase() === "null");
667
+ if (node.nullable === false && typeIncludesNull) {
668
+ addGoogleToolSchemaLoss(state.report, "nullability-overridden");
669
+ }
187
670
  if (typeof node.nullable === "boolean") out.nullable = node.nullable;
671
+ else if (Object.hasOwn(node, "nullable")) addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
188
672
  if (typeof node.description === "string") out.description = node.description;
189
673
  if (typeof node.format === "string") out.format = node.format;
674
+ else if (Object.hasOwn(node, "format")) addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
190
675
 
191
- const enumValues = sanitizeEnum(node.enum ?? (typeof node.const === "string" ? [node.const] : undefined));
676
+ if (Object.hasOwn(node, "enum") && !Array.isArray(node.enum)) {
677
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
678
+ } else if (Array.isArray(node.enum) && node.enum.length === 0) {
679
+ addGoogleToolSchemaLoss(state.report, "enum-constraint-dropped");
680
+ }
681
+ if (Object.hasOwn(node, "const")) {
682
+ if (typeof node.const !== "string") {
683
+ addGoogleToolSchemaLoss(state.report, "const-value-filtered");
684
+ } else if (Object.hasOwn(node, "enum")) {
685
+ const enumValues = Array.isArray(node.enum)
686
+ ? [...new Set(node.enum.filter((item): item is string => typeof item === "string"))]
687
+ : [];
688
+ if (enumValues.length !== 1 || enumValues[0] !== node.const) {
689
+ addGoogleToolSchemaLoss(state.report, "const-value-filtered");
690
+ }
691
+ }
692
+ }
693
+ const enumValues = sanitizeEnum(
694
+ node.enum ?? (typeof node.const === "string" ? [node.const] : undefined),
695
+ state,
696
+ state.enumFilterLossOverrides.get(node),
697
+ );
192
698
  if (enumValues) out.enum = enumValues;
193
699
 
700
+ if (Object.hasOwn(node, "properties") && !isRecord(node.properties)) {
701
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
702
+ }
194
703
  const properties = sanitizeProperties(node.properties, defs, depth, refDepth, state);
195
704
  if (properties) out.properties = properties;
196
705
 
197
706
  if (properties && Array.isArray(node.required)) {
198
- const required = [...new Set(node.required.filter((item): item is string => (
199
- typeof item === "string" && Object.hasOwn(properties, item)
200
- )))];
707
+ const stringRequired = node.required.filter((item): item is string => typeof item === "string");
708
+ const uniqueRequired = [...new Set(stringRequired)];
709
+ const required = uniqueRequired.filter(item => Object.hasOwn(properties, item));
710
+ addGoogleToolSchemaLoss(
711
+ state.report,
712
+ "unsupported-constraint-dropped",
713
+ uniqueRequired.length - required.length,
714
+ );
715
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened", node.required.length - stringRequired.length);
201
716
  if (required.length > 0) out.required = required;
717
+ } else if (!properties && Array.isArray(node.required)) {
718
+ const stringRequired = node.required.filter((item): item is string => typeof item === "string");
719
+ const required = new Set(stringRequired);
720
+ addGoogleToolSchemaLoss(state.report, "unsupported-constraint-dropped", required.size);
721
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened", node.required.length - stringRequired.length);
722
+ } else if (Object.hasOwn(node, "required") && !Array.isArray(node.required)) {
723
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
202
724
  }
203
725
 
204
- if (state.remainingNodes <= 0) return out;
726
+ if (state.remainingNodes <= 0) {
727
+ if (Object.hasOwn(node, "items") || Object.hasOwn(node, "anyOf")) reportBudgetExhausted(state);
728
+ return out;
729
+ }
205
730
 
206
- if (isRecord(node.items)) {
731
+ if (Array.isArray(node.items)) {
732
+ addGoogleToolSchemaLoss(state.report, "tuple-prefix-dropped");
733
+ } else if (isRecord(node.items)) {
207
734
  const items = sanitizeSchema(node.items, defs, depth + 1, refDepth, false, state);
208
735
  if (items !== BUDGET_EXHAUSTED) out.items = items;
736
+ } else if (Object.hasOwn(node, "items")) {
737
+ addGoogleToolSchemaLoss(state.report, "invalid-schema-widened");
209
738
  }
210
739
 
211
- if (state.remainingNodes <= 0) return out;
212
- if (node.anyOf !== undefined) {
213
- Object.assign(out, normalizeAnyOf(node.anyOf, defs, depth, refDepth, state));
740
+ if (state.remainingNodes <= 0) {
741
+ if (Object.hasOwn(node, "anyOf")) reportBudgetExhausted(state);
742
+ return out;
743
+ }
744
+ if (Object.hasOwn(node, "anyOf")) {
745
+ const normalized = normalizeAnyOf(node.anyOf, defs, depth, refDepth, state);
746
+ for (const key of RETAINED_UNION_KEYS) {
747
+ if (Object.hasOwn(out, key) && Object.hasOwn(normalized, key)
748
+ ) {
749
+ const comparison = compareMergedValue(key, out[key], normalized[key]);
750
+ if (comparison === "different") addGoogleToolSchemaLoss(state.report, "union-sibling-replaced");
751
+ else if (comparison === "unknown") addGoogleToolSchemaUncertainty(state.report);
752
+ }
753
+ }
754
+ Object.assign(out, normalized);
214
755
  }
215
756
  return out;
216
757
  }
217
758
 
218
- export function sanitizeGeminiToolParameters(parameters: unknown): Record<string, unknown> {
759
+ export function sanitizeGeminiToolParametersWithReport(
760
+ parameters: unknown,
761
+ profile: GoogleToolSchemaProfile,
762
+ ): GoogleToolSchemaSanitizeResult {
763
+ const report = createGoogleToolSchemaLossReport(profile);
764
+ if (parameters === undefined) {
765
+ return { parameters: { type: "object", properties: {} }, lossReport: report };
766
+ }
219
767
  try {
220
768
  const defs = new Map<string, unknown>();
221
769
  collectDefs(parameters, defs);
222
770
  const state: SanitizeState = {
223
771
  activeRefs: new Set(),
224
772
  remainingNodes: MAX_SCHEMA_NODES,
773
+ budgetReported: false,
774
+ enumFilterLossOverrides: new WeakMap(),
775
+ report,
225
776
  };
777
+ if (isRecord(parameters)) {
778
+ for (const bag of ["$defs", "definitions"] as const) {
779
+ if (Object.hasOwn(parameters, bag) && !isRecord(parameters[bag])) {
780
+ addGoogleToolSchemaLoss(report, "invalid-schema-widened");
781
+ }
782
+ }
783
+ }
226
784
  const sanitized = sanitizeSchema(parameters, defs, 0, 0, false, state);
227
785
  const root = sanitized === BUDGET_EXHAUSTED ? {} : sanitized;
228
786
 
229
787
  // Function arguments are always an object. Claude additionally rejects root composition and a
230
788
  // missing root type even when those forms are valid general-purpose JSON Schema.
789
+ if (root.type !== "object") addGoogleToolSchemaLoss(report, "root-object-coerced");
231
790
  root.type = "object";
232
791
  if (!isRecord(root.properties)) root.properties = {};
233
- return root;
792
+ return { parameters: root, lossReport: report };
234
793
  } catch {
235
794
  // Last-resort containment: no third-party schema may break every tool in the request.
236
- return { type: "object", properties: {} };
795
+ addGoogleToolSchemaLoss(report, "invalid-schema-widened");
796
+ return { parameters: { type: "object", properties: {} }, lossReport: report };
237
797
  }
238
798
  }
799
+
800
+ export function sanitizeGeminiToolParameters(parameters: unknown): Record<string, unknown> {
801
+ return sanitizeGeminiToolParametersWithReport(parameters, { endpointClass: "ai-studio" }).parameters;
802
+ }