@f5-sales-demo/pi-ai 19.51.2
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/CHANGELOG.md +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { logger } from "@f5xc-salesdemos/pi-utils";
|
|
2
|
+
import type { AnySchema } from "ajv";
|
|
3
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
4
|
+
import { areJsonValuesEqual, mergePropertySchemas } from "./equality";
|
|
5
|
+
import { CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS } from "./fields";
|
|
6
|
+
import { sanitizeSchemaForCCA } from "./sanitize-google";
|
|
7
|
+
import type { JsonObject } from "./types";
|
|
8
|
+
import { isJsonObject } from "./types";
|
|
9
|
+
|
|
10
|
+
/** Copy all keys from a schema except the specified combiner key. */
|
|
11
|
+
export function copySchemaWithout(schema: JsonObject, combiner: string): JsonObject {
|
|
12
|
+
const { [combiner]: _, ...rest } = schema;
|
|
13
|
+
return rest;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Claude via Cloud Code Assist (`parameters` path) can reject schemas that keep
|
|
18
|
+
* object variant combiners, so flatten object-only unions into one object shape.
|
|
19
|
+
*/
|
|
20
|
+
function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
|
|
21
|
+
const variantsRaw = schema[combiner];
|
|
22
|
+
if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
|
|
23
|
+
return schema;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const variants: JsonObject[] = [];
|
|
27
|
+
for (const entry of variantsRaw) {
|
|
28
|
+
if (!isJsonObject(entry)) {
|
|
29
|
+
return schema;
|
|
30
|
+
}
|
|
31
|
+
const variantType = entry.type;
|
|
32
|
+
const hasObjectShape =
|
|
33
|
+
isJsonObject(entry.properties) ||
|
|
34
|
+
Array.isArray(entry.required) ||
|
|
35
|
+
Object.hasOwn(entry, "additionalProperties");
|
|
36
|
+
if (variantType === undefined && !hasObjectShape) {
|
|
37
|
+
return schema;
|
|
38
|
+
}
|
|
39
|
+
if (variantType !== undefined && variantType !== "object") {
|
|
40
|
+
return schema;
|
|
41
|
+
}
|
|
42
|
+
if (entry.properties !== undefined && !isJsonObject(entry.properties)) {
|
|
43
|
+
return schema;
|
|
44
|
+
}
|
|
45
|
+
if (entry.required !== undefined && !Array.isArray(entry.required)) {
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
variants.push(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const mergedProperties: JsonObject = {};
|
|
52
|
+
const ownProperties = isJsonObject(schema.properties) ? schema.properties : {};
|
|
53
|
+
for (const [name, propertySchema] of Object.entries(ownProperties)) {
|
|
54
|
+
mergedProperties[name] = propertySchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const variant of variants) {
|
|
58
|
+
const properties = isJsonObject(variant.properties) ? variant.properties : {};
|
|
59
|
+
for (const [name, propertySchema] of Object.entries(properties)) {
|
|
60
|
+
const existingSchema = mergedProperties[name];
|
|
61
|
+
mergedProperties[name] =
|
|
62
|
+
existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nextSchema = copySchemaWithout(schema, combiner);
|
|
67
|
+
|
|
68
|
+
nextSchema.type = "object";
|
|
69
|
+
nextSchema.properties = mergedProperties;
|
|
70
|
+
|
|
71
|
+
// Compute intersection of all variants' required arrays
|
|
72
|
+
let requiredIntersection: string[] | undefined;
|
|
73
|
+
for (const variant of variants) {
|
|
74
|
+
const variantRequired = Array.isArray(variant.required)
|
|
75
|
+
? variant.required.filter((r): r is string => typeof r === "string")
|
|
76
|
+
: [];
|
|
77
|
+
if (requiredIntersection === undefined) {
|
|
78
|
+
requiredIntersection = [...variantRequired];
|
|
79
|
+
} else {
|
|
80
|
+
const reqSet = new Set(variantRequired);
|
|
81
|
+
requiredIntersection = requiredIntersection.filter(r => reqSet.has(r));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const parentRequired = Array.isArray(schema.required)
|
|
85
|
+
? schema.required.filter((r): r is string => typeof r === "string")
|
|
86
|
+
: [];
|
|
87
|
+
const ownPropertyNames = new Set(Object.keys(ownProperties));
|
|
88
|
+
const safeRequired = new Set<string>();
|
|
89
|
+
for (const name of requiredIntersection ?? []) {
|
|
90
|
+
if (name in mergedProperties) safeRequired.add(name);
|
|
91
|
+
}
|
|
92
|
+
for (const name of parentRequired) {
|
|
93
|
+
if (ownPropertyNames.has(name) && name in mergedProperties) {
|
|
94
|
+
safeRequired.add(name);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const requiredInPropertyOrder = Object.keys(mergedProperties).filter(name => safeRequired.has(name));
|
|
98
|
+
if (requiredInPropertyOrder.length > 0) {
|
|
99
|
+
nextSchema.required = requiredInPropertyOrder;
|
|
100
|
+
} else {
|
|
101
|
+
delete nextSchema.required;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return nextSchema;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Collapse anyOf/oneOf with distinct typed variants into a single-type schema.
|
|
109
|
+
* Picks the first non-null type as a scalar. This is lossy for multi-type unions
|
|
110
|
+
* (e.g., string|number|null narrows to string), but CCA requires a scalar type field
|
|
111
|
+
* and an uncollapsed anyOf would be rejected by the CCA API at runtime.
|
|
112
|
+
*/
|
|
113
|
+
function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
|
|
114
|
+
const variantsRaw = schema[combiner];
|
|
115
|
+
if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
|
|
116
|
+
return schema;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const seenTypes = new Set<string>();
|
|
120
|
+
const variantTypes: string[] = [];
|
|
121
|
+
const mergedVariantFields: JsonObject = {};
|
|
122
|
+
for (const entry of variantsRaw) {
|
|
123
|
+
if (!isJsonObject(entry) || typeof entry.type !== "string") {
|
|
124
|
+
return schema;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const variantType = entry.type;
|
|
128
|
+
if (seenTypes.has(variantType)) {
|
|
129
|
+
return schema;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const allowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[variantType];
|
|
133
|
+
if (!allowedKeys) {
|
|
134
|
+
return schema;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const [key, variantValue] of Object.entries(entry)) {
|
|
138
|
+
if (key === "type") continue;
|
|
139
|
+
if (!allowedKeys.has(key) && !CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS.has(key)) {
|
|
140
|
+
return schema;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const existingValue = mergedVariantFields[key];
|
|
144
|
+
if (existingValue !== undefined && !areJsonValuesEqual(existingValue, variantValue)) {
|
|
145
|
+
return schema;
|
|
146
|
+
}
|
|
147
|
+
mergedVariantFields[key] = variantValue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
seenTypes.add(variantType);
|
|
151
|
+
variantTypes.push(variantType);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
|
|
155
|
+
return schema;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nextSchema = copySchemaWithout(schema, combiner);
|
|
159
|
+
|
|
160
|
+
const nonNullTypes = variantTypes.filter(t => t !== "null");
|
|
161
|
+
// Lossy: when multiple non-null types exist we pick the first. CCA requires
|
|
162
|
+
// a scalar type and keeping the anyOf would cause an API rejection at runtime.
|
|
163
|
+
nextSchema.type = nonNullTypes[0] ?? variantTypes[0];
|
|
164
|
+
for (const [key, value] of Object.entries(mergedVariantFields)) {
|
|
165
|
+
const existingValue = nextSchema[key];
|
|
166
|
+
if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
|
|
167
|
+
return schema;
|
|
168
|
+
}
|
|
169
|
+
if (existingValue === undefined) {
|
|
170
|
+
nextSchema[key] = value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return nextSchema;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Collapse anyOf/oneOf where all variants share the same primitive type.
|
|
178
|
+
* E.g. anyOf: [{type: "string", desc: "A"}, {type: "string", desc: "B"}] -> {type: "string", desc: "A"}
|
|
179
|
+
* Claude via CCA rejects any remaining anyOf/oneOf, so pick first variant.
|
|
180
|
+
* Note: constraints from non-first variants are silently dropped.
|
|
181
|
+
*/
|
|
182
|
+
function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
|
|
183
|
+
const variantsRaw = schema[combiner];
|
|
184
|
+
if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) return schema;
|
|
185
|
+
let commonType: string | undefined;
|
|
186
|
+
let firstEntry: JsonObject | undefined;
|
|
187
|
+
for (const entry of variantsRaw) {
|
|
188
|
+
if (!isJsonObject(entry) || typeof entry.type !== "string") return schema;
|
|
189
|
+
if (commonType === undefined) {
|
|
190
|
+
commonType = entry.type;
|
|
191
|
+
firstEntry = entry;
|
|
192
|
+
} else if (entry.type !== commonType) return schema;
|
|
193
|
+
}
|
|
194
|
+
if (!firstEntry) return schema;
|
|
195
|
+
const nextSchema = copySchemaWithout(schema, combiner);
|
|
196
|
+
for (const [key, value] of Object.entries(firstEntry)) {
|
|
197
|
+
if (!(key in nextSchema)) nextSchema[key] = value;
|
|
198
|
+
}
|
|
199
|
+
return nextSchema;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Recursively strip any remaining anyOf/oneOf that collapseSameTypeCombinerVariants can handle.
|
|
204
|
+
* This is needed because mergeObjectCombinerVariants can create new anyOf in merged
|
|
205
|
+
* properties AFTER the recursive normalization pass has already processed children.
|
|
206
|
+
*/
|
|
207
|
+
export function stripResidualCombiners(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
if (seen.has(value)) return [];
|
|
210
|
+
seen.add(value);
|
|
211
|
+
return value.map(entry => stripResidualCombiners(entry, seen));
|
|
212
|
+
}
|
|
213
|
+
if (!isJsonObject(value)) return value;
|
|
214
|
+
if (seen.has(value)) return {};
|
|
215
|
+
seen.add(value);
|
|
216
|
+
const result: JsonObject = {};
|
|
217
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
218
|
+
result[key] = stripResidualCombiners(entry, seen);
|
|
219
|
+
}
|
|
220
|
+
let current: JsonObject = result;
|
|
221
|
+
let changed = true;
|
|
222
|
+
while (changed) {
|
|
223
|
+
changed = false;
|
|
224
|
+
for (const combiner of ["anyOf", "oneOf"] as const) {
|
|
225
|
+
const sameType = collapseSameTypeCombinerVariants(current, combiner);
|
|
226
|
+
if (sameType !== current) {
|
|
227
|
+
current = sameType;
|
|
228
|
+
changed = true;
|
|
229
|
+
}
|
|
230
|
+
const mixed = collapseMixedTypeCombinerVariants(current, combiner);
|
|
231
|
+
if (mixed !== current) {
|
|
232
|
+
current = mixed;
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function normalizeSchemaForCCA(value: unknown, seen?: WeakSet<object>): unknown {
|
|
241
|
+
if (!seen) seen = new WeakSet();
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
if (seen.has(value)) return [];
|
|
244
|
+
seen.add(value);
|
|
245
|
+
return value.map(entry => normalizeSchemaForCCA(entry, seen));
|
|
246
|
+
}
|
|
247
|
+
if (!isJsonObject(value)) {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
if (seen.has(value)) return {};
|
|
251
|
+
seen.add(value);
|
|
252
|
+
|
|
253
|
+
const normalized: JsonObject = {};
|
|
254
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
255
|
+
normalized[key] = normalizeSchemaForCCA(entry, seen);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const mergedAnyOf = mergeObjectCombinerVariants(normalized, "anyOf");
|
|
259
|
+
const collapsedAnyOf = collapseMixedTypeCombinerVariants(mergedAnyOf, "anyOf");
|
|
260
|
+
const sameTypeAnyOf = collapseSameTypeCombinerVariants(collapsedAnyOf, "anyOf");
|
|
261
|
+
const mergedOneOf = mergeObjectCombinerVariants(sameTypeAnyOf, "oneOf");
|
|
262
|
+
const collapsedOneOf = collapseMixedTypeCombinerVariants(mergedOneOf, "oneOf");
|
|
263
|
+
return collapseSameTypeCombinerVariants(collapsedOneOf, "oneOf");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface NullableExtractionResult {
|
|
267
|
+
schema: unknown;
|
|
268
|
+
nullable: boolean;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
|
|
272
|
+
if (!isJsonObject(schema)) {
|
|
273
|
+
return { schema, nullable: false };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (schema.nullable === true) {
|
|
277
|
+
const nextSchema = { ...schema };
|
|
278
|
+
delete nextSchema.nullable;
|
|
279
|
+
return { schema: nextSchema, nullable: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (Array.isArray(schema.type)) {
|
|
283
|
+
const typeVariants = schema.type.filter((entry): entry is string => typeof entry === "string");
|
|
284
|
+
const nonNullTypes = typeVariants.filter(entry => entry !== "null");
|
|
285
|
+
if (typeVariants.includes("null") && nonNullTypes.length === 1) {
|
|
286
|
+
const nextSchema = { ...schema, type: nonNullTypes[0] };
|
|
287
|
+
return { schema: nextSchema, nullable: true };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const combiner of ["anyOf", "oneOf"] as const) {
|
|
292
|
+
const variantsRaw = schema[combiner];
|
|
293
|
+
if (!Array.isArray(variantsRaw)) continue;
|
|
294
|
+
|
|
295
|
+
let hasNullVariant = false;
|
|
296
|
+
const nonNullVariants: unknown[] = [];
|
|
297
|
+
for (const variant of variantsRaw) {
|
|
298
|
+
if (isJsonObject(variant) && variant.type === "null" && Object.keys(variant).length === 1) {
|
|
299
|
+
hasNullVariant = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
nonNullVariants.push(variant);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!hasNullVariant || nonNullVariants.length !== 1 || !isJsonObject(nonNullVariants[0])) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const nextSchema = copySchemaWithout(schema, combiner);
|
|
310
|
+
for (const [key, value] of Object.entries(nonNullVariants[0])) {
|
|
311
|
+
const existingValue = nextSchema[key];
|
|
312
|
+
if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
|
|
313
|
+
return { schema, nullable: false };
|
|
314
|
+
}
|
|
315
|
+
if (existingValue === undefined) {
|
|
316
|
+
nextSchema[key] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { schema: nextSchema, nullable: true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { schema, nullable: false };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
interface NullableNormalizationResult {
|
|
326
|
+
schema: unknown;
|
|
327
|
+
nullable: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function normalizeNullablePropertiesForCloudCodeAssist(
|
|
331
|
+
value: unknown,
|
|
332
|
+
isPropertySchema = false,
|
|
333
|
+
seen?: WeakSet<object>,
|
|
334
|
+
): NullableNormalizationResult {
|
|
335
|
+
if (!seen) seen = new WeakSet();
|
|
336
|
+
if (Array.isArray(value)) {
|
|
337
|
+
if (seen.has(value)) {
|
|
338
|
+
return { schema: [], nullable: false };
|
|
339
|
+
}
|
|
340
|
+
seen.add(value);
|
|
341
|
+
return {
|
|
342
|
+
schema: value.map(entry => normalizeNullablePropertiesForCloudCodeAssist(entry, false, seen).schema),
|
|
343
|
+
nullable: false,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (!isJsonObject(value)) {
|
|
347
|
+
return { schema: value, nullable: false };
|
|
348
|
+
}
|
|
349
|
+
if (seen.has(value)) {
|
|
350
|
+
return { schema: {}, nullable: false };
|
|
351
|
+
}
|
|
352
|
+
seen.add(value);
|
|
353
|
+
|
|
354
|
+
const normalized: JsonObject = {};
|
|
355
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
356
|
+
normalized[key] = normalizeNullablePropertiesForCloudCodeAssist(entry, false, seen).schema;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (isJsonObject(normalized.properties)) {
|
|
360
|
+
const required = new Set(
|
|
361
|
+
Array.isArray(normalized.required)
|
|
362
|
+
? normalized.required.filter((entry): entry is string => typeof entry === "string")
|
|
363
|
+
: [],
|
|
364
|
+
);
|
|
365
|
+
const nextProperties: JsonObject = {};
|
|
366
|
+
for (const [name, propertySchema] of Object.entries(normalized.properties)) {
|
|
367
|
+
const normalizedProperty = normalizeNullablePropertiesForCloudCodeAssist(propertySchema, true, seen);
|
|
368
|
+
nextProperties[name] = normalizedProperty.schema;
|
|
369
|
+
if (normalizedProperty.nullable) {
|
|
370
|
+
required.delete(name);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
normalized.properties = nextProperties;
|
|
374
|
+
if (Array.isArray(normalized.required)) {
|
|
375
|
+
normalized.required = Array.from(required);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!isPropertySchema) {
|
|
380
|
+
return { schema: normalized, nullable: false };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return extractNullableUnionSchema(normalized);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Lazy singleton AJV instance. Only used for validateSchema() — never compile().
|
|
388
|
+
* Do NOT use compile() on this instance: compiled schemas accumulate and leak memory.
|
|
389
|
+
* Note: Not thread-safe if Bun workers share this module.
|
|
390
|
+
*/
|
|
391
|
+
let cloudCodeAssistSchemaValidator: Ajv2020 | null = null;
|
|
392
|
+
function getCloudCodeAssistSchemaValidator(): Ajv2020 {
|
|
393
|
+
if (cloudCodeAssistSchemaValidator) {
|
|
394
|
+
return cloudCodeAssistSchemaValidator;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
cloudCodeAssistSchemaValidator = new Ajv2020({
|
|
398
|
+
allErrors: true,
|
|
399
|
+
strict: false,
|
|
400
|
+
validateSchema: true,
|
|
401
|
+
});
|
|
402
|
+
return cloudCodeAssistSchemaValidator;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Keep validation synchronous in this request path.
|
|
407
|
+
*/
|
|
408
|
+
function isValidCCASchema(schema: unknown): boolean {
|
|
409
|
+
try {
|
|
410
|
+
const result = getCloudCodeAssistSchemaValidator().validateSchema(schema as AnySchema);
|
|
411
|
+
return typeof result === "boolean" ? result : false;
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** See COMBINATOR_KEYS in fields.ts — CCA forbids all three combiners. */
|
|
418
|
+
const CCA_FORBIDDEN_COMBINERS = new Set(["anyOf", "oneOf", "allOf"] as const);
|
|
419
|
+
|
|
420
|
+
function hasResidualCloudCodeAssistIncompatibilities(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
421
|
+
if (Array.isArray(value)) {
|
|
422
|
+
if (seen.has(value)) return false;
|
|
423
|
+
seen.add(value);
|
|
424
|
+
return value.some(entry => hasResidualCloudCodeAssistIncompatibilities(entry, seen));
|
|
425
|
+
}
|
|
426
|
+
if (!isJsonObject(value)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
if (seen.has(value)) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
seen.add(value);
|
|
433
|
+
|
|
434
|
+
if (Array.isArray(value.type) || value.type === "null") {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (Object.hasOwn(value, "nullable")) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
for (const combiner of CCA_FORBIDDEN_COMBINERS) {
|
|
441
|
+
if (Array.isArray(value[combiner])) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const entry of Object.values(value)) {
|
|
446
|
+
if (hasResidualCloudCodeAssistIncompatibilities(entry, seen)) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
const CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA = {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {},
|
|
455
|
+
} as const;
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Prepare schema for Claude on Cloud Code Assist:
|
|
459
|
+
* sanitize -> normalize union objects -> validate -> fallback.
|
|
460
|
+
*
|
|
461
|
+
* Fallback is per-tool and fail-open to avoid rejecting the entire request when
|
|
462
|
+
* one tool schema is invalid.
|
|
463
|
+
*/
|
|
464
|
+
export function prepareSchemaForCCA(value: unknown): unknown {
|
|
465
|
+
const sanitized = sanitizeSchemaForCCA(value);
|
|
466
|
+
const pass1 = normalizeSchemaForCCA(sanitized);
|
|
467
|
+
// Second pass: strip anyOf/oneOf created by mergeObjectCombinerVariants during pass1
|
|
468
|
+
const normalized = stripResidualCombiners(pass1);
|
|
469
|
+
const nullableNormalized = normalizeNullablePropertiesForCloudCodeAssist(normalized).schema;
|
|
470
|
+
if (hasResidualCloudCodeAssistIncompatibilities(nullableNormalized)) {
|
|
471
|
+
logger.debug("CCA schema has residual incompatibilities, using fallback");
|
|
472
|
+
return CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA;
|
|
473
|
+
}
|
|
474
|
+
if (isValidCCASchema(nullableNormalized)) {
|
|
475
|
+
return nullableNormalized;
|
|
476
|
+
}
|
|
477
|
+
logger.debug("CCA schema failed validation, using fallback");
|
|
478
|
+
return CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA;
|
|
479
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { dereferenceJsonSchema } from "./dereference";
|
|
2
|
+
import { areJsonValuesEqual } from "./equality";
|
|
3
|
+
import { UNSUPPORTED_SCHEMA_FIELDS } from "./fields";
|
|
4
|
+
|
|
5
|
+
interface SanitizeSchemaOptions {
|
|
6
|
+
insideProperties: boolean;
|
|
7
|
+
normalizeTypeArrayToNullable: boolean;
|
|
8
|
+
stripNullableKeyword: boolean;
|
|
9
|
+
unsupportedFields: ReadonlySet<string>;
|
|
10
|
+
seen: WeakSet<object>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function inferJsonSchemaTypeFromValue(value: unknown): string | undefined {
|
|
14
|
+
if (value === null) return "null";
|
|
15
|
+
if (Array.isArray(value)) return "array";
|
|
16
|
+
switch (typeof value) {
|
|
17
|
+
case "string":
|
|
18
|
+
return "string";
|
|
19
|
+
case "number":
|
|
20
|
+
return "number";
|
|
21
|
+
case "boolean":
|
|
22
|
+
return "boolean";
|
|
23
|
+
case "object":
|
|
24
|
+
return "object";
|
|
25
|
+
default:
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pushEnumValue(values: unknown[], value: unknown): void {
|
|
31
|
+
if (!values.some(existing => areJsonValuesEqual(existing, value))) {
|
|
32
|
+
values.push(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sanitizeSchemaImpl(value: unknown, options: SanitizeSchemaOptions): unknown {
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
if (options.seen.has(value)) return [];
|
|
39
|
+
options.seen.add(value);
|
|
40
|
+
return value.map(entry => sanitizeSchemaImpl(entry, options));
|
|
41
|
+
}
|
|
42
|
+
if (!value || typeof value !== "object") {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (options.seen.has(value as object)) return {};
|
|
46
|
+
options.seen.add(value as object);
|
|
47
|
+
const obj = value as Record<string, unknown>;
|
|
48
|
+
const result: Record<string, unknown> = {};
|
|
49
|
+
for (const combiner of ["anyOf", "oneOf"] as const) {
|
|
50
|
+
if (Array.isArray(obj[combiner])) {
|
|
51
|
+
const variants = obj[combiner] as Record<string, unknown>[];
|
|
52
|
+
const allHaveConst = variants.every(v => v && typeof v === "object" && "const" in v);
|
|
53
|
+
if (allHaveConst && variants.length > 0) {
|
|
54
|
+
const dedupedEnum: unknown[] = [];
|
|
55
|
+
for (const variant of variants) {
|
|
56
|
+
pushEnumValue(dedupedEnum, variant.const);
|
|
57
|
+
}
|
|
58
|
+
result.enum = dedupedEnum;
|
|
59
|
+
|
|
60
|
+
const explicitTypes = variants
|
|
61
|
+
.map(variant => variant.type)
|
|
62
|
+
.filter((variantType): variantType is string => typeof variantType === "string");
|
|
63
|
+
const allHaveSameExplicitType =
|
|
64
|
+
explicitTypes.length === variants.length &&
|
|
65
|
+
explicitTypes.every(variantType => variantType === explicitTypes[0]);
|
|
66
|
+
if (allHaveSameExplicitType && explicitTypes[0]) {
|
|
67
|
+
result.type = explicitTypes[0];
|
|
68
|
+
} else {
|
|
69
|
+
const inferredTypes = dedupedEnum
|
|
70
|
+
.map(enumValue => inferJsonSchemaTypeFromValue(enumValue))
|
|
71
|
+
.filter((inferredType): inferredType is string => inferredType !== undefined);
|
|
72
|
+
const inferredTypeSet = new Set(inferredTypes);
|
|
73
|
+
if (inferredTypeSet.size === 1) {
|
|
74
|
+
result.type = inferredTypes[0];
|
|
75
|
+
} else {
|
|
76
|
+
const nonNullInferredTypes = inferredTypes.filter(inferredType => inferredType !== "null");
|
|
77
|
+
const nonNullTypeSet = new Set(nonNullInferredTypes);
|
|
78
|
+
if (inferredTypes.includes("null") && nonNullTypeSet.size === 1) {
|
|
79
|
+
result.type = nonNullInferredTypes[0];
|
|
80
|
+
if (!options.stripNullableKeyword) {
|
|
81
|
+
result.nullable = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Copy description and other top-level fields (not the combiner)
|
|
88
|
+
for (const [key, entry] of Object.entries(obj)) {
|
|
89
|
+
if (key !== combiner && !(key in result)) {
|
|
90
|
+
result[key] = sanitizeSchemaImpl(entry, {
|
|
91
|
+
...options,
|
|
92
|
+
insideProperties: key === "properties",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Regular field processing
|
|
101
|
+
let constValue: unknown;
|
|
102
|
+
for (const [key, entry] of Object.entries(obj)) {
|
|
103
|
+
// Only strip unsupported schema keywords when NOT inside "properties" object
|
|
104
|
+
// Inside "properties", keys are property names (e.g., "pattern") not schema keywords
|
|
105
|
+
if (!options.insideProperties && options.unsupportedFields.has(key)) continue;
|
|
106
|
+
if (options.stripNullableKeyword && key === "nullable") continue;
|
|
107
|
+
if (key === "const") {
|
|
108
|
+
constValue = entry;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// When key is "properties", child keys are property names, not schema keywords
|
|
112
|
+
result[key] = sanitizeSchemaImpl(entry, {
|
|
113
|
+
...options,
|
|
114
|
+
insideProperties: key === "properties",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Normalize array-valued "type" (e.g. ["string", "null"]) to a single type + nullable.
|
|
118
|
+
// Google's Schema proto expects type to be a single enum string, not an array.
|
|
119
|
+
if (options.normalizeTypeArrayToNullable && Array.isArray(result.type)) {
|
|
120
|
+
const types = (result.type as unknown[]).filter((t): t is string => typeof t === "string");
|
|
121
|
+
const nonNull = types.filter(t => t !== "null");
|
|
122
|
+
if (types.includes("null") && !options.stripNullableKeyword) {
|
|
123
|
+
result.nullable = true;
|
|
124
|
+
}
|
|
125
|
+
result.type = nonNull[0] ?? types[0];
|
|
126
|
+
}
|
|
127
|
+
if (constValue !== undefined) {
|
|
128
|
+
// Convert const to enum, merging with existing enum if present
|
|
129
|
+
const existingEnum = Array.isArray(result.enum) ? result.enum : [];
|
|
130
|
+
pushEnumValue(existingEnum, constValue);
|
|
131
|
+
result.enum = existingEnum;
|
|
132
|
+
if (!result.type) {
|
|
133
|
+
result.type = inferJsonSchemaTypeFromValue(constValue);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure object schemas have a properties field (some LLM providers require it)
|
|
138
|
+
if (result.type === "object" && !("properties" in result)) {
|
|
139
|
+
result.properties = {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Sanitize a JSON Schema for Google's generative AI APIs by stripping unsupported
|
|
147
|
+
* JSON Schema keywords and normalizing representable nullable/type patterns.
|
|
148
|
+
*
|
|
149
|
+
* **Prerequisite:** The input schema must be fully dereferenced — all `$ref`
|
|
150
|
+
* pointers resolved inline — before calling this function. `$ref` is silently
|
|
151
|
+
* stripped as an unsupported keyword; unresolved references will produce an
|
|
152
|
+
* incomplete schema with no warning.
|
|
153
|
+
*/
|
|
154
|
+
export function sanitizeSchemaForGoogle(value: unknown): unknown {
|
|
155
|
+
return sanitizeSchemaImpl(value, {
|
|
156
|
+
insideProperties: false,
|
|
157
|
+
normalizeTypeArrayToNullable: true,
|
|
158
|
+
stripNullableKeyword: false,
|
|
159
|
+
unsupportedFields: UNSUPPORTED_SCHEMA_FIELDS,
|
|
160
|
+
seen: new WeakSet(),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sanitize a JSON Schema for Cloud Code Assist Claude.
|
|
166
|
+
* Starts from Google sanitizer behavior, then strips `nullable` markers.
|
|
167
|
+
*
|
|
168
|
+
* **Prerequisite:** The input schema must be fully dereferenced — all `$ref`
|
|
169
|
+
* pointers resolved inline — before calling this function. `$ref` is silently
|
|
170
|
+
* stripped as an unsupported keyword; unresolved references will produce an
|
|
171
|
+
* incomplete schema with no warning.
|
|
172
|
+
*/
|
|
173
|
+
export function sanitizeSchemaForCCA(value: unknown): unknown {
|
|
174
|
+
return sanitizeSchemaImpl(value, {
|
|
175
|
+
insideProperties: false,
|
|
176
|
+
normalizeTypeArrayToNullable: true,
|
|
177
|
+
stripNullableKeyword: true,
|
|
178
|
+
unsupportedFields: UNSUPPORTED_SCHEMA_FIELDS,
|
|
179
|
+
seen: new WeakSet(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Fields stripped for MCP/AJV compatibility.
|
|
185
|
+
* Only `$schema` — AJV throws on unrecognised meta-schema URIs
|
|
186
|
+
* (e.g. draft 2020-12 emitted by schemars 1.x / rmcp 0.15+).
|
|
187
|
+
*/
|
|
188
|
+
const MCP_UNSUPPORTED_SCHEMA_FIELDS = new Set(["$schema"]);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sanitize a JSON Schema for MCP tool parameter validation (AJV compatibility).
|
|
192
|
+
*
|
|
193
|
+
* Strips only the minimal set of fields that cause AJV validation errors:
|
|
194
|
+
* - `$schema`: AJV throws on unknown meta-schema URIs.
|
|
195
|
+
* - `nullable`: OpenAPI 3.0 extension, not standard JSON Schema.
|
|
196
|
+
*
|
|
197
|
+
* Unlike the Google/CCA sanitizers this preserves validation keywords
|
|
198
|
+
* (`pattern`, `format`, `additionalProperties`, etc.) and `$ref`/`$defs`.
|
|
199
|
+
*/
|
|
200
|
+
export function sanitizeSchemaForMCP(value: unknown): unknown {
|
|
201
|
+
// Dereference $ref/$defs first — MCP servers emit standard JSON Schema
|
|
202
|
+
// with $defs, but providers (Anthropic, Google) only forward `properties`
|
|
203
|
+
// and `required`, dropping $defs and leaving dangling $ref pointers.
|
|
204
|
+
const dereferenced = dereferenceJsonSchema(value);
|
|
205
|
+
return sanitizeSchemaImpl(dereferenced, {
|
|
206
|
+
insideProperties: false,
|
|
207
|
+
normalizeTypeArrayToNullable: false,
|
|
208
|
+
stripNullableKeyword: true,
|
|
209
|
+
unsupportedFields: MCP_UNSUPPORTED_SCHEMA_FIELDS,
|
|
210
|
+
seen: new WeakSet(),
|
|
211
|
+
});
|
|
212
|
+
}
|