@gajae-code/ai 0.15.6 → 0.16.1

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 (51) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/types/adapter-internals/aws-region.d.ts +7 -0
  3. package/dist/types/core.d.ts +1 -0
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/provider-models/openai-compat.d.ts +1 -0
  6. package/dist/types/providers/anthropic.d.ts +1 -1
  7. package/dist/types/providers/cursor.d.ts +27 -1
  8. package/dist/types/providers/google-gemini-headers.d.ts +1 -1
  9. package/dist/types/providers/openai-codex-responses.d.ts +6 -0
  10. package/dist/types/utils/discovery/openai-compatible.d.ts +21 -0
  11. package/dist/types/utils/h2-fetch.d.ts +7 -0
  12. package/dist/types/utils/schema/normalize.d.ts +0 -5
  13. package/dist/types/utils/sqlite-errors.d.ts +4 -0
  14. package/package.json +3 -3
  15. package/src/adapter-internals/aws-region.d.ts +7 -0
  16. package/src/adapter-internals/aws-region.ts +14 -0
  17. package/src/auth-broker/server.ts +10 -1
  18. package/src/auth-storage.ts +14 -14
  19. package/src/core.ts +1 -0
  20. package/src/index.ts +1 -0
  21. package/src/model-thinking.ts +8 -0
  22. package/src/models.json +201 -3
  23. package/src/provider-models/openai-compat.ts +93 -8
  24. package/src/providers/amazon-bedrock.ts +5 -1
  25. package/src/providers/anthropic.d.ts +1 -1
  26. package/src/providers/anthropic.ts +1 -1
  27. package/src/providers/aws-credentials.ts +6 -0
  28. package/src/providers/cursor.d.ts +27 -1
  29. package/src/providers/cursor.ts +234 -17
  30. package/src/providers/google-gemini-headers.d.ts +1 -1
  31. package/src/providers/google-gemini-headers.ts +1 -1
  32. package/src/providers/kiro-api-key.ts +33 -8
  33. package/src/providers/kiro-codewhisperer.ts +4 -1
  34. package/src/providers/openai-codex-responses.d.ts +6 -0
  35. package/src/providers/openai-codex-responses.ts +17 -2
  36. package/src/providers/pi-native-client.ts +24 -1
  37. package/src/utils/discovery/antigravity.ts +10 -1
  38. package/src/utils/discovery/openai-compatible.ts +38 -0
  39. package/src/utils/h2-fetch.ts +10 -0
  40. package/src/utils/oauth/callback-server.ts +8 -1
  41. package/src/utils/oauth/glm-zcode.ts +1 -1
  42. package/src/utils/oauth/kiro.ts +91 -22
  43. package/src/utils/schema/dereference.ts +169 -49
  44. package/src/utils/schema/draft.ts +46 -23
  45. package/src/utils/schema/normalize.d.ts +0 -5
  46. package/src/utils/schema/normalize.ts +396 -119
  47. package/src/utils/schema/types.ts +3 -1
  48. package/src/utils/schema/zod-decontaminate.ts +83 -29
  49. package/src/utils/sqlite-errors.d.ts +4 -0
  50. package/src/utils/sqlite-errors.ts +13 -0
  51. package/src/utils/tool-choice-capability.ts +2 -3
