@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.
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- 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
|
-
|
|
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))
|
|
63
|
-
|
|
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(
|
|
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")
|
|
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 (
|
|
78
|
-
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
615
|
+
if (state.remainingNodes <= 0) {
|
|
616
|
+
reportBudgetExhausted(state);
|
|
617
|
+
return BUDGET_EXHAUSTED;
|
|
618
|
+
}
|
|
165
619
|
state.remainingNodes -= 1;
|
|
166
|
-
if (depth >= MAX_SCHEMA_DEPTH
|
|
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
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
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)
|
|
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 (
|
|
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)
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|