@@ -70,6 +70,14 @@ const SNAKE_TO_CAMEL_RENAMES = new Map<string, string>([
70
70
 
71
71
  const JSON_SCHEMA_COMBINERS = ["anyOf", "oneOf"] as const;
72
72
  const CCA_FORBIDDEN_COMBINERS = new Set(["anyOf", "oneOf", "allOf"]);
73
+ const JSON_SCHEMA_MAP_KEYS = new Set([
74
+ "properties",
75
+ "patternProperties",
76
+ "dependencies",
77
+ "dependentSchemas",
78
+ "$defs",
79
+ "definitions",
80
+ ]);
73
81
 
74
82
  const CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA = {
75
83
  type: "object",
@@ -109,9 +117,9 @@ function applySnakeCaseRenames(obj: JsonObject): JsonObject {
109
117
  if (!Object.hasOwn(obj, k)) continue;
110
118
  const renamed = SNAKE_TO_CAMEL_RENAMES.get(k);
111
119
  if (renamed !== undefined) {
112
- out[renamed] = obj[k];
120
+ setOwnKey(out, renamed, obj[k]);
113
121
  } else if (!outHasOwn(out, k)) {
114
- out[k] = obj[k];
122
+ setOwnKey(out, k, obj[k]);
115
123
  }
116
124
  }
117
125
  return out;
@@ -124,21 +132,21 @@ function applySnakeCaseRenames(obj: JsonObject): JsonObject {
124
132
  * original reference otherwise (zero-allocation fast path).
125
133
  */
126
134
  function preHandleNullFields(obj: JsonObject): JsonObject {
127
- if (obj.type === "null") {
135
+ if (Object.hasOwn(obj, "type") && obj.type === "null") {
128
136
  const out: JsonObject = {};
129
137
  for (const k in obj) {
130
138
  if (!Object.hasOwn(obj, k) || k === "type") continue;
131
- out[k] = obj[k];
139
+ setOwnKey(out, k, obj[k]);
132
140
  }
133
141
  out.nullable = true;
134
142
  return out;
135
143
  }
136
- if (!Array.isArray(obj.anyOf)) return obj;
144
+ if (!Object.hasOwn(obj, "anyOf") || !Array.isArray(obj.anyOf)) return obj;
137
145
  const variants = obj.anyOf as unknown[];
138
146
  let sawNull = false;
139
147
  const kept: unknown[] = [];
140
148
  for (const v of variants) {
141
- if (isJsonObject(v) && v.type === "null") {
149
+ if (isJsonObject(v) && Object.hasOwn(v, "type") && v.type === "null") {
142
150
  sawNull = true;
143
151
  continue;
144
152
  }
@@ -147,7 +155,7 @@ function preHandleNullFields(obj: JsonObject): JsonObject {
147
155
  if (!sawNull) return obj;
148
156
  const out: JsonObject = {};
149
157
  for (const k in obj) {
150
- if (Object.hasOwn(obj, k)) out[k] = obj[k];
158
+ if (Object.hasOwn(obj, k)) setOwnKey(out, k, obj[k]);
151
159
  }
152
160
  out.nullable = true;
153
161
  if (kept.length === 0) {
@@ -156,7 +164,7 @@ function preHandleNullFields(obj: JsonObject): JsonObject {
156
164
  delete out.anyOf;
157
165
  const only = kept[0];
158
166
  for (const k in only) {
159
- if (Object.hasOwn(only, k) && !outHasOwn(out, k)) out[k] = only[k];
167
+ if (Object.hasOwn(only, k) && !outHasOwn(out, k)) setOwnKey(out, k, only[k]);
160
168
  }
161
169
  } else {
162
170
  out.anyOf = kept;
@@ -168,6 +176,50 @@ function outHasOwn(obj: JsonObject, key: string): boolean {
168
176
  return Object.hasOwn(obj, key);
169
177
  }
170
178
 
179
+ /**
180
+ * Setter-independent own-data write. `JSON.parse` produces `__proto__` as an own
181
+ * data property, but plain-object assignment routes it to `Object.prototype`'s
182
+ * `__proto__` setter, so the key would be silently dropped from the copy (and a
183
+ * schema-shaped value would mutate the copy's prototype). Every schema copy site
184
+ * writes through this helper so arbitrary property names round-trip as data.
185
+ */
186
+ function setOwnKey(target: JsonObject, key: string, value: unknown): void {
187
+ if (key === "__proto__") {
188
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
189
+ return;
190
+ }
191
+ target[key] = value;
192
+ }
193
+
194
+ /**
195
+ * JSON Schema keywords whose values are literal instance data, not subschemas.
196
+ * The schema walkers must copy them verbatim: recursing into them rewrites user
197
+ * payloads (a `default` of `{type:"object",nullable:true}` is a default value,
198
+ * not a nullable object schema).
199
+ */
200
+ const JSON_SCHEMA_LITERAL_PAYLOAD_KEYS = new Set(["default", "const", "enum", "examples"]);
201
+
202
+ /** Deep copy of literal payload data; preserves arbitrary keys and breaks aliasing to the caller's input. */
203
+ function cloneJsonLiteral(value: unknown, seen?: WeakMap<object, unknown>): unknown {
204
+ if (!value || typeof value !== "object") return value;
205
+ const cache = seen ?? new WeakMap<object, unknown>();
206
+ const cached = cache.get(value);
207
+ if (cached !== undefined) return cached;
208
+ if (Array.isArray(value)) {
209
+ const out: unknown[] = [];
210
+ cache.set(value, out);
211
+ for (const entry of value) out.push(cloneJsonLiteral(entry, cache));
212
+ return out;
213
+ }
214
+ const out: JsonObject = {};
215
+ cache.set(value, out);
216
+ for (const key in value as JsonObject) {
217
+ if (!Object.hasOwn(value, key)) continue;
218
+ setOwnKey(out, key, cloneJsonLiteral((value as JsonObject)[key], cache));
219
+ }
220
+ return out;
221
+ }
222
+
171
223
  function inferJsonSchemaTypeFromValue(value: unknown): string | undefined {
172
224
  if (value === null) return "null";
173
225
  if (Array.isArray(value)) return "array";
@@ -225,6 +277,21 @@ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions
225
277
  return value;
226
278
  }
227
279
  if (!once(value, options.epoch)) return {};
280
+ if (options.insideProperties) {
281
+ let changed = false;
282
+ const result: JsonObject = {};
283
+ for (const key in value) {
284
+ if (!Object.hasOwn(value, key)) continue;
285
+ const child = value[key];
286
+ const next = normalizeSchemaNode(child, {
287
+ ...options,
288
+ insideProperties: false,
289
+ });
290
+ if (next !== child) changed = true;
291
+ setOwnKey(result, key, next);
292
+ }
293
+ return changed ? result : value;
294
+ }
228
295
  let obj = options.normalizeFieldNames && !options.insideProperties ? applySnakeCaseRenames(value) : value;
229
296
  if (options.collapseNullFields && !options.insideProperties) {
230
297
  obj = preHandleNullFields(obj);
@@ -232,19 +299,20 @@ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions
232
299
  const result: JsonObject = {};
233
300
  let spill: Array<[string, unknown]> | undefined;
234
301
  for (const combiner of JSON_SCHEMA_COMBINERS) {
235
- if (!Array.isArray(obj[combiner])) continue;
236
- const variants = obj[combiner] as JsonObject[];
237
- const allHaveConst = variants.every(v => isJsonObject(v) && "const" in v);
302
+ const variantsRaw = Object.hasOwn(obj, combiner) ? obj[combiner] : undefined;
303
+ if (!Array.isArray(variantsRaw)) continue;
304
+ const variants = variantsRaw as JsonObject[];
305
+ const allHaveConst = variants.every(v => isJsonObject(v) && Object.hasOwn(v, "const"));
238
306
  if (!allHaveConst || variants.length === 0) continue;
239
307
 
240
308
  const dedupedEnum: unknown[] = [];
241
309
  for (const variant of variants) {
242
- pushEnumValue(dedupedEnum, variant.const);
310
+ pushEnumValue(dedupedEnum, cloneJsonLiteral(variant.const));
243
311
  }
244
312
  result.enum = dedupedEnum;
245
313
 
246
314
  const explicitTypes = variants
247
- .map(variant => variant.type)
315
+ .map(variant => (Object.hasOwn(variant, "type") ? variant.type : undefined))
248
316
  .filter((variantType): variantType is string => typeof variantType === "string");
249
317
  const allHaveSameExplicitType =
250
318
  explicitTypes.length === variants.length &&
@@ -278,10 +346,18 @@ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions
278
346
  continue;
279
347
  }
280
348
  if (options.stripNullableKeyword && key === "nullable") continue;
281
- result[key] = normalizeSchemaNode(entry, {
282
- ...options,
283
- insideProperties: key === "properties",
284
- });
349
+ if (JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) {
350
+ setOwnKey(result, key, cloneJsonLiteral(entry));
351
+ continue;
352
+ }
353
+ setOwnKey(
354
+ result,
355
+ key,
356
+ normalizeSchemaNode(entry, {
357
+ ...options,
358
+ insideProperties: JSON_SCHEMA_MAP_KEYS.has(key),
359
+ }),
360
+ );
285
361
  }
286
362
  applyDescriptionSpill(result, spill, options);
287
363
  return applyNodePostProcessing(result, options);
@@ -297,13 +373,21 @@ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions
297
373
  }
298
374
  if (options.stripNullableKeyword && key === "nullable") continue;
299
375
  if (key === "const") {
300
- constValue = entry;
376
+ constValue = cloneJsonLiteral(entry);
301
377
  continue;
302
378
  }
303
- result[key] = normalizeSchemaNode(entry, {
304
- ...options,
305
- insideProperties: key === "properties",
306
- });
379
+ if (JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) {
380
+ setOwnKey(result, key, cloneJsonLiteral(entry));
381
+ continue;
382
+ }
383
+ setOwnKey(
384
+ result,
385
+ key,
386
+ normalizeSchemaNode(entry, {
387
+ ...options,
388
+ insideProperties: JSON_SCHEMA_MAP_KEYS.has(key),
389
+ }),
390
+ );
307
391
  }
308
392
 
309
393
  if (options.normalizeTypeArrayToNullable && Array.isArray(result.type)) {
@@ -367,7 +451,7 @@ export function copySchemaWithout(schema: JsonObject, combiner: string): JsonObj
367
451
  }
368
452
 
369
453
  function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
370
- const variantsRaw = schema[combiner];
454
+ const variantsRaw = Object.hasOwn(schema, combiner) ? schema[combiner] : undefined;
371
455
  if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
372
456
  return schema;
373
457
  }
@@ -377,10 +461,10 @@ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "on
377
461
  if (!isJsonObject(entry)) {
378
462
  return schema;
379
463
  }
380
- const variantType = entry.type;
464
+ const variantType = Object.hasOwn(entry, "type") ? entry.type : undefined;
381
465
  const hasObjectShape =
382
- isJsonObject(entry.properties) ||
383
- Array.isArray(entry.required) ||
466
+ (Object.hasOwn(entry, "properties") && isJsonObject(entry.properties)) ||
467
+ (Object.hasOwn(entry, "required") && Array.isArray(entry.required)) ||
384
468
  Object.hasOwn(entry, "additionalProperties");
385
469
  if (variantType === undefined && !hasObjectShape) {
386
470
  return schema;
@@ -388,29 +472,34 @@ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "on
388
472
  if (variantType !== undefined && variantType !== "object") {
389
473
  return schema;
390
474
  }
391
- if (entry.properties !== undefined && !isJsonObject(entry.properties)) {
475
+ if (Object.hasOwn(entry, "properties") && !isJsonObject(entry.properties)) {
392
476
  return schema;
393
477
  }
394
- if (entry.required !== undefined && !Array.isArray(entry.required)) {
478
+ if (Object.hasOwn(entry, "required") && !Array.isArray(entry.required)) {
395
479
  return schema;
396
480
  }
397
481
  variants.push(entry);
398
482
  }
399
483
 
400
484
  const mergedProperties: JsonObject = {};
401
- const ownProperties = isJsonObject(schema.properties) ? schema.properties : {};
485
+ const ownProperties =
486
+ Object.hasOwn(schema, "properties") && isJsonObject(schema.properties) ? schema.properties : {};
402
487
  for (const name in ownProperties) {
403
- if (Object.hasOwn(ownProperties, name)) mergedProperties[name] = ownProperties[name];
488
+ if (Object.hasOwn(ownProperties, name)) setOwnKey(mergedProperties, name, ownProperties[name]);
404
489
  }
405
490
 
406
491
  for (const variant of variants) {
407
- const properties = isJsonObject(variant.properties) ? variant.properties : {};
492
+ const properties =
493
+ Object.hasOwn(variant, "properties") && isJsonObject(variant.properties) ? variant.properties : {};
408
494
  for (const name in properties) {
409
495
  if (!Object.hasOwn(properties, name)) continue;
410
496
  const propertySchema = properties[name];
411
- const existingSchema = mergedProperties[name];
412
- mergedProperties[name] =
413
- existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema);
497
+ const existingSchema = Object.hasOwn(mergedProperties, name) ? mergedProperties[name] : undefined;
498
+ setOwnKey(
499
+ mergedProperties,
500
+ name,
501
+ existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema),
502
+ );
414
503
  }
415
504
  }
416
505
 
@@ -420,9 +509,10 @@ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "on
420
509
 
421
510
  let requiredIntersection: string[] | undefined;
422
511
  for (const variant of variants) {
423
- const variantRequired = Array.isArray(variant.required)
424
- ? variant.required.filter((r): r is string => typeof r === "string")
425
- : [];
512
+ const variantRequired =
513
+ Object.hasOwn(variant, "required") && Array.isArray(variant.required)
514
+ ? variant.required.filter((r): r is string => typeof r === "string")
515
+ : [];
426
516
  if (requiredIntersection === undefined) {
427
517
  requiredIntersection = [...variantRequired];
428
518
  } else {
@@ -430,9 +520,10 @@ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "on
430
520
  requiredIntersection = requiredIntersection.filter(r => reqSet.has(r));
431
521
  }
432
522
  }
433
- const parentRequired = Array.isArray(schema.required)
434
- ? schema.required.filter((r): r is string => typeof r === "string")
435
- : [];
523
+ const parentRequired =
524
+ Object.hasOwn(schema, "required") && Array.isArray(schema.required)
525
+ ? schema.required.filter((r): r is string => typeof r === "string")
526
+ : [];
436
527
  const safeRequired = new Set<string>();
437
528
  for (const name of requiredIntersection ?? []) {
438
529
  if (Object.hasOwn(mergedProperties, name)) safeRequired.add(name);
@@ -456,7 +547,7 @@ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "on
456
547
  }
457
548
 
458
549
  function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
459
- const variantsRaw = schema[combiner];
550
+ const variantsRaw = Object.hasOwn(schema, combiner) ? schema[combiner] : undefined;
460
551
  if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
461
552
  return schema;
462
553
  }
@@ -465,7 +556,7 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
465
556
  const variantTypes: string[] = [];
466
557
  const mergedVariantFields: JsonObject = {};
467
558
  for (const entry of variantsRaw) {
468
- if (!isJsonObject(entry) || typeof entry.type !== "string") {
559
+ if (!isJsonObject(entry) || !Object.hasOwn(entry, "type") || typeof entry.type !== "string") {
469
560
  return schema;
470
561
  }
471
562
 
@@ -487,11 +578,11 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
487
578
  return schema;
488
579
  }
489
580
 
490
- const existingValue = mergedVariantFields[key];
581
+ const existingValue = Object.hasOwn(mergedVariantFields, key) ? mergedVariantFields[key] : undefined;
491
582
  if (existingValue !== undefined && !areJsonValuesEqual(existingValue, variantValue)) {
492
583
  return schema;
493
584
  }
494
- mergedVariantFields[key] = variantValue;
585
+ setOwnKey(mergedVariantFields, key, variantValue);
495
586
  }
496
587
 
497
588
  seenTypes.add(variantType);
@@ -508,24 +599,24 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
508
599
  for (const key in mergedVariantFields) {
509
600
  if (!Object.hasOwn(mergedVariantFields, key)) continue;
510
601
  const value = mergedVariantFields[key];
511
- const existingValue = nextSchema[key];
602
+ const existingValue = Object.hasOwn(nextSchema, key) ? nextSchema[key] : undefined;
512
603
  if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
513
604
  return schema;
514
605
  }
515
606
  if (existingValue === undefined) {
516
- nextSchema[key] = value;
607
+ setOwnKey(nextSchema, key, value);
517
608
  }
518
609
  }
519
610
  return nextSchema;
520
611
  }
521
612
 
522
613
  function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
523
- const variantsRaw = schema[combiner];
614
+ const variantsRaw = Object.hasOwn(schema, combiner) ? schema[combiner] : undefined;
524
615
  if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) return schema;
525
616
  let commonType: string | undefined;
526
617
  let firstEntry: JsonObject | undefined;
527
618
  for (const entry of variantsRaw) {
528
- if (!isJsonObject(entry) || typeof entry.type !== "string") return schema;
619
+ if (!isJsonObject(entry) || !Object.hasOwn(entry, "type") || typeof entry.type !== "string") return schema;
529
620
  if (commonType === undefined) {
530
621
  commonType = entry.type;
531
622
  firstEntry = entry;
@@ -534,7 +625,7 @@ function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
534
625
  if (!firstEntry) return schema;
535
626
  const nextSchema = copySchemaWithout(schema, combiner);
536
627
  for (const key in firstEntry) {
537
- if (Object.hasOwn(firstEntry, key) && !outHasOwn(nextSchema, key)) nextSchema[key] = firstEntry[key];
628
+ if (Object.hasOwn(firstEntry, key) && !outHasOwn(nextSchema, key)) setOwnKey(nextSchema, key, firstEntry[key]);
538
629
  }
539
630
  return nextSchema;
540
631
  }
@@ -544,6 +635,16 @@ function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
544
635
  * collapse can handle. This is needed because object-combiner merging can
545
636
  * create new anyOf in merged subtrees after child normalization already ran.
546
637
  */
638
+ function stripResidualCombinersMap(schemaMap: JsonObject, epoch: number): JsonObject {
639
+ if (!once(schemaMap, epoch)) return {};
640
+ const result: JsonObject = {};
641
+ for (const key in schemaMap) {
642
+ if (!Object.hasOwn(schemaMap, key)) continue;
643
+ setOwnKey(result, key, stripResidualCombiners(schemaMap[key], epoch));
644
+ }
645
+ return result;
646
+ }
647
+
547
648
  export function stripResidualCombiners(value: unknown, epoch: number = epochNext()): unknown {
548
649
  if (Array.isArray(value)) {
549
650
  if (!once(value, epoch)) return [];
@@ -553,7 +654,19 @@ export function stripResidualCombiners(value: unknown, epoch: number = epochNext
553
654
  if (!once(value, epoch)) return {};
554
655
  const result: JsonObject = {};
555
656
  for (const key in value) {
556
- if (Object.hasOwn(value, key)) result[key] = stripResidualCombiners(value[key], epoch);
657
+ if (!Object.hasOwn(value, key)) continue;
658
+ if (JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) {
659
+ setOwnKey(result, key, value[key]);
660
+ continue;
661
+ }
662
+ const child = value[key];
663
+ setOwnKey(
664
+ result,
665
+ key,
666
+ JSON_SCHEMA_MAP_KEYS.has(key) && isJsonObject(child)
667
+ ? stripResidualCombinersMap(child, epoch)
668
+ : stripResidualCombiners(child, epoch),
669
+ );
557
670
  }
558
671
  let current: JsonObject = result;
559
672
  let changed = true;
@@ -585,13 +698,13 @@ function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
585
698
  return { schema, nullable: false };
586
699
  }
587
700
 
588
- if (schema.nullable === true) {
701
+ if (Object.hasOwn(schema, "nullable") && schema.nullable === true) {
589
702
  const nextSchema = { ...schema };
590
703
  delete nextSchema.nullable;
591
704
  return { schema: nextSchema, nullable: true };
592
705
  }
593
706
 
594
- if (Array.isArray(schema.type)) {
707
+ if (Object.hasOwn(schema, "type") && Array.isArray(schema.type)) {
595
708
  const typeVariants = schema.type.filter((entry): entry is string => typeof entry === "string");
596
709
  const nonNullTypes = typeVariants.filter(entry => entry !== "null");
597
710
  if (typeVariants.includes("null") && nonNullTypes.length === 1) {
@@ -601,13 +714,13 @@ function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
601
714
  }
602
715
 
603
716
  for (const combiner of JSON_SCHEMA_COMBINERS) {
604
- const variantsRaw = schema[combiner];
717
+ const variantsRaw = Object.hasOwn(schema, combiner) ? schema[combiner] : undefined;
605
718
  if (!Array.isArray(variantsRaw)) continue;
606
719
 
607
720
  let hasNullVariant = false;
608
721
  const nonNullVariants: unknown[] = [];
609
722
  for (const variant of variantsRaw) {
610
- if (isJsonObject(variant) && variant.type === "null") {
723
+ if (isJsonObject(variant) && Object.hasOwn(variant, "type") && variant.type === "null") {
611
724
  let keyCount = 0;
612
725
  for (const k in variant) {
613
726
  if (!Object.hasOwn(variant, k)) continue;
@@ -630,12 +743,12 @@ function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
630
743
  for (const key in nonNullVariant) {
631
744
  if (!Object.hasOwn(nonNullVariant, key)) continue;
632
745
  const value = nonNullVariant[key];
633
- const existingValue = nextSchema[key];
746
+ const existingValue = Object.hasOwn(nextSchema, key) ? nextSchema[key] : undefined;
634
747
  if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
635
748
  return { schema, nullable: false };
636
749
  }
637
750
  if (existingValue === undefined) {
638
- nextSchema[key] = value;
751
+ setOwnKey(nextSchema, key, value);
639
752
  }
640
753
  }
641
754
  return { schema: nextSchema, nullable: true };
@@ -649,6 +762,23 @@ interface NullableNormalizationResult {
649
762
  nullable: boolean;
650
763
  }
651
764
 
765
+ function normalizeNullableSchemaMapForCloudCodeAssist(
766
+ value: JsonObject,
767
+ propertyMap: boolean,
768
+ epoch: number,
769
+ ): { schema: JsonObject; nullableKeys: Set<string> } {
770
+ if (!once(value, epoch)) return { schema: {}, nullableKeys: new Set() };
771
+ const normalized: JsonObject = {};
772
+ const nullableKeys = new Set<string>();
773
+ for (const key in value) {
774
+ if (!Object.hasOwn(value, key)) continue;
775
+ const child = normalizeNullablePropertiesForCloudCodeAssist(value[key], propertyMap, epoch);
776
+ setOwnKey(normalized, key, child.schema);
777
+ if (child.nullable) nullableKeys.add(key);
778
+ }
779
+ return { schema: normalized, nullableKeys };
780
+ }
781
+
652
782
  function normalizeNullablePropertiesForCloudCodeAssist(
653
783
  value: unknown,
654
784
  isPropertySchema = false,
@@ -671,29 +801,30 @@ function normalizeNullablePropertiesForCloudCodeAssist(
671
801
  }
672
802
 
673
803
  const normalized: JsonObject = {};
804
+ let nullablePropertyKeys: Set<string> | undefined;
674
805
  for (const key in value) {
675
- if (Object.hasOwn(value, key))
676
- normalized[key] = normalizeNullablePropertiesForCloudCodeAssist(value[key], false, epoch).schema;
806
+ if (!Object.hasOwn(value, key)) continue;
807
+ if (JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) {
808
+ setOwnKey(normalized, key, value[key]);
809
+ continue;
810
+ }
811
+ if (JSON_SCHEMA_MAP_KEYS.has(key) && isJsonObject(value[key])) {
812
+ const mapped = normalizeNullableSchemaMapForCloudCodeAssist(value[key], key === "properties", epoch);
813
+ setOwnKey(normalized, key, mapped.schema);
814
+ if (key === "properties") nullablePropertyKeys = mapped.nullableKeys;
815
+ continue;
816
+ }
817
+ setOwnKey(normalized, key, normalizeNullablePropertiesForCloudCodeAssist(value[key], false, epoch).schema);
677
818
  }
678
819
 
679
- if (isJsonObject(normalized.properties)) {
680
- const properties = normalized.properties;
820
+ if (nullablePropertyKeys && isJsonObject(normalized.properties)) {
681
821
  const required = new Set(
682
822
  Array.isArray(normalized.required)
683
823
  ? normalized.required.filter((entry): entry is string => typeof entry === "string")
684
824
  : [],
685
825
  );
686
- const nextProperties: JsonObject = {};
687
- for (const name in properties) {
688
- if (!Object.hasOwn(properties, name)) continue;
689
- const normalizedProperty = normalizeNullablePropertiesForCloudCodeAssist(properties[name], true, epoch);
690
- nextProperties[name] = normalizedProperty.schema;
691
- if (normalizedProperty.nullable) {
692
- required.delete(name);
693
- }
694
- }
695
- normalized.properties = nextProperties;
696
- if (Array.isArray(normalized.required)) {
826
+ for (const key of nullablePropertyKeys) required.delete(key);
827
+ if (Object.hasOwn(normalized, "required") && Array.isArray(normalized.required)) {
697
828
  normalized.required = Array.from(required);
698
829
  }
699
830
  }
@@ -750,23 +881,38 @@ function hasResidualSchemaIncompatibilities(
750
881
  return false;
751
882
  }
752
883
 
753
- if (checks.typeArray && Array.isArray(value.type)) return true;
754
- if (checks.typeNull && value.type === "null") return true;
884
+ if (checks.typeArray && Object.hasOwn(value, "type") && Array.isArray(value.type)) return true;
885
+ if (checks.typeNull && Object.hasOwn(value, "type") && value.type === "null") return true;
755
886
  if (checks.nullable && Object.hasOwn(value, "nullable")) return true;
756
887
  if (checks.combiners) {
757
888
  for (const combiner of CCA_FORBIDDEN_COMBINERS) {
758
- if (Array.isArray(value[combiner])) return true;
889
+ if (Object.hasOwn(value, combiner) && Array.isArray(value[combiner])) return true;
759
890
  }
760
891
  }
761
892
  for (const k in value) {
762
893
  if (!Object.hasOwn(value, k)) continue;
763
- if (hasResidualSchemaIncompatibilities(value[k], checks, epoch)) {
894
+ if (JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(k)) continue;
895
+ const child = value[k];
896
+ const hasResidual =
897
+ JSON_SCHEMA_MAP_KEYS.has(k) && isJsonObject(child)
898
+ ? hasResidualSchemaMap(child, checks, epoch)
899
+ : hasResidualSchemaIncompatibilities(child, checks, epoch);
900
+ if (hasResidual) {
764
901
  return true;
765
902
  }
766
903
  }
767
904
  return false;
768
905
  }
769
906
 
907
+ function hasResidualSchemaMap(value: JsonObject, checks: ResidualIncompatibilityChecks, epoch: number): boolean {
908
+ if (!once(value, epoch)) return false;
909
+ for (const key in value) {
910
+ if (!Object.hasOwn(value, key)) continue;
911
+ if (hasResidualSchemaIncompatibilities(value[key], checks, epoch)) return true;
912
+ }
913
+ return false;
914
+ }
915
+
770
916
  export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions): unknown {
771
917
  const detoxified = decontaminateZodInstance(value);
772
918
  const upgraded = upgradeJsonSchemaTo202012(detoxified);
@@ -832,8 +978,102 @@ export function normalizeSchemaForCCA(value: unknown): unknown {
832
978
  });
833
979
  }
834
980
 
981
+ const MCP_SCHEMA_ARRAY_KEYS = new Set(["anyOf", "oneOf", "allOf", "prefixItems"]);
982
+ const MCP_SCHEMA_MAP_KEYS = new Set([
983
+ "properties",
984
+ "patternProperties",
985
+ "dependencies",
986
+ "dependentSchemas",
987
+ "$defs",
988
+ "definitions",
989
+ ]);
990
+ const MCP_SCHEMA_VALUE_KEYS = new Set([
991
+ "items",
992
+ "additionalItems",
993
+ "contains",
994
+ "contentSchema",
995
+ "propertyNames",
996
+ "if",
997
+ "then",
998
+ "else",
999
+ "not",
1000
+ "additionalProperties",
1001
+ "unevaluatedItems",
1002
+ "unevaluatedProperties",
1003
+ ]);
1004
+
1005
+ function makeImplicitMcpObjectMapsExplicit(value: unknown): unknown {
1006
+ return normalizeMcpObjectMapNode(value, new WeakMap());
1007
+ }
1008
+
1009
+ function normalizeMcpObjectMapNode(value: unknown, cache: WeakMap<JsonObject, JsonObject>): unknown {
1010
+ if (!isJsonObject(value)) return value;
1011
+ const cached = cache.get(value);
1012
+ if (cached) return cached;
1013
+
1014
+ const output: JsonObject = {};
1015
+ cache.set(value, output);
1016
+ let changed = false;
1017
+
1018
+ for (const key in value) {
1019
+ if (!Object.hasOwn(value, key)) continue;
1020
+ const child = value[key];
1021
+ let next: unknown = child;
1022
+ if (MCP_SCHEMA_MAP_KEYS.has(key) && isJsonObject(child)) {
1023
+ next = normalizeMcpObjectMap(child, cache);
1024
+ } else if (MCP_SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(child)) {
1025
+ next = normalizeMcpObjectMapArray(child, cache);
1026
+ } else if (MCP_SCHEMA_VALUE_KEYS.has(key)) {
1027
+ next = Array.isArray(child)
1028
+ ? normalizeMcpObjectMapArray(child, cache)
1029
+ : normalizeMcpObjectMapNode(child, cache);
1030
+ }
1031
+ if (next !== child) changed = true;
1032
+ setOwnKey(output, key, next);
1033
+ }
1034
+
1035
+ const schemaType = Object.hasOwn(value, "type") ? value.type : undefined;
1036
+ if (
1037
+ declaresObjectType(schemaType) &&
1038
+ !Object.hasOwn(value, "properties") &&
1039
+ !Object.hasOwn(value, "additionalProperties") &&
1040
+ !Object.hasOwn(value, "patternProperties") &&
1041
+ !Object.hasOwn(value, "unevaluatedProperties")
1042
+ ) {
1043
+ output.additionalProperties = true;
1044
+ changed = true;
1045
+ }
1046
+
1047
+ const result = changed ? output : value;
1048
+ cache.set(value, result);
1049
+ return result;
1050
+ }
1051
+
1052
+ function normalizeMcpObjectMapArray(value: unknown[], cache: WeakMap<JsonObject, JsonObject>): unknown[] {
1053
+ let changed = false;
1054
+ const output = value.map(item => {
1055
+ const next = normalizeMcpObjectMapNode(item, cache);
1056
+ if (next !== item) changed = true;
1057
+ return next;
1058
+ });
1059
+ return changed ? output : value;
1060
+ }
1061
+
1062
+ function normalizeMcpObjectMap(schemaMap: JsonObject, cache: WeakMap<JsonObject, JsonObject>): JsonObject {
1063
+ let changed = false;
1064
+ const output: JsonObject = {};
1065
+ for (const key in schemaMap) {
1066
+ if (!Object.hasOwn(schemaMap, key)) continue;
1067
+ const child = schemaMap[key];
1068
+ const next = normalizeMcpObjectMapNode(child, cache);
1069
+ if (next !== child) changed = true;
1070
+ setOwnKey(output, key, next);
1071
+ }
1072
+ return changed ? output : schemaMap;
1073
+ }
1074
+
835
1075
  export function normalizeSchemaForMCP(value: unknown): unknown {
836
- return normalizeSchema(value, {
1076
+ const normalized = normalizeSchema(value, {
837
1077
  unsupportedFields: isMcpUnsupportedSchemaField,
838
1078
  normalizeFieldNames: false,
839
1079
  collapseNullFields: false,
@@ -848,6 +1088,7 @@ export function normalizeSchemaForMCP(value: unknown): unknown {
848
1088
  stripResidualCombinersFixpoint: false,
849
1089
  extractNullableFromUnions: false,
850
1090
  });
1091
+ return makeImplicitMcpObjectMapsExplicit(normalized);
851
1092
  }
852
1093
 
853
1094
  // ---------------------------------------------------------------------------
@@ -952,7 +1193,7 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonO
952
1193
  }
953
1194
 
954
1195
  if (next !== child) changed = true;
955
- output[key] = next;
1196
+ setOwnKey(output, key, next);
956
1197
  }
957
1198
 
958
1199
  if (Array.isArray(value.oneOf)) {
@@ -966,7 +1207,7 @@ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonO
966
1207
  // Draft 2020-12 lets `type` be an array (e.g. `["object", "null"]`); treat
967
1208
  // any variant that includes "object" as an object position for the
968
1209
  // properties requirement.
969
- if (declaresObjectType(value.type) && !Object.hasOwn(value, "properties")) {
1210
+ if (Object.hasOwn(value, "type") && declaresObjectType(value.type) && !Object.hasOwn(value, "properties")) {
970
1211
  output.properties = {};
971
1212
  changed = true;
972
1213
  }
@@ -1007,7 +1248,7 @@ function normalizeOpenAIResponsesSchemaMap(schemaMap: JsonObject, cache: WeakMap
1007
1248
  const child = schemaMap[key];
1008
1249
  const next = normalizeOpenAIResponsesSchemaNode(child, cache);
1009
1250
  if (next !== child) changed = true;
1010
- output[key] = next;
1251
+ setOwnKey(output, key, next);
1011
1252
  }
1012
1253
  return changed ? output : schemaMap;
1013
1254
  }
@@ -1089,21 +1330,24 @@ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoc
1089
1330
  if (!once(schema, epoch)) return false;
1090
1331
 
1091
1332
  let hasPatternProperties = false;
1092
- if (isJsonObject(schema.patternProperties)) {
1333
+ if (Object.hasOwn(schema, "patternProperties") && isJsonObject(schema.patternProperties)) {
1093
1334
  for (const _ in schema.patternProperties) {
1094
1335
  hasPatternProperties = true;
1095
1336
  break;
1096
1337
  }
1097
1338
  }
1098
- const additionalPropertiesValue = schema.additionalProperties;
1339
+ const additionalPropertiesValue = Object.hasOwn(schema, "additionalProperties")
1340
+ ? schema.additionalProperties
1341
+ : undefined;
1099
1342
  const hasSchemaAdditionalProperties = additionalPropertiesValue === true || isJsonObject(additionalPropertiesValue);
1100
1343
  if (hasPatternProperties || hasSchemaAdditionalProperties) {
1101
1344
  return true;
1102
1345
  }
1103
1346
 
1104
- if (isJsonObject(schema.properties)) {
1347
+ if (Object.hasOwn(schema, "properties") && isJsonObject(schema.properties)) {
1105
1348
  const properties = schema.properties;
1106
1349
  for (const k in properties) {
1350
+ if (!Object.hasOwn(properties, k)) continue;
1107
1351
  const propertySchema = properties[k];
1108
1352
  if (isJsonObject(propertySchema) && hasUnrepresentableStrictObjectMap(propertySchema, epoch)) {
1109
1353
  return true;
@@ -1111,18 +1355,18 @@ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoc
1111
1355
  }
1112
1356
  }
1113
1357
 
1114
- if (isJsonObject(schema.items)) {
1358
+ if (Object.hasOwn(schema, "items") && isJsonObject(schema.items)) {
1115
1359
  if (hasUnrepresentableStrictObjectMap(schema.items, epoch)) {
1116
1360
  return true;
1117
1361
  }
1118
- } else if (Array.isArray(schema.items)) {
1362
+ } else if (Object.hasOwn(schema, "items") && Array.isArray(schema.items)) {
1119
1363
  for (const itemSchema of schema.items) {
1120
1364
  if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1121
1365
  return true;
1122
1366
  }
1123
1367
  }
1124
1368
  }
1125
- if (Array.isArray(schema.prefixItems)) {
1369
+ if (Object.hasOwn(schema, "prefixItems") && Array.isArray(schema.prefixItems)) {
1126
1370
  for (const itemSchema of schema.prefixItems) {
1127
1371
  if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1128
1372
  return true;
@@ -1131,7 +1375,7 @@ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoc
1131
1375
  }
1132
1376
 
1133
1377
  for (const key of COMBINATOR_KEYS) {
1134
- const variants = schema[key];
1378
+ const variants = Object.hasOwn(schema, key) ? schema[key] : undefined;
1135
1379
  if (!Array.isArray(variants)) continue;
1136
1380
  for (const variant of variants) {
1137
1381
  if (isJsonObject(variant) && hasUnrepresentableStrictObjectMap(variant, epoch)) {
@@ -1141,9 +1385,10 @@ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoc
1141
1385
  }
1142
1386
 
1143
1387
  for (const defsKey of ["$defs", "definitions"] as const) {
1144
- const defs = schema[defsKey];
1388
+ const defs = Object.hasOwn(schema, defsKey) ? schema[defsKey] : undefined;
1145
1389
  if (!isJsonObject(defs)) continue;
1146
1390
  for (const k in defs) {
1391
+ if (!Object.hasOwn(defs, k)) continue;
1147
1392
  const defSchema = defs[k];
1148
1393
  if (isJsonObject(defSchema) && hasUnrepresentableStrictObjectMap(defSchema, epoch)) {
1149
1394
  return true;
@@ -1186,7 +1431,7 @@ export function sanitizeSchemaForStrictMode(
1186
1431
  // OpenAI strict mode forbids `{$ref, description, ...}`; the SDK resolves
1187
1432
  // and merges, with sibling keys taking precedence over the ref'd def.
1188
1433
  // Cite: openai-python/src/openai/lib/_pydantic.py:96-110 (`_ensure_strict_json_schema`)
1189
- if (typeof schema.$ref === "string") {
1434
+ if (Object.hasOwn(schema, "$ref") && typeof schema.$ref === "string") {
1190
1435
  let hasSibling = false;
1191
1436
  for (const k in schema) {
1192
1437
  if (k !== "$ref" && Object.hasOwn(schema, k)) {
@@ -1201,7 +1446,7 @@ export function sanitizeSchemaForStrictMode(
1201
1446
  const merged: Record<string, unknown> = { ...resolved };
1202
1447
  for (const k in schema) {
1203
1448
  if (k === "$ref" || !Object.hasOwn(schema, k)) continue;
1204
- merged[k] = schema[k];
1449
+ setOwnKey(merged, k, schema[k]);
1205
1450
  }
1206
1451
  const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1207
1452
  cache.set(schema, result);
@@ -1215,13 +1460,13 @@ export function sanitizeSchemaForStrictMode(
1215
1460
  // entry's keys WIN over original sibling keys, then `allOf` is dropped.
1216
1461
  // Cite: openai-python/src/openai/lib/_pydantic.py:79-83
1217
1462
  {
1218
- const allOf = schema.allOf;
1463
+ const allOf = Object.hasOwn(schema, "allOf") ? schema.allOf : undefined;
1219
1464
  if (Array.isArray(allOf) && allOf.length === 1 && isJsonObject(allOf[0])) {
1220
1465
  const merged: Record<string, unknown> = { ...schema };
1221
1466
  delete merged.allOf;
1222
1467
  const sole = allOf[0] as Record<string, unknown>;
1223
1468
  for (const k in sole) {
1224
- if (Object.hasOwn(sole, k)) merged[k] = sole[k];
1469
+ if (Object.hasOwn(sole, k)) setOwnKey(merged, k, sole[k]);
1225
1470
  }
1226
1471
  const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1227
1472
  cache.set(schema, result);
@@ -1229,7 +1474,7 @@ export function sanitizeSchemaForStrictMode(
1229
1474
  }
1230
1475
  }
1231
1476
 
1232
- const typeValue = schema.type;
1477
+ const typeValue = Object.hasOwn(schema, "type") ? schema.type : undefined;
1233
1478
  if (Array.isArray(typeValue)) {
1234
1479
  const typeVariants = typeValue.filter((entry): entry is string => typeof entry === "string");
1235
1480
  const schemaWithoutType = { ...schema };
@@ -1282,8 +1527,9 @@ export function sanitizeSchemaForStrictMode(
1282
1527
  const sanitized: Record<string, unknown> = {};
1283
1528
  cache.set(schema, sanitized);
1284
1529
  for (const key in schema) {
1530
+ if (!Object.hasOwn(schema, key)) continue;
1285
1531
  const value = schema[key];
1286
- if (key in NON_STRUCTURAL_SCHEMA_KEYS || key === "type" || key === "const" || key === "nullable") {
1532
+ if (Object.hasOwn(NON_STRUCTURAL_SCHEMA_KEYS, key) || key === "type" || key === "const" || key === "nullable") {
1287
1533
  continue;
1288
1534
  }
1289
1535
  // `properties` map — recurse into each property schema.
@@ -1291,10 +1537,15 @@ export function sanitizeSchemaForStrictMode(
1291
1537
  if (key === "properties" && isJsonObject(value)) {
1292
1538
  const properties: Record<string, unknown> = {};
1293
1539
  for (const propertyName in value) {
1540
+ if (!Object.hasOwn(value, propertyName)) continue;
1294
1541
  const propertySchema = value[propertyName];
1295
- properties[propertyName] = isJsonObject(propertySchema)
1296
- ? sanitizeSchemaForStrictMode(propertySchema, epoch, cache, root)
1297
- : propertySchema;
1542
+ setOwnKey(
1543
+ properties,
1544
+ propertyName,
1545
+ isJsonObject(propertySchema)
1546
+ ? sanitizeSchemaForStrictMode(propertySchema, epoch, cache, root)
1547
+ : propertySchema,
1548
+ );
1298
1549
  }
1299
1550
  sanitized.properties = properties;
1300
1551
  continue;
@@ -1324,8 +1575,10 @@ export function sanitizeSchemaForStrictMode(
1324
1575
  // `anyOf`/`oneOf`/`allOf` arrays — recurse into each branch.
1325
1576
 
1326
1577
  if (COMBINATOR_KEYS.includes(key as (typeof COMBINATOR_KEYS)[number]) && Array.isArray(value)) {
1327
- sanitized[key] = value.map(entry =>
1328
- isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1578
+ setOwnKey(
1579
+ sanitized,
1580
+ key,
1581
+ value.map(entry => (isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry)),
1329
1582
  );
1330
1583
  continue;
1331
1584
  }
@@ -1334,12 +1587,17 @@ export function sanitizeSchemaForStrictMode(
1334
1587
  if ((key === "$defs" || key === "definitions") && isJsonObject(value)) {
1335
1588
  const defs: Record<string, unknown> = {};
1336
1589
  for (const definitionName in value) {
1590
+ if (!Object.hasOwn(value, definitionName)) continue;
1337
1591
  const definitionSchema = value[definitionName];
1338
- defs[definitionName] = isJsonObject(definitionSchema)
1339
- ? sanitizeSchemaForStrictMode(definitionSchema, epoch, cache, root)
1340
- : definitionSchema;
1592
+ setOwnKey(
1593
+ defs,
1594
+ definitionName,
1595
+ isJsonObject(definitionSchema)
1596
+ ? sanitizeSchemaForStrictMode(definitionSchema, epoch, cache, root)
1597
+ : definitionSchema,
1598
+ );
1341
1599
  }
1342
- sanitized[key] = defs;
1600
+ setOwnKey(sanitized, key, defs);
1343
1601
  continue;
1344
1602
  }
1345
1603
  // `additionalProperties` is owned by `enforceStrictSchema`, which sets it to false.
@@ -1348,7 +1606,12 @@ export function sanitizeSchemaForStrictMode(
1348
1606
  continue;
1349
1607
  }
1350
1608
 
1351
- if (key === "description" && typeof value === "string" && schema.default !== undefined) {
1609
+ if (
1610
+ key === "description" &&
1611
+ typeof value === "string" &&
1612
+ Object.hasOwn(schema, "default") &&
1613
+ schema.default !== undefined
1614
+ ) {
1352
1615
  // Preserve `default:` info for strict-mode providers that strip the keyword.
1353
1616
  // Inline as `(default: X)` text in the description, matching the convention for
1354
1617
  // runtime-placeholder defaults (e.g. `cwd`) that cannot live in the keyword form.
@@ -1358,7 +1621,7 @@ export function sanitizeSchemaForStrictMode(
1358
1621
  continue;
1359
1622
  }
1360
1623
 
1361
- sanitized[key] = value;
1624
+ setOwnKey(sanitized, key, value);
1362
1625
  }
1363
1626
  // Post-pass: re-derive `type` and turn dropped keywords into a representable shape.
1364
1627
 
@@ -1394,7 +1657,7 @@ export function sanitizeSchemaForStrictMode(
1394
1657
  // `description` hoists to the wrapper so both branches share it without
1395
1658
  // duplication — matches the optional-property wrap in `enforceStrictSchema`
1396
1659
  // and the typical OpenAI strict-mode "description on the union" shape.
1397
- if (schema.nullable === true) {
1660
+ if (Object.hasOwn(schema, "nullable") && schema.nullable === true) {
1398
1661
  const { nullable: _, description, ...withoutNullable } = sanitized;
1399
1662
  const wrapper: JsonObject = { anyOf: [withoutNullable, { type: "null" }] };
1400
1663
  if (description !== undefined) wrapper.description = description;
@@ -1456,6 +1719,7 @@ function enforceStrictSchemaBody(
1456
1719
  );
1457
1720
  const strictProperties: Record<string, unknown> = {};
1458
1721
  for (const key in props) {
1722
+ if (!Object.hasOwn(props, key)) continue;
1459
1723
  const value = props[key];
1460
1724
  const processed =
1461
1725
  value != null && typeof value === "object" && !Array.isArray(value)
@@ -1467,20 +1731,20 @@ function enforceStrictSchemaBody(
1467
1731
  if (
1468
1732
  isJsonObject(processed) &&
1469
1733
  Array.isArray(processed.anyOf) &&
1470
- processed.anyOf.some(v => isJsonObject(v) && v.type === "null")
1734
+ processed.anyOf.some(v => isJsonObject(v) && Object.hasOwn(v, "type") && v.type === "null")
1471
1735
  ) {
1472
- strictProperties[key] = processed;
1736
+ setOwnKey(strictProperties, key, processed);
1473
1737
  continue;
1474
1738
  }
1475
1739
  if (isJsonObject(processed) && typeof processed.description === "string") {
1476
1740
  const { description, ...withoutDescription } = processed;
1477
- strictProperties[key] = { anyOf: [withoutDescription, { type: "null" }], description };
1741
+ setOwnKey(strictProperties, key, { anyOf: [withoutDescription, { type: "null" }], description });
1478
1742
  continue;
1479
1743
  }
1480
- strictProperties[key] = { anyOf: [processed, { type: "null" }] };
1744
+ setOwnKey(strictProperties, key, { anyOf: [processed, { type: "null" }] });
1481
1745
  continue;
1482
1746
  }
1483
- strictProperties[key] = processed;
1747
+ setOwnKey(strictProperties, key, processed);
1484
1748
  }
1485
1749
  result.properties = strictProperties;
1486
1750
  result.required = Object.keys(strictProperties);
@@ -1504,26 +1768,39 @@ function enforceStrictSchemaBody(
1504
1768
  );
1505
1769
  }
1506
1770
  for (const key of COMBINATOR_KEYS) {
1507
- if (Array.isArray(result[key])) {
1508
- result[key] = (result[key] as unknown[]).map(entry =>
1509
- entry != null && typeof entry === "object" && !Array.isArray(entry)
1510
- ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1511
- : entry,
1771
+ if (Object.hasOwn(result, key) && Array.isArray(result[key])) {
1772
+ setOwnKey(
1773
+ result,
1774
+ key,
1775
+ (result[key] as unknown[]).map(entry =>
1776
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1777
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1778
+ : entry,
1779
+ ),
1512
1780
  );
1513
1781
  }
1514
1782
  }
1515
1783
  for (const defsKey of ["$defs", "definitions"] as const) {
1516
- if (result[defsKey] != null && typeof result[defsKey] === "object" && !Array.isArray(result[defsKey])) {
1784
+ if (
1785
+ Object.hasOwn(result, defsKey) &&
1786
+ result[defsKey] != null &&
1787
+ typeof result[defsKey] === "object" &&
1788
+ !Array.isArray(result[defsKey])
1789
+ ) {
1517
1790
  const defs = result[defsKey] as Record<string, unknown>;
1518
1791
  const nextDefs: Record<string, unknown> = {};
1519
1792
  for (const name in defs) {
1793
+ if (!Object.hasOwn(defs, name)) continue;
1520
1794
  const def = defs[name];
1521
- nextDefs[name] =
1795
+ setOwnKey(
1796
+ nextDefs,
1797
+ name,
1522
1798
  def != null && typeof def === "object" && !Array.isArray(def)
1523
1799
  ? enforceStrictSchema(def as Record<string, unknown>, cache)
1524
- : def;
1800
+ : def,
1801
+ );
1525
1802
  }
1526
- result[defsKey] = nextDefs;
1803
+ setOwnKey(result, defsKey, nextDefs);
1527
1804
  }
1528
1805
  }
1529
1806
  // Strict mode requires every schema node to declare a concrete type (or