@gajae-code/ai 0.1.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 (349) hide show
  1. package/CHANGELOG.md +2644 -0
  2. package/README.md +1181 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +66 -0
  5. package/dist/types/auth-broker/index.d.ts +5 -0
  6. package/dist/types/auth-broker/refresher.d.ts +25 -0
  7. package/dist/types/auth-broker/remote-store.d.ts +96 -0
  8. package/dist/types/auth-broker/server.d.ts +32 -0
  9. package/dist/types/auth-broker/types.d.ts +105 -0
  10. package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
  11. package/dist/types/auth-gateway/http.d.ts +39 -0
  12. package/dist/types/auth-gateway/index.d.ts +3 -0
  13. package/dist/types/auth-gateway/server.d.ts +17 -0
  14. package/dist/types/auth-gateway/types.d.ts +115 -0
  15. package/dist/types/auth-storage.d.ts +641 -0
  16. package/dist/types/cli.d.ts +2 -0
  17. package/dist/types/index.d.ts +49 -0
  18. package/dist/types/model-cache.d.ts +17 -0
  19. package/dist/types/model-manager.d.ts +62 -0
  20. package/dist/types/model-thinking.d.ts +71 -0
  21. package/dist/types/models.d.ts +12 -0
  22. package/dist/types/provider-details.d.ts +24 -0
  23. package/dist/types/provider-models/bundled-references.d.ts +4 -0
  24. package/dist/types/provider-models/descriptors.d.ts +48 -0
  25. package/dist/types/provider-models/google.d.ts +20 -0
  26. package/dist/types/provider-models/index.d.ts +5 -0
  27. package/dist/types/provider-models/ollama.d.ts +7 -0
  28. package/dist/types/provider-models/openai-compat.d.ts +237 -0
  29. package/dist/types/provider-models/special.d.ts +16 -0
  30. package/dist/types/providers/amazon-bedrock.d.ts +36 -0
  31. package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
  32. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  33. package/dist/types/providers/anthropic.d.ts +188 -0
  34. package/dist/types/providers/aws-credentials.d.ts +43 -0
  35. package/dist/types/providers/aws-eventstream.d.ts +38 -0
  36. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  37. package/dist/types/providers/azure-openai-responses.d.ts +15 -0
  38. package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
  39. package/dist/types/providers/cursor.d.ts +42 -0
  40. package/dist/types/providers/error-message.d.ts +27 -0
  41. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  42. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  43. package/dist/types/providers/google-auth.d.ts +24 -0
  44. package/dist/types/providers/google-gemini-cli.d.ts +72 -0
  45. package/dist/types/providers/google-gemini-headers.d.ts +18 -0
  46. package/dist/types/providers/google-shared.d.ts +163 -0
  47. package/dist/types/providers/google-types.d.ts +138 -0
  48. package/dist/types/providers/google-vertex.d.ts +7 -0
  49. package/dist/types/providers/google.d.ts +4 -0
  50. package/dist/types/providers/grammar.d.ts +1 -0
  51. package/dist/types/providers/kimi.d.ts +27 -0
  52. package/dist/types/providers/mock.d.ts +175 -0
  53. package/dist/types/providers/ollama.d.ts +6 -0
  54. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  55. package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
  56. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  57. package/dist/types/providers/openai-codex/constants.d.ts +26 -0
  58. package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
  59. package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
  60. package/dist/types/providers/openai-codex-responses.d.ts +67 -0
  61. package/dist/types/providers/openai-completions-compat.d.ts +25 -0
  62. package/dist/types/providers/openai-completions.d.ts +33 -0
  63. package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
  64. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  65. package/dist/types/providers/openai-responses-shared.d.ts +89 -0
  66. package/dist/types/providers/openai-responses.d.ts +32 -0
  67. package/dist/types/providers/pi-native-client.d.ts +13 -0
  68. package/dist/types/providers/pi-native-server.d.ts +68 -0
  69. package/dist/types/providers/register-builtins.d.ts +31 -0
  70. package/dist/types/providers/synthetic.d.ts +26 -0
  71. package/dist/types/providers/transform-messages.d.ts +12 -0
  72. package/dist/types/providers/vision-guard.d.ts +8 -0
  73. package/dist/types/rate-limit-utils.d.ts +19 -0
  74. package/dist/types/stream.d.ts +24 -0
  75. package/dist/types/types.d.ts +746 -0
  76. package/dist/types/usage/claude.d.ts +3 -0
  77. package/dist/types/usage/gemini.d.ts +2 -0
  78. package/dist/types/usage/github-copilot.d.ts +7 -0
  79. package/dist/types/usage/google-antigravity.d.ts +2 -0
  80. package/dist/types/usage/kimi.d.ts +2 -0
  81. package/dist/types/usage/minimax-code.d.ts +2 -0
  82. package/dist/types/usage/openai-codex.d.ts +3 -0
  83. package/dist/types/usage/shared.d.ts +1 -0
  84. package/dist/types/usage/zai.d.ts +2 -0
  85. package/dist/types/usage.d.ts +258 -0
  86. package/dist/types/utils/abort.d.ts +19 -0
  87. package/dist/types/utils/anthropic-auth.d.ts +31 -0
  88. package/dist/types/utils/discovery/antigravity.d.ts +61 -0
  89. package/dist/types/utils/discovery/codex.d.ts +38 -0
  90. package/dist/types/utils/discovery/cursor.d.ts +23 -0
  91. package/dist/types/utils/discovery/gemini.d.ts +25 -0
  92. package/dist/types/utils/discovery/index.d.ts +4 -0
  93. package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
  94. package/dist/types/utils/event-stream.d.ts +28 -0
  95. package/dist/types/utils/fireworks-model-id.d.ts +10 -0
  96. package/dist/types/utils/foundry.d.ts +1 -0
  97. package/dist/types/utils/h2-fetch.d.ts +22 -0
  98. package/dist/types/utils/http-inspector.d.ts +31 -0
  99. package/dist/types/utils/idle-iterator.d.ts +67 -0
  100. package/dist/types/utils/json-parse.d.ts +10 -0
  101. package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
  102. package/dist/types/utils/oauth/anthropic.d.ts +22 -0
  103. package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
  104. package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
  105. package/dist/types/utils/oauth/callback-server.d.ts +57 -0
  106. package/dist/types/utils/oauth/cerebras.d.ts +1 -0
  107. package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
  108. package/dist/types/utils/oauth/cursor.d.ts +15 -0
  109. package/dist/types/utils/oauth/deepseek.d.ts +10 -0
  110. package/dist/types/utils/oauth/firepass.d.ts +1 -0
  111. package/dist/types/utils/oauth/fireworks.d.ts +1 -0
  112. package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
  113. package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
  114. package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
  115. package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
  116. package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
  117. package/dist/types/utils/oauth/huggingface.d.ts +19 -0
  118. package/dist/types/utils/oauth/index.d.ts +38 -0
  119. package/dist/types/utils/oauth/kagi.d.ts +17 -0
  120. package/dist/types/utils/oauth/kilo.d.ts +5 -0
  121. package/dist/types/utils/oauth/kimi.d.ts +21 -0
  122. package/dist/types/utils/oauth/litellm.d.ts +18 -0
  123. package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
  124. package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
  125. package/dist/types/utils/oauth/moonshot.d.ts +1 -0
  126. package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
  127. package/dist/types/utils/oauth/nvidia.d.ts +18 -0
  128. package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
  129. package/dist/types/utils/oauth/ollama.d.ts +18 -0
  130. package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
  131. package/dist/types/utils/oauth/opencode.d.ts +18 -0
  132. package/dist/types/utils/oauth/parallel.d.ts +17 -0
  133. package/dist/types/utils/oauth/perplexity.d.ts +9 -0
  134. package/dist/types/utils/oauth/pkce.d.ts +8 -0
  135. package/dist/types/utils/oauth/qianfan.d.ts +17 -0
  136. package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
  137. package/dist/types/utils/oauth/synthetic.d.ts +1 -0
  138. package/dist/types/utils/oauth/tavily.d.ts +17 -0
  139. package/dist/types/utils/oauth/together.d.ts +1 -0
  140. package/dist/types/utils/oauth/types.d.ts +44 -0
  141. package/dist/types/utils/oauth/venice.d.ts +18 -0
  142. package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
  143. package/dist/types/utils/oauth/vllm.d.ts +16 -0
  144. package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
  145. package/dist/types/utils/oauth/zai.d.ts +18 -0
  146. package/dist/types/utils/oauth/zenmux.d.ts +1 -0
  147. package/dist/types/utils/overflow.d.ts +54 -0
  148. package/dist/types/utils/parse-bind.d.ts +23 -0
  149. package/dist/types/utils/provider-response.d.ts +3 -0
  150. package/dist/types/utils/retry-after.d.ts +3 -0
  151. package/dist/types/utils/retry.d.ts +26 -0
  152. package/dist/types/utils/schema/adapt.d.ts +24 -0
  153. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  154. package/dist/types/utils/schema/dereference.d.ts +11 -0
  155. package/dist/types/utils/schema/draft.d.ts +10 -0
  156. package/dist/types/utils/schema/equality.d.ts +4 -0
  157. package/dist/types/utils/schema/fields.d.ts +49 -0
  158. package/dist/types/utils/schema/index.d.ts +13 -0
  159. package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
  160. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  161. package/dist/types/utils/schema/normalize.d.ts +93 -0
  162. package/dist/types/utils/schema/spill.d.ts +8 -0
  163. package/dist/types/utils/schema/stamps.d.ts +25 -0
  164. package/dist/types/utils/schema/types.d.ts +4 -0
  165. package/dist/types/utils/schema/wire.d.ts +54 -0
  166. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  167. package/dist/types/utils/sse-debug.d.ts +10 -0
  168. package/dist/types/utils/tool-call-healing.d.ts +71 -0
  169. package/dist/types/utils/tool-choice.d.ts +50 -0
  170. package/dist/types/utils/validation.d.ts +17 -0
  171. package/dist/types/utils.d.ts +28 -0
  172. package/package.json +146 -0
  173. package/src/api-registry.ts +96 -0
  174. package/src/auth-broker/client.ts +358 -0
  175. package/src/auth-broker/index.ts +5 -0
  176. package/src/auth-broker/refresher.ts +127 -0
  177. package/src/auth-broker/remote-store.ts +623 -0
  178. package/src/auth-broker/server.ts +644 -0
  179. package/src/auth-broker/types.ts +127 -0
  180. package/src/auth-broker/wire-schemas.ts +200 -0
  181. package/src/auth-gateway/http.ts +194 -0
  182. package/src/auth-gateway/index.ts +3 -0
  183. package/src/auth-gateway/server.ts +717 -0
  184. package/src/auth-gateway/types.ts +134 -0
  185. package/src/auth-storage.ts +4104 -0
  186. package/src/cli.ts +262 -0
  187. package/src/index.ts +54 -0
  188. package/src/model-cache.ts +129 -0
  189. package/src/model-manager.ts +450 -0
  190. package/src/model-thinking.ts +691 -0
  191. package/src/models.json +73853 -0
  192. package/src/models.json.d.ts +9 -0
  193. package/src/models.ts +56 -0
  194. package/src/prompts/turn-aborted-guidance.md +4 -0
  195. package/src/provider-details.ts +90 -0
  196. package/src/provider-models/bundled-references.ts +38 -0
  197. package/src/provider-models/descriptors.ts +308 -0
  198. package/src/provider-models/google.ts +91 -0
  199. package/src/provider-models/index.ts +5 -0
  200. package/src/provider-models/ollama.ts +153 -0
  201. package/src/provider-models/openai-compat.ts +2275 -0
  202. package/src/provider-models/special.ts +67 -0
  203. package/src/providers/amazon-bedrock.ts +849 -0
  204. package/src/providers/anthropic-messages-server-schema.ts +229 -0
  205. package/src/providers/anthropic-messages-server.ts +677 -0
  206. package/src/providers/anthropic.ts +2696 -0
  207. package/src/providers/aws-credentials.ts +501 -0
  208. package/src/providers/aws-eventstream.ts +185 -0
  209. package/src/providers/aws-sigv4.ts +218 -0
  210. package/src/providers/azure-openai-responses.ts +337 -0
  211. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  212. package/src/providers/cursor/proto/agent.proto +3526 -0
  213. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  214. package/src/providers/cursor/proto/buf.yaml +17 -0
  215. package/src/providers/cursor.ts +2561 -0
  216. package/src/providers/error-message.ts +21 -0
  217. package/src/providers/github-copilot-headers.ts +140 -0
  218. package/src/providers/gitlab-duo.ts +372 -0
  219. package/src/providers/google-auth.ts +252 -0
  220. package/src/providers/google-gemini-cli.ts +795 -0
  221. package/src/providers/google-gemini-headers.ts +41 -0
  222. package/src/providers/google-shared.ts +902 -0
  223. package/src/providers/google-types.ts +167 -0
  224. package/src/providers/google-vertex.ts +88 -0
  225. package/src/providers/google.ts +41 -0
  226. package/src/providers/grammar.ts +70 -0
  227. package/src/providers/kimi.ts +52 -0
  228. package/src/providers/mock.ts +500 -0
  229. package/src/providers/ollama.ts +544 -0
  230. package/src/providers/openai-anthropic-shim.ts +138 -0
  231. package/src/providers/openai-chat-server-schema.ts +243 -0
  232. package/src/providers/openai-chat-server.ts +628 -0
  233. package/src/providers/openai-codex/constants.ts +43 -0
  234. package/src/providers/openai-codex/request-transformer.ts +161 -0
  235. package/src/providers/openai-codex/response-handler.ts +81 -0
  236. package/src/providers/openai-codex-responses.ts +2598 -0
  237. package/src/providers/openai-completions-compat.ts +279 -0
  238. package/src/providers/openai-completions.ts +1853 -0
  239. package/src/providers/openai-responses-server-schema.ts +290 -0
  240. package/src/providers/openai-responses-server.ts +1183 -0
  241. package/src/providers/openai-responses-shared.ts +800 -0
  242. package/src/providers/openai-responses.ts +621 -0
  243. package/src/providers/pi-native-client.ts +228 -0
  244. package/src/providers/pi-native-server.ts +210 -0
  245. package/src/providers/register-builtins.ts +412 -0
  246. package/src/providers/synthetic.ts +50 -0
  247. package/src/providers/transform-messages.ts +309 -0
  248. package/src/providers/vision-guard.ts +31 -0
  249. package/src/rate-limit-utils.ts +84 -0
  250. package/src/stream.ts +895 -0
  251. package/src/types.ts +884 -0
  252. package/src/usage/claude.ts +431 -0
  253. package/src/usage/gemini.ts +250 -0
  254. package/src/usage/github-copilot.ts +421 -0
  255. package/src/usage/google-antigravity.ts +201 -0
  256. package/src/usage/kimi.ts +271 -0
  257. package/src/usage/minimax-code.ts +31 -0
  258. package/src/usage/openai-codex.ts +503 -0
  259. package/src/usage/shared.ts +10 -0
  260. package/src/usage/zai.ts +247 -0
  261. package/src/usage.ts +183 -0
  262. package/src/utils/abort.ts +51 -0
  263. package/src/utils/anthropic-auth.ts +87 -0
  264. package/src/utils/discovery/antigravity.ts +261 -0
  265. package/src/utils/discovery/codex.ts +371 -0
  266. package/src/utils/discovery/cursor.ts +306 -0
  267. package/src/utils/discovery/gemini.ts +248 -0
  268. package/src/utils/discovery/index.ts +4 -0
  269. package/src/utils/discovery/openai-compatible.ts +224 -0
  270. package/src/utils/event-stream.ts +142 -0
  271. package/src/utils/fireworks-model-id.ts +30 -0
  272. package/src/utils/foundry.ts +8 -0
  273. package/src/utils/h2-fetch.ts +60 -0
  274. package/src/utils/http-inspector.ts +176 -0
  275. package/src/utils/idle-iterator.ts +250 -0
  276. package/src/utils/json-parse.ts +148 -0
  277. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  278. package/src/utils/oauth/anthropic.ts +200 -0
  279. package/src/utils/oauth/api-key-login.ts +87 -0
  280. package/src/utils/oauth/api-key-validation.ts +92 -0
  281. package/src/utils/oauth/callback-server.ts +276 -0
  282. package/src/utils/oauth/cerebras.ts +16 -0
  283. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  284. package/src/utils/oauth/cursor.ts +157 -0
  285. package/src/utils/oauth/deepseek.ts +53 -0
  286. package/src/utils/oauth/firepass.ts +24 -0
  287. package/src/utils/oauth/fireworks.ts +15 -0
  288. package/src/utils/oauth/github-copilot.ts +362 -0
  289. package/src/utils/oauth/gitlab-duo.ts +123 -0
  290. package/src/utils/oauth/google-antigravity.ts +200 -0
  291. package/src/utils/oauth/google-gemini-cli.ts +256 -0
  292. package/src/utils/oauth/google-oauth-shared.ts +110 -0
  293. package/src/utils/oauth/huggingface.ts +62 -0
  294. package/src/utils/oauth/index.ts +444 -0
  295. package/src/utils/oauth/kagi.ts +47 -0
  296. package/src/utils/oauth/kilo.ts +87 -0
  297. package/src/utils/oauth/kimi.ts +254 -0
  298. package/src/utils/oauth/litellm.ts +47 -0
  299. package/src/utils/oauth/lm-studio.ts +38 -0
  300. package/src/utils/oauth/minimax-code.ts +78 -0
  301. package/src/utils/oauth/moonshot.ts +16 -0
  302. package/src/utils/oauth/nanogpt.ts +15 -0
  303. package/src/utils/oauth/nvidia.ts +70 -0
  304. package/src/utils/oauth/oauth.html +199 -0
  305. package/src/utils/oauth/ollama-cloud.ts +28 -0
  306. package/src/utils/oauth/ollama.ts +47 -0
  307. package/src/utils/oauth/openai-codex.ts +299 -0
  308. package/src/utils/oauth/opencode.ts +49 -0
  309. package/src/utils/oauth/parallel.ts +46 -0
  310. package/src/utils/oauth/perplexity.ts +206 -0
  311. package/src/utils/oauth/pkce.ts +18 -0
  312. package/src/utils/oauth/qianfan.ts +58 -0
  313. package/src/utils/oauth/qwen-portal.ts +60 -0
  314. package/src/utils/oauth/synthetic.ts +16 -0
  315. package/src/utils/oauth/tavily.ts +46 -0
  316. package/src/utils/oauth/together.ts +16 -0
  317. package/src/utils/oauth/types.ts +94 -0
  318. package/src/utils/oauth/venice.ts +59 -0
  319. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  320. package/src/utils/oauth/vllm.ts +40 -0
  321. package/src/utils/oauth/xiaomi.ts +137 -0
  322. package/src/utils/oauth/zai.ts +60 -0
  323. package/src/utils/oauth/zenmux.ts +15 -0
  324. package/src/utils/overflow.ts +137 -0
  325. package/src/utils/parse-bind.ts +54 -0
  326. package/src/utils/provider-response.ts +30 -0
  327. package/src/utils/retry-after.ts +110 -0
  328. package/src/utils/retry.ts +54 -0
  329. package/src/utils/schema/CONSTRAINTS.md +164 -0
  330. package/src/utils/schema/adapt.ts +36 -0
  331. package/src/utils/schema/compatibility.ts +435 -0
  332. package/src/utils/schema/dereference.ts +98 -0
  333. package/src/utils/schema/draft.ts +341 -0
  334. package/src/utils/schema/equality.ts +97 -0
  335. package/src/utils/schema/fields.ts +190 -0
  336. package/src/utils/schema/index.ts +13 -0
  337. package/src/utils/schema/json-schema-validator.ts +577 -0
  338. package/src/utils/schema/meta-validator.ts +167 -0
  339. package/src/utils/schema/normalize.ts +1588 -0
  340. package/src/utils/schema/spill.ts +43 -0
  341. package/src/utils/schema/stamps.ts +97 -0
  342. package/src/utils/schema/types.ts +11 -0
  343. package/src/utils/schema/wire.ts +213 -0
  344. package/src/utils/schema/zod-decontaminate.ts +331 -0
  345. package/src/utils/sse-debug.ts +289 -0
  346. package/src/utils/tool-call-healing.ts +271 -0
  347. package/src/utils/tool-choice.ts +99 -0
  348. package/src/utils/validation.ts +1019 -0
  349. package/src/utils.ts +166 -0
@@ -0,0 +1,1588 @@
1
+ /**
2
+ * Provider-specific JSON Schema normalization used in the request path.
3
+ *
4
+ * Google's Schema proto, Cloud Code Assist's Anthropic model bridge, and MCP/AJV
5
+ * validation all reject different subsets of standard JSON Schema. This module
6
+ * exposes one option-driven core plus thin dispatchers that pin the option set
7
+ * for each target.
8
+ */
9
+ import { logger } from "@gajae-code/utils";
10
+ import { dereferenceJsonSchema } from "./dereference";
11
+ import { upgradeJsonSchemaTo202012 } from "./draft";
12
+ import { areJsonValuesEqual, mergePropertySchemas } from "./equality";
13
+ import {
14
+ CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS,
15
+ CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS,
16
+ COMBINATOR_KEYS,
17
+ LIFTABLE_TO_DESCRIPTION_FIELDS,
18
+ NON_STRUCTURAL_SCHEMA_KEYS,
19
+ UNSUPPORTED_SCHEMA_FIELDS,
20
+ } from "./fields";
21
+ import { isValidJsonSchema } from "./meta-validator";
22
+ import { type DescriptionSpillFormat, spillToDescription } from "./spill";
23
+ import { enter, epochNext, exit, once, stamp } from "./stamps";
24
+ import { isJsonObject, isJsonObjectEmpty, type JsonObject } from "./types";
25
+ import { decontaminateZodInstance } from "./zod-decontaminate";
26
+
27
+ export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners";
28
+
29
+ export interface NormalizeSchemaOptions {
30
+ unsupportedFields: (key: string) => boolean;
31
+ normalizeFieldNames: boolean;
32
+ collapseNullFields: boolean;
33
+ normalizeTypeArrayToNullable: boolean;
34
+ stripNullableKeyword: boolean;
35
+ autoPropertyOrdering: boolean;
36
+ ensureObjectProperties: boolean;
37
+ liftStrippedToDescription:
38
+ | false
39
+ | {
40
+ keys?: (key: string) => boolean;
41
+ format?: DescriptionSpillFormat;
42
+ };
43
+ mergeObjectCombiners: boolean;
44
+ collapseSameTypeCombiners: boolean;
45
+ collapseMixedTypeCombiners: boolean;
46
+ stripResidualCombinersFixpoint: boolean;
47
+ extractNullableFromUnions: boolean;
48
+ rejectResidualIncompatibilities?: ReadonlyArray<ResidualSchemaIncompatibility>;
49
+ validateAndFallback?: { fallback: unknown };
50
+ }
51
+
52
+ interface NormalizeSchemaWalkOptions extends NormalizeSchemaOptions {
53
+ insideProperties: boolean;
54
+ epoch: number;
55
+ }
56
+
57
+ interface ResidualIncompatibilityChecks {
58
+ typeArray: boolean;
59
+ typeNull: boolean;
60
+ nullable: boolean;
61
+ combiners: boolean;
62
+ }
63
+
64
+ const SNAKE_TO_CAMEL_RENAMES = new Map<string, string>([
65
+ ["additional_properties", "additionalProperties"],
66
+ ["any_of", "anyOf"],
67
+ ["prefix_items", "prefixItems"],
68
+ ["property_ordering", "propertyOrdering"],
69
+ ]);
70
+
71
+ const JSON_SCHEMA_COMBINERS = ["anyOf", "oneOf"] as const;
72
+ const CCA_FORBIDDEN_COMBINERS = new Set(["anyOf", "oneOf", "allOf"]);
73
+
74
+ const CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA = {
75
+ type: "object",
76
+ properties: {},
77
+ } as const;
78
+
79
+ function isGoogleUnsupportedSchemaField(key: string): boolean {
80
+ return Object.hasOwn(UNSUPPORTED_SCHEMA_FIELDS, key);
81
+ }
82
+
83
+ function isMcpUnsupportedSchemaField(key: string): boolean {
84
+ return key === "$schema";
85
+ }
86
+
87
+ function isDefaultLiftableToDescriptionField(key: string): boolean {
88
+ return Object.hasOwn(LIFTABLE_TO_DESCRIPTION_FIELDS, key);
89
+ }
90
+
91
+ /**
92
+ * Returns `obj` unchanged when no renamable key is present; otherwise returns
93
+ * a fresh shallow-copy with snake_case keys rewritten. The collision rule
94
+ * matches upstream (`pop(from)` → `set(to)`): snake_case wins over an
95
+ * existing camelCase entry, matching python-genai/_transformers.py:751.
96
+ */
97
+ function applySnakeCaseRenames(obj: JsonObject): JsonObject {
98
+ let needsRename = false;
99
+ for (const k in obj) {
100
+ if (!Object.hasOwn(obj, k)) continue;
101
+ if (SNAKE_TO_CAMEL_RENAMES.has(k)) {
102
+ needsRename = true;
103
+ break;
104
+ }
105
+ }
106
+ if (!needsRename) return obj;
107
+ const out: JsonObject = {};
108
+ for (const k in obj) {
109
+ if (!Object.hasOwn(obj, k)) continue;
110
+ const renamed = SNAKE_TO_CAMEL_RENAMES.get(k);
111
+ if (renamed !== undefined) {
112
+ out[renamed] = obj[k];
113
+ } else if (!outHasOwn(out, k)) {
114
+ out[k] = obj[k];
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /**
121
+ * `handle_null_fields` (python-genai/_transformers.py:584-640) applied at the
122
+ * parent level BEFORE child recursion — matches upstream's call order at
123
+ * `process_schema` line 768. Returns a new object when changes apply, the
124
+ * original reference otherwise (zero-allocation fast path).
125
+ */
126
+ function preHandleNullFields(obj: JsonObject): JsonObject {
127
+ if (obj.type === "null") {
128
+ const out: JsonObject = {};
129
+ for (const k in obj) {
130
+ if (!Object.hasOwn(obj, k) || k === "type") continue;
131
+ out[k] = obj[k];
132
+ }
133
+ out.nullable = true;
134
+ return out;
135
+ }
136
+ if (!Array.isArray(obj.anyOf)) return obj;
137
+ const variants = obj.anyOf as unknown[];
138
+ let sawNull = false;
139
+ const kept: unknown[] = [];
140
+ for (const v of variants) {
141
+ if (isJsonObject(v) && v.type === "null") {
142
+ sawNull = true;
143
+ continue;
144
+ }
145
+ kept.push(v);
146
+ }
147
+ if (!sawNull) return obj;
148
+ const out: JsonObject = {};
149
+ for (const k in obj) {
150
+ if (Object.hasOwn(obj, k)) out[k] = obj[k];
151
+ }
152
+ out.nullable = true;
153
+ if (kept.length === 0) {
154
+ delete out.anyOf;
155
+ } else if (kept.length === 1 && isJsonObject(kept[0])) {
156
+ delete out.anyOf;
157
+ const only = kept[0];
158
+ for (const k in only) {
159
+ if (Object.hasOwn(only, k) && !outHasOwn(out, k)) out[k] = only[k];
160
+ }
161
+ } else {
162
+ out.anyOf = kept;
163
+ }
164
+ return out;
165
+ }
166
+
167
+ function outHasOwn(obj: JsonObject, key: string): boolean {
168
+ return Object.hasOwn(obj, key);
169
+ }
170
+
171
+ function inferJsonSchemaTypeFromValue(value: unknown): string | undefined {
172
+ if (value === null) return "null";
173
+ if (Array.isArray(value)) return "array";
174
+ switch (typeof value) {
175
+ case "string":
176
+ return "string";
177
+ case "number":
178
+ return "number";
179
+ case "boolean":
180
+ return "boolean";
181
+ case "object":
182
+ return "object";
183
+ default:
184
+ return undefined;
185
+ }
186
+ }
187
+
188
+ function pushEnumValue(values: unknown[], value: unknown): void {
189
+ if (!values.some(existing => areJsonValuesEqual(existing, value))) {
190
+ values.push(value);
191
+ }
192
+ }
193
+
194
+ function pushStrippedDescriptionEntry(
195
+ spill: Array<[string, unknown]> | undefined,
196
+ key: string,
197
+ value: unknown,
198
+ options: NormalizeSchemaWalkOptions,
199
+ ): Array<[string, unknown]> | undefined {
200
+ const lift = options.liftStrippedToDescription;
201
+ if (!lift) return spill;
202
+ const isLiftable = lift.keys ?? isDefaultLiftableToDescriptionField;
203
+ if (!isLiftable(key)) return spill;
204
+ const next = spill ?? [];
205
+ next.push([key, value]);
206
+ return next;
207
+ }
208
+
209
+ function applyDescriptionSpill(
210
+ result: JsonObject,
211
+ spill: Array<[string, unknown]> | undefined,
212
+ options: NormalizeSchemaWalkOptions,
213
+ ): void {
214
+ const lift = options.liftStrippedToDescription;
215
+ if (!lift || spill === undefined) return;
216
+ spillToDescription(result, spill, lift.format ?? "spill");
217
+ }
218
+
219
+ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions): unknown {
220
+ if (Array.isArray(value)) {
221
+ if (!once(value, options.epoch)) return [];
222
+ return value.map(entry => normalizeSchemaNode(entry, options));
223
+ }
224
+ if (!isJsonObject(value)) {
225
+ return value;
226
+ }
227
+ if (!once(value, options.epoch)) return {};
228
+ let obj = options.normalizeFieldNames && !options.insideProperties ? applySnakeCaseRenames(value) : value;
229
+ if (options.collapseNullFields && !options.insideProperties) {
230
+ obj = preHandleNullFields(obj);
231
+ }
232
+ const result: JsonObject = {};
233
+ let spill: Array<[string, unknown]> | undefined;
234
+ 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);
238
+ if (!allHaveConst || variants.length === 0) continue;
239
+
240
+ const dedupedEnum: unknown[] = [];
241
+ for (const variant of variants) {
242
+ pushEnumValue(dedupedEnum, variant.const);
243
+ }
244
+ result.enum = dedupedEnum;
245
+
246
+ const explicitTypes = variants
247
+ .map(variant => variant.type)
248
+ .filter((variantType): variantType is string => typeof variantType === "string");
249
+ const allHaveSameExplicitType =
250
+ explicitTypes.length === variants.length &&
251
+ explicitTypes.every(variantType => variantType === explicitTypes[0]);
252
+ if (allHaveSameExplicitType && explicitTypes[0]) {
253
+ result.type = explicitTypes[0];
254
+ } else {
255
+ const inferredTypes = dedupedEnum
256
+ .map(enumValue => inferJsonSchemaTypeFromValue(enumValue))
257
+ .filter((inferredType): inferredType is string => inferredType !== undefined);
258
+ const inferredTypeSet = new Set(inferredTypes);
259
+ if (inferredTypeSet.size === 1) {
260
+ result.type = inferredTypes[0];
261
+ } else {
262
+ const nonNullInferredTypes = inferredTypes.filter(inferredType => inferredType !== "null");
263
+ const nonNullTypeSet = new Set(nonNullInferredTypes);
264
+ if (inferredTypes.includes("null") && nonNullTypeSet.size === 1) {
265
+ result.type = nonNullInferredTypes[0];
266
+ if (!options.stripNullableKeyword) {
267
+ result.nullable = true;
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ for (const key in obj) {
274
+ if (!Object.hasOwn(obj, key) || key === combiner || outHasOwn(result, key)) continue;
275
+ const entry = obj[key];
276
+ if (!options.insideProperties && options.unsupportedFields(key)) {
277
+ spill = pushStrippedDescriptionEntry(spill, key, entry, options);
278
+ continue;
279
+ }
280
+ if (options.stripNullableKeyword && key === "nullable") continue;
281
+ result[key] = normalizeSchemaNode(entry, {
282
+ ...options,
283
+ insideProperties: key === "properties",
284
+ });
285
+ }
286
+ applyDescriptionSpill(result, spill, options);
287
+ return applyNodePostProcessing(result, options);
288
+ }
289
+
290
+ let constValue: unknown;
291
+ for (const key in obj) {
292
+ if (!Object.hasOwn(obj, key)) continue;
293
+ const entry = obj[key];
294
+ if (!options.insideProperties && options.unsupportedFields(key)) {
295
+ spill = pushStrippedDescriptionEntry(spill, key, entry, options);
296
+ continue;
297
+ }
298
+ if (options.stripNullableKeyword && key === "nullable") continue;
299
+ if (key === "const") {
300
+ constValue = entry;
301
+ continue;
302
+ }
303
+ result[key] = normalizeSchemaNode(entry, {
304
+ ...options,
305
+ insideProperties: key === "properties",
306
+ });
307
+ }
308
+
309
+ if (options.normalizeTypeArrayToNullable && Array.isArray(result.type)) {
310
+ const types = (result.type as unknown[]).filter((t): t is string => typeof t === "string");
311
+ const nonNull = types.filter(t => t !== "null");
312
+ if (types.includes("null") && !options.stripNullableKeyword) {
313
+ result.nullable = true;
314
+ }
315
+ result.type = nonNull[0] ?? types[0];
316
+ }
317
+ if (constValue !== undefined) {
318
+ const existingEnum = Array.isArray(result.enum) ? result.enum : [];
319
+ pushEnumValue(existingEnum, constValue);
320
+ result.enum = existingEnum;
321
+ if (!result.type) {
322
+ result.type = inferJsonSchemaTypeFromValue(constValue);
323
+ }
324
+ }
325
+
326
+ if (options.collapseNullFields && result.type === "null") {
327
+ delete result.type;
328
+ if (!options.stripNullableKeyword) result.nullable = true;
329
+ }
330
+
331
+ if (
332
+ options.autoPropertyOrdering &&
333
+ result.type === "object" &&
334
+ !outHasOwn(result, "propertyOrdering") &&
335
+ isJsonObject(result.properties)
336
+ ) {
337
+ const props = result.properties;
338
+ const keys: string[] = [];
339
+ for (const k in props) {
340
+ if (Object.hasOwn(props, k)) keys.push(k);
341
+ }
342
+ if (keys.length > 1) result.propertyOrdering = keys;
343
+ }
344
+
345
+ if (options.ensureObjectProperties && result.type === "object" && !outHasOwn(result, "properties")) {
346
+ result.properties = {};
347
+ }
348
+
349
+ applyDescriptionSpill(result, spill, options);
350
+ return applyNodePostProcessing(result, options);
351
+ }
352
+
353
+ function applyNodePostProcessing(schema: JsonObject, options: NormalizeSchemaWalkOptions): JsonObject {
354
+ let current = schema;
355
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
356
+ if (options.mergeObjectCombiners) current = mergeObjectCombinerVariants(current, combiner);
357
+ if (options.collapseMixedTypeCombiners) current = collapseMixedTypeCombinerVariants(current, combiner);
358
+ if (options.collapseSameTypeCombiners) current = collapseSameTypeCombinerVariants(current, combiner);
359
+ }
360
+ return current;
361
+ }
362
+
363
+ /** Copy all keys from a schema except the specified combiner key. */
364
+ export function copySchemaWithout(schema: JsonObject, combiner: string): JsonObject {
365
+ const { [combiner]: _, ...rest } = schema;
366
+ return rest;
367
+ }
368
+
369
+ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
370
+ const variantsRaw = schema[combiner];
371
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
372
+ return schema;
373
+ }
374
+
375
+ const variants: JsonObject[] = [];
376
+ for (const entry of variantsRaw) {
377
+ if (!isJsonObject(entry)) {
378
+ return schema;
379
+ }
380
+ const variantType = entry.type;
381
+ const hasObjectShape =
382
+ isJsonObject(entry.properties) ||
383
+ Array.isArray(entry.required) ||
384
+ Object.hasOwn(entry, "additionalProperties");
385
+ if (variantType === undefined && !hasObjectShape) {
386
+ return schema;
387
+ }
388
+ if (variantType !== undefined && variantType !== "object") {
389
+ return schema;
390
+ }
391
+ if (entry.properties !== undefined && !isJsonObject(entry.properties)) {
392
+ return schema;
393
+ }
394
+ if (entry.required !== undefined && !Array.isArray(entry.required)) {
395
+ return schema;
396
+ }
397
+ variants.push(entry);
398
+ }
399
+
400
+ const mergedProperties: JsonObject = {};
401
+ const ownProperties = isJsonObject(schema.properties) ? schema.properties : {};
402
+ for (const name in ownProperties) {
403
+ if (Object.hasOwn(ownProperties, name)) mergedProperties[name] = ownProperties[name];
404
+ }
405
+
406
+ for (const variant of variants) {
407
+ const properties = isJsonObject(variant.properties) ? variant.properties : {};
408
+ for (const name in properties) {
409
+ if (!Object.hasOwn(properties, name)) continue;
410
+ const propertySchema = properties[name];
411
+ const existingSchema = mergedProperties[name];
412
+ mergedProperties[name] =
413
+ existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema);
414
+ }
415
+ }
416
+
417
+ const nextSchema = copySchemaWithout(schema, combiner);
418
+ nextSchema.type = "object";
419
+ nextSchema.properties = mergedProperties;
420
+
421
+ let requiredIntersection: string[] | undefined;
422
+ for (const variant of variants) {
423
+ const variantRequired = Array.isArray(variant.required)
424
+ ? variant.required.filter((r): r is string => typeof r === "string")
425
+ : [];
426
+ if (requiredIntersection === undefined) {
427
+ requiredIntersection = [...variantRequired];
428
+ } else {
429
+ const reqSet = new Set(variantRequired);
430
+ requiredIntersection = requiredIntersection.filter(r => reqSet.has(r));
431
+ }
432
+ }
433
+ const parentRequired = Array.isArray(schema.required)
434
+ ? schema.required.filter((r): r is string => typeof r === "string")
435
+ : [];
436
+ const safeRequired = new Set<string>();
437
+ for (const name of requiredIntersection ?? []) {
438
+ if (Object.hasOwn(mergedProperties, name)) safeRequired.add(name);
439
+ }
440
+ for (const name of parentRequired) {
441
+ if (Object.hasOwn(ownProperties, name) && Object.hasOwn(mergedProperties, name)) {
442
+ safeRequired.add(name);
443
+ }
444
+ }
445
+ const requiredInPropertyOrder: string[] = [];
446
+ for (const name in mergedProperties) {
447
+ if (Object.hasOwn(mergedProperties, name) && safeRequired.has(name)) requiredInPropertyOrder.push(name);
448
+ }
449
+ if (requiredInPropertyOrder.length > 0) {
450
+ nextSchema.required = requiredInPropertyOrder;
451
+ } else {
452
+ delete nextSchema.required;
453
+ }
454
+
455
+ return nextSchema;
456
+ }
457
+
458
+ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
459
+ const variantsRaw = schema[combiner];
460
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
461
+ return schema;
462
+ }
463
+
464
+ const seenTypes = new Set<string>();
465
+ const variantTypes: string[] = [];
466
+ const mergedVariantFields: JsonObject = {};
467
+ for (const entry of variantsRaw) {
468
+ if (!isJsonObject(entry) || typeof entry.type !== "string") {
469
+ return schema;
470
+ }
471
+
472
+ const variantType = entry.type;
473
+ if (seenTypes.has(variantType)) {
474
+ return schema;
475
+ }
476
+
477
+ const allowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[variantType];
478
+ if (!allowedKeys) {
479
+ return schema;
480
+ }
481
+
482
+ for (const key in entry) {
483
+ if (!Object.hasOwn(entry, key)) continue;
484
+ const variantValue = entry[key];
485
+ if (key === "type") continue;
486
+ if (!Object.hasOwn(allowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) {
487
+ return schema;
488
+ }
489
+
490
+ const existingValue = mergedVariantFields[key];
491
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, variantValue)) {
492
+ return schema;
493
+ }
494
+ mergedVariantFields[key] = variantValue;
495
+ }
496
+
497
+ seenTypes.add(variantType);
498
+ variantTypes.push(variantType);
499
+ }
500
+
501
+ if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
502
+ return schema;
503
+ }
504
+
505
+ const nextSchema = copySchemaWithout(schema, combiner);
506
+ const nonNullTypes = variantTypes.filter(t => t !== "null");
507
+ nextSchema.type = nonNullTypes[0] ?? variantTypes[0];
508
+ for (const key in mergedVariantFields) {
509
+ if (!Object.hasOwn(mergedVariantFields, key)) continue;
510
+ const value = mergedVariantFields[key];
511
+ const existingValue = nextSchema[key];
512
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
513
+ return schema;
514
+ }
515
+ if (existingValue === undefined) {
516
+ nextSchema[key] = value;
517
+ }
518
+ }
519
+ return nextSchema;
520
+ }
521
+
522
+ function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
523
+ const variantsRaw = schema[combiner];
524
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) return schema;
525
+ let commonType: string | undefined;
526
+ let firstEntry: JsonObject | undefined;
527
+ for (const entry of variantsRaw) {
528
+ if (!isJsonObject(entry) || typeof entry.type !== "string") return schema;
529
+ if (commonType === undefined) {
530
+ commonType = entry.type;
531
+ firstEntry = entry;
532
+ } else if (entry.type !== commonType) return schema;
533
+ }
534
+ if (!firstEntry) return schema;
535
+ const nextSchema = copySchemaWithout(schema, combiner);
536
+ for (const key in firstEntry) {
537
+ if (Object.hasOwn(firstEntry, key) && !outHasOwn(nextSchema, key)) nextSchema[key] = firstEntry[key];
538
+ }
539
+ return nextSchema;
540
+ }
541
+
542
+ /**
543
+ * Recursively strip any remaining anyOf/oneOf that same-type or mixed-type
544
+ * collapse can handle. This is needed because object-combiner merging can
545
+ * create new anyOf in merged subtrees after child normalization already ran.
546
+ */
547
+ export function stripResidualCombiners(value: unknown, epoch: number = epochNext()): unknown {
548
+ if (Array.isArray(value)) {
549
+ if (!once(value, epoch)) return [];
550
+ return value.map(entry => stripResidualCombiners(entry, epoch));
551
+ }
552
+ if (!isJsonObject(value)) return value;
553
+ if (!once(value, epoch)) return {};
554
+ const result: JsonObject = {};
555
+ for (const key in value) {
556
+ if (Object.hasOwn(value, key)) result[key] = stripResidualCombiners(value[key], epoch);
557
+ }
558
+ let current: JsonObject = result;
559
+ let changed = true;
560
+ while (changed) {
561
+ changed = false;
562
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
563
+ const sameType = collapseSameTypeCombinerVariants(current, combiner);
564
+ if (sameType !== current) {
565
+ current = sameType;
566
+ changed = true;
567
+ }
568
+ const mixed = collapseMixedTypeCombinerVariants(current, combiner);
569
+ if (mixed !== current) {
570
+ current = mixed;
571
+ changed = true;
572
+ }
573
+ }
574
+ }
575
+ return current;
576
+ }
577
+
578
+ interface NullableExtractionResult {
579
+ schema: unknown;
580
+ nullable: boolean;
581
+ }
582
+
583
+ function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
584
+ if (!isJsonObject(schema)) {
585
+ return { schema, nullable: false };
586
+ }
587
+
588
+ if (schema.nullable === true) {
589
+ const nextSchema = { ...schema };
590
+ delete nextSchema.nullable;
591
+ return { schema: nextSchema, nullable: true };
592
+ }
593
+
594
+ if (Array.isArray(schema.type)) {
595
+ const typeVariants = schema.type.filter((entry): entry is string => typeof entry === "string");
596
+ const nonNullTypes = typeVariants.filter(entry => entry !== "null");
597
+ if (typeVariants.includes("null") && nonNullTypes.length === 1) {
598
+ const nextSchema = { ...schema, type: nonNullTypes[0] };
599
+ return { schema: nextSchema, nullable: true };
600
+ }
601
+ }
602
+
603
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
604
+ const variantsRaw = schema[combiner];
605
+ if (!Array.isArray(variantsRaw)) continue;
606
+
607
+ let hasNullVariant = false;
608
+ const nonNullVariants: unknown[] = [];
609
+ for (const variant of variantsRaw) {
610
+ if (isJsonObject(variant) && variant.type === "null") {
611
+ let keyCount = 0;
612
+ for (const k in variant) {
613
+ if (!Object.hasOwn(variant, k)) continue;
614
+ if (++keyCount > 1) break;
615
+ }
616
+ if (keyCount === 1) {
617
+ hasNullVariant = true;
618
+ continue;
619
+ }
620
+ }
621
+ nonNullVariants.push(variant);
622
+ }
623
+
624
+ if (!hasNullVariant || nonNullVariants.length !== 1 || !isJsonObject(nonNullVariants[0])) {
625
+ continue;
626
+ }
627
+
628
+ const nextSchema = copySchemaWithout(schema, combiner);
629
+ const nonNullVariant = nonNullVariants[0];
630
+ for (const key in nonNullVariant) {
631
+ if (!Object.hasOwn(nonNullVariant, key)) continue;
632
+ const value = nonNullVariant[key];
633
+ const existingValue = nextSchema[key];
634
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
635
+ return { schema, nullable: false };
636
+ }
637
+ if (existingValue === undefined) {
638
+ nextSchema[key] = value;
639
+ }
640
+ }
641
+ return { schema: nextSchema, nullable: true };
642
+ }
643
+
644
+ return { schema, nullable: false };
645
+ }
646
+
647
+ interface NullableNormalizationResult {
648
+ schema: unknown;
649
+ nullable: boolean;
650
+ }
651
+
652
+ function normalizeNullablePropertiesForCloudCodeAssist(
653
+ value: unknown,
654
+ isPropertySchema = false,
655
+ epoch: number = epochNext(),
656
+ ): NullableNormalizationResult {
657
+ if (Array.isArray(value)) {
658
+ if (!once(value, epoch)) {
659
+ return { schema: [], nullable: false };
660
+ }
661
+ return {
662
+ schema: value.map(entry => normalizeNullablePropertiesForCloudCodeAssist(entry, false, epoch).schema),
663
+ nullable: false,
664
+ };
665
+ }
666
+ if (!isJsonObject(value)) {
667
+ return { schema: value, nullable: false };
668
+ }
669
+ if (!once(value, epoch)) {
670
+ return { schema: {}, nullable: false };
671
+ }
672
+
673
+ const normalized: JsonObject = {};
674
+ for (const key in value) {
675
+ if (Object.hasOwn(value, key))
676
+ normalized[key] = normalizeNullablePropertiesForCloudCodeAssist(value[key], false, epoch).schema;
677
+ }
678
+
679
+ if (isJsonObject(normalized.properties)) {
680
+ const properties = normalized.properties;
681
+ const required = new Set(
682
+ Array.isArray(normalized.required)
683
+ ? normalized.required.filter((entry): entry is string => typeof entry === "string")
684
+ : [],
685
+ );
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)) {
697
+ normalized.required = Array.from(required);
698
+ }
699
+ }
700
+
701
+ if (!isPropertySchema) {
702
+ return { schema: normalized, nullable: false };
703
+ }
704
+
705
+ return extractNullableUnionSchema(normalized);
706
+ }
707
+
708
+ function createResidualIncompatibilityChecks(
709
+ checks: ReadonlyArray<ResidualSchemaIncompatibility> | undefined,
710
+ ): ResidualIncompatibilityChecks | undefined {
711
+ if (!checks || checks.length === 0) return undefined;
712
+ const result: ResidualIncompatibilityChecks = {
713
+ typeArray: false,
714
+ typeNull: false,
715
+ nullable: false,
716
+ combiners: false,
717
+ };
718
+ for (const check of checks) {
719
+ switch (check) {
720
+ case "type-array":
721
+ result.typeArray = true;
722
+ break;
723
+ case "type-null":
724
+ result.typeNull = true;
725
+ break;
726
+ case "nullable":
727
+ result.nullable = true;
728
+ break;
729
+ case "combiners":
730
+ result.combiners = true;
731
+ break;
732
+ }
733
+ }
734
+ return result;
735
+ }
736
+
737
+ function hasResidualSchemaIncompatibilities(
738
+ value: unknown,
739
+ checks: ResidualIncompatibilityChecks,
740
+ epoch: number = epochNext(),
741
+ ): boolean {
742
+ if (Array.isArray(value)) {
743
+ if (!once(value, epoch)) return false;
744
+ return value.some(entry => hasResidualSchemaIncompatibilities(entry, checks, epoch));
745
+ }
746
+ if (!isJsonObject(value)) {
747
+ return false;
748
+ }
749
+ if (!once(value, epoch)) {
750
+ return false;
751
+ }
752
+
753
+ if (checks.typeArray && Array.isArray(value.type)) return true;
754
+ if (checks.typeNull && value.type === "null") return true;
755
+ if (checks.nullable && Object.hasOwn(value, "nullable")) return true;
756
+ if (checks.combiners) {
757
+ for (const combiner of CCA_FORBIDDEN_COMBINERS) {
758
+ if (Array.isArray(value[combiner])) return true;
759
+ }
760
+ }
761
+ for (const k in value) {
762
+ if (!Object.hasOwn(value, k)) continue;
763
+ if (hasResidualSchemaIncompatibilities(value[k], checks, epoch)) {
764
+ return true;
765
+ }
766
+ }
767
+ return false;
768
+ }
769
+
770
+ export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions): unknown {
771
+ const detoxified = decontaminateZodInstance(value);
772
+ const upgraded = upgradeJsonSchemaTo202012(detoxified);
773
+ const dereferenced = dereferenceJsonSchema(upgraded);
774
+ let normalized = normalizeSchemaNode(dereferenced, {
775
+ ...options,
776
+ insideProperties: false,
777
+ epoch: epochNext(),
778
+ });
779
+ if (options.stripResidualCombinersFixpoint) {
780
+ normalized = stripResidualCombiners(normalized);
781
+ }
782
+ if (options.extractNullableFromUnions) {
783
+ normalized = normalizeNullablePropertiesForCloudCodeAssist(normalized).schema;
784
+ }
785
+ const residualChecks = createResidualIncompatibilityChecks(options.rejectResidualIncompatibilities);
786
+ if (residualChecks && hasResidualSchemaIncompatibilities(normalized, residualChecks)) {
787
+ logger.debug("Schema has residual provider incompatibilities, using fallback");
788
+ return options.validateAndFallback?.fallback ?? normalized;
789
+ }
790
+ if (options.validateAndFallback && !isValidJsonSchema(normalized)) {
791
+ logger.debug("Schema failed validation, using fallback");
792
+ return options.validateAndFallback.fallback;
793
+ }
794
+ return normalized;
795
+ }
796
+
797
+ export function normalizeSchemaForGoogle(value: unknown): unknown {
798
+ return normalizeSchema(value, {
799
+ unsupportedFields: isGoogleUnsupportedSchemaField,
800
+ normalizeFieldNames: true,
801
+ collapseNullFields: true,
802
+ normalizeTypeArrayToNullable: true,
803
+ stripNullableKeyword: false,
804
+ autoPropertyOrdering: true,
805
+ ensureObjectProperties: true,
806
+ liftStrippedToDescription: { format: "spill" },
807
+ mergeObjectCombiners: false,
808
+ collapseSameTypeCombiners: false,
809
+ collapseMixedTypeCombiners: false,
810
+ stripResidualCombinersFixpoint: false,
811
+ extractNullableFromUnions: false,
812
+ });
813
+ }
814
+
815
+ export function normalizeSchemaForCCA(value: unknown): unknown {
816
+ return normalizeSchema(value, {
817
+ unsupportedFields: isGoogleUnsupportedSchemaField,
818
+ normalizeFieldNames: true,
819
+ collapseNullFields: false,
820
+ normalizeTypeArrayToNullable: true,
821
+ stripNullableKeyword: true,
822
+ autoPropertyOrdering: false,
823
+ ensureObjectProperties: true,
824
+ liftStrippedToDescription: { format: "spill" },
825
+ mergeObjectCombiners: true,
826
+ collapseSameTypeCombiners: true,
827
+ collapseMixedTypeCombiners: true,
828
+ stripResidualCombinersFixpoint: true,
829
+ extractNullableFromUnions: true,
830
+ rejectResidualIncompatibilities: ["type-array", "type-null", "nullable", "combiners"],
831
+ validateAndFallback: { fallback: CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA },
832
+ });
833
+ }
834
+
835
+ export function normalizeSchemaForMCP(value: unknown): unknown {
836
+ return normalizeSchema(value, {
837
+ unsupportedFields: isMcpUnsupportedSchemaField,
838
+ normalizeFieldNames: false,
839
+ collapseNullFields: false,
840
+ normalizeTypeArrayToNullable: false,
841
+ stripNullableKeyword: true,
842
+ autoPropertyOrdering: false,
843
+ ensureObjectProperties: false,
844
+ liftStrippedToDescription: false,
845
+ mergeObjectCombiners: false,
846
+ collapseSameTypeCombiners: false,
847
+ collapseMixedTypeCombiners: false,
848
+ stripResidualCombinersFixpoint: false,
849
+ extractNullableFromUnions: false,
850
+ });
851
+ }
852
+
853
+ // ---------------------------------------------------------------------------
854
+ // OpenAI Responses — schema-valued normalization
855
+ // ---------------------------------------------------------------------------
856
+
857
+ const OPENAI_RESPONSES_SCHEMA_ARRAY_KEYS = new Set(["anyOf", "oneOf", "allOf", "prefixItems"]);
858
+ const OPENAI_RESPONSES_SCHEMA_MAP_KEYS = new Set([
859
+ "properties",
860
+ "patternProperties",
861
+ // `dependencies` is the Draft-04..07 schema-valued form; older MCP servers
862
+ // still emit `{ dependencies: { foo: { type: "object" } } }`. String-array
863
+ // branches per key pass through `normalizeOpenAIResponsesSchemaNode`
864
+ // untouched because non-objects return as-is.
865
+ "dependencies",
866
+ "dependentSchemas",
867
+ "$defs",
868
+ "definitions",
869
+ ]);
870
+ const OPENAI_RESPONSES_SCHEMA_VALUE_KEYS = new Set([
871
+ "items",
872
+ "additionalItems",
873
+ "contains",
874
+ "contentSchema",
875
+ "propertyNames",
876
+ "if",
877
+ "then",
878
+ "else",
879
+ "not",
880
+ "additionalProperties",
881
+ "unevaluatedItems",
882
+ "unevaluatedProperties",
883
+ ]);
884
+
885
+ /**
886
+ * OpenAI Responses rejects `oneOf` in tool schemas even when strict mode is
887
+ * disabled, and rejects every schema node with `type: "object"` unless it has
888
+ * a `properties` member. Normalize only schema-valued positions so literal
889
+ * payloads under `enum`, `const`, `default`, and `examples` remain unchanged.
890
+ *
891
+ * Identity-preserving: returns the input reference unchanged when no rewrite
892
+ * occurred so callers can dedupe via reference equality (and the strict-mode
893
+ * cache stays warm). If a node has both `oneOf` and `anyOf`, the two are
894
+ * concatenated (the wire payload accepts a single union; preserving both
895
+ * would not survive).
896
+ */
897
+ export function sanitizeSchemaForOpenAIResponses(schema: JsonObject): JsonObject {
898
+ return normalizeOpenAIResponsesSchemaNode(schema, new WeakMap()) as JsonObject;
899
+ }
900
+
901
+ /**
902
+ * Alias for {@link sanitizeSchemaForOpenAIResponses} matching the
903
+ * `normalizeSchemaFor*` dispatcher naming used elsewhere in this module.
904
+ */
905
+ export const normalizeSchemaForOpenAIResponses: (schema: JsonObject) => JsonObject = sanitizeSchemaForOpenAIResponses;
906
+
907
+ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonObject, JsonObject>): unknown {
908
+ if (!isJsonObject(value)) return value;
909
+
910
+ // `{}` (empty JSON Schema) ≡ `true` (JSON Schema draft 2020-12 §4.3.1).
911
+ // Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
912
+ // "generate an empty object" rather than "any JSON value" (issue #1179).
913
+ // `toolWireSchema` already runs `normalizeEmptySchemas` upstream, but this
914
+ // guard remains as a safety net for callers that invoke
915
+ // `sanitizeSchemaForOpenAIResponses` directly on a schema that bypassed
916
+ // the wire-schema pipeline (e.g. provider-specific fixtures, debug paths).
917
+ if (isJsonObjectEmpty(value)) return true;
918
+
919
+ const cached = cache.get(value);
920
+ if (cached) return cached;
921
+
922
+ // Seed the cache with the in-flight `output` BEFORE recursing so that a
923
+ // child re-entering this node mid-walk gets the partial back instead of
924
+ // triggering an infinite recursion. A cycle hitting this seeded entry
925
+ // forces `changed = true` below (the cached partial is referentially
926
+ // distinct from `value`), which is why the final `cache.set(value, result)`
927
+ // never silently overwrites the seed with `value` on a cyclic input.
928
+ const output: JsonObject = {};
929
+ cache.set(value, output);
930
+
931
+ let changed = false;
932
+ for (const key in value) {
933
+ if (!Object.hasOwn(value, key)) continue;
934
+ // Drop only well-formed `oneOf` arrays here; they are re-emitted as
935
+ // `anyOf` after the loop so any neighboring `anyOf` entries can be
936
+ // concatenated. A non-array `oneOf` is malformed for the wire but
937
+ // still preserved verbatim so callers can see the original payload
938
+ // instead of having it silently disappear.
939
+ if (key === "oneOf" && Array.isArray(value.oneOf)) {
940
+ changed = true;
941
+ continue;
942
+ }
943
+
944
+ const child = value[key];
945
+ let next: unknown = child;
946
+ if (OPENAI_RESPONSES_SCHEMA_MAP_KEYS.has(key) && isJsonObject(child)) {
947
+ next = normalizeOpenAIResponsesSchemaMap(child, cache);
948
+ } else if (OPENAI_RESPONSES_SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(child)) {
949
+ next = normalizeOpenAIResponsesSchemaArray(child, cache);
950
+ } else if (OPENAI_RESPONSES_SCHEMA_VALUE_KEYS.has(key) && isJsonObject(child)) {
951
+ next = normalizeOpenAIResponsesSchemaNode(child, cache);
952
+ }
953
+
954
+ if (next !== child) changed = true;
955
+ output[key] = next;
956
+ }
957
+
958
+ if (Array.isArray(value.oneOf)) {
959
+ const rewrittenOneOf = normalizeOpenAIResponsesSchemaArray(value.oneOf, cache);
960
+ const existingAnyOf = output.anyOf;
961
+ output.anyOf = Array.isArray(existingAnyOf)
962
+ ? [...existingAnyOf, ...(rewrittenOneOf as unknown[])]
963
+ : rewrittenOneOf;
964
+ }
965
+
966
+ // Draft 2020-12 lets `type` be an array (e.g. `["object", "null"]`); treat
967
+ // any variant that includes "object" as an object position for the
968
+ // properties requirement.
969
+ if (declaresObjectType(value.type) && !Object.hasOwn(value, "properties")) {
970
+ output.properties = {};
971
+ changed = true;
972
+ }
973
+
974
+ // Safe to overwrite the seed: any cyclic re-entry above already observed
975
+ // the seeded partial and set `changed = true` for that node, so a node
976
+ // that finishes with `changed === false` is provably non-cyclic and
977
+ // referentially equal to its input.
978
+ const result = changed ? output : value;
979
+ cache.set(value, result);
980
+ return result;
981
+ }
982
+
983
+ function declaresObjectType(type: unknown): boolean {
984
+ if (type === "object") return true;
985
+ if (!Array.isArray(type)) return false;
986
+ for (const variant of type) {
987
+ if (variant === "object") return true;
988
+ }
989
+ return false;
990
+ }
991
+
992
+ function normalizeOpenAIResponsesSchemaArray(value: unknown[], cache: WeakMap<JsonObject, JsonObject>): unknown[] {
993
+ let changed = false;
994
+ const output = value.map(item => {
995
+ const next = normalizeOpenAIResponsesSchemaNode(item, cache);
996
+ if (next !== item) changed = true;
997
+ return next;
998
+ });
999
+ return changed ? output : value;
1000
+ }
1001
+
1002
+ function normalizeOpenAIResponsesSchemaMap(schemaMap: JsonObject, cache: WeakMap<JsonObject, JsonObject>): JsonObject {
1003
+ let changed = false;
1004
+ const output: JsonObject = {};
1005
+ for (const key in schemaMap) {
1006
+ if (!Object.hasOwn(schemaMap, key)) continue;
1007
+ const child = schemaMap[key];
1008
+ const next = normalizeOpenAIResponsesSchemaNode(child, cache);
1009
+ if (next !== child) changed = true;
1010
+ output[key] = next;
1011
+ }
1012
+ return changed ? output : schemaMap;
1013
+ }
1014
+
1015
+ // ---------------------------------------------------------------------------
1016
+ // OpenAI strict mode — sanitize + enforce
1017
+ // ---------------------------------------------------------------------------
1018
+
1019
+ /**
1020
+ * Single primitive JSON Schema `type` keyword. Strict mode treats these
1021
+ * scalar types as concrete-enough; aggregate shapes (object, array) are not
1022
+ * included because they're not derivable from a single `enum`/`const` value.
1023
+ */
1024
+ type StrictPrimitiveType = "null" | "string" | "number" | "boolean";
1025
+
1026
+ function primitiveJsonTypeOf(value: unknown): StrictPrimitiveType | undefined {
1027
+ if (value === null) return "null";
1028
+ switch (typeof value) {
1029
+ case "string":
1030
+ return "string";
1031
+ case "number":
1032
+ return "number";
1033
+ case "boolean":
1034
+ return "boolean";
1035
+ default:
1036
+ return undefined;
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Returns the primitive `type` keyword that fully describes the constraint
1042
+ * expressed by this node's `enum` (or `const`), or `undefined` when the
1043
+ * constraint cannot be reduced to a single primitive type.
1044
+ *
1045
+ * Strict mode requires every schema node to declare a concrete `type`. When
1046
+ * the author wrote `{enum:[...]}` or `{const:X}` without a `type`, we can
1047
+ * infer one — but only when every value reduces to the same primitive type.
1048
+ * Mixed-primitive enums (`[1, "two", null]`), enums containing non-primitives
1049
+ * (`[{a:1}]`), and non-primitive consts (`{a:1}`, `[1,2,3]`) all return
1050
+ * undefined: those shapes cannot be described by a single `type` keyword, so
1051
+ * strict mode cannot represent them and the caller must fall back.
1052
+ */
1053
+ function inferStrictPrimitiveTypeFromEnumOrConst(node: Record<string, unknown>): StrictPrimitiveType | undefined {
1054
+ const values: unknown[] = Array.isArray(node.enum) ? node.enum : Object.hasOwn(node, "const") ? [node.const] : [];
1055
+ if (values.length === 0) return undefined;
1056
+ let inferred: StrictPrimitiveType | undefined;
1057
+ for (const value of values) {
1058
+ const t = primitiveJsonTypeOf(value);
1059
+ if (t === undefined) return undefined; // non-primitive (object/array) — strict can't represent
1060
+ if (inferred === undefined) inferred = t;
1061
+ else if (inferred !== t) return undefined; // mixed primitives
1062
+ }
1063
+ return inferred;
1064
+ }
1065
+
1066
+ /**
1067
+ * Per-schema-object memoization slot. The result of `tryEnforceStrictSchema`
1068
+ * is stamped directly onto the input via `stamp(target, kStrictSchema, …)`
1069
+ * so repeated calls (different providers, retries, batching) reuse the same
1070
+ * computed pair without re-walking the tree.
1071
+ */
1072
+ const kStrictSchema = Symbol("pi.schema.strict");
1073
+
1074
+ /**
1075
+ * Detect schemas that strict mode *cannot* represent.
1076
+ *
1077
+ * Strict mode requires closed object shapes — every property is declared in
1078
+ * `properties` and listed in `required`. That is incompatible with:
1079
+ * - `patternProperties` (open keyset matched by regex),
1080
+ * - `additionalProperties: true` or `additionalProperties: <schema>` (open
1081
+ * keyset with optional further constraint).
1082
+ *
1083
+ * This check recurses into every place a child schema may live (properties,
1084
+ * items/prefixItems, combinator branches, $defs) so a single offender deep
1085
+ * in the tree disqualifies the whole schema. Used to fail-open early in
1086
+ * `tryEnforceStrictSchema` rather than throwing during enforcement.
1087
+ */
1088
+ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoch: number = epochNext()): boolean {
1089
+ if (!once(schema, epoch)) return false;
1090
+
1091
+ let hasPatternProperties = false;
1092
+ if (isJsonObject(schema.patternProperties)) {
1093
+ for (const _ in schema.patternProperties) {
1094
+ hasPatternProperties = true;
1095
+ break;
1096
+ }
1097
+ }
1098
+ const additionalPropertiesValue = schema.additionalProperties;
1099
+ const hasSchemaAdditionalProperties = additionalPropertiesValue === true || isJsonObject(additionalPropertiesValue);
1100
+ if (hasPatternProperties || hasSchemaAdditionalProperties) {
1101
+ return true;
1102
+ }
1103
+
1104
+ if (isJsonObject(schema.properties)) {
1105
+ const properties = schema.properties;
1106
+ for (const k in properties) {
1107
+ const propertySchema = properties[k];
1108
+ if (isJsonObject(propertySchema) && hasUnrepresentableStrictObjectMap(propertySchema, epoch)) {
1109
+ return true;
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ if (isJsonObject(schema.items)) {
1115
+ if (hasUnrepresentableStrictObjectMap(schema.items, epoch)) {
1116
+ return true;
1117
+ }
1118
+ } else if (Array.isArray(schema.items)) {
1119
+ for (const itemSchema of schema.items) {
1120
+ if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1121
+ return true;
1122
+ }
1123
+ }
1124
+ }
1125
+ if (Array.isArray(schema.prefixItems)) {
1126
+ for (const itemSchema of schema.prefixItems) {
1127
+ if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1128
+ return true;
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ for (const key of COMBINATOR_KEYS) {
1134
+ const variants = schema[key];
1135
+ if (!Array.isArray(variants)) continue;
1136
+ for (const variant of variants) {
1137
+ if (isJsonObject(variant) && hasUnrepresentableStrictObjectMap(variant, epoch)) {
1138
+ return true;
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ for (const defsKey of ["$defs", "definitions"] as const) {
1144
+ const defs = schema[defsKey];
1145
+ if (!isJsonObject(defs)) continue;
1146
+ for (const k in defs) {
1147
+ const defSchema = defs[k];
1148
+ if (isJsonObject(defSchema) && hasUnrepresentableStrictObjectMap(defSchema, epoch)) {
1149
+ return true;
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ return false;
1155
+ }
1156
+
1157
+ /**
1158
+ * First pass of strict-mode preparation.
1159
+ *
1160
+ * Rewrites everything strict mode forbids into something it accepts:
1161
+ * - Drops non-structural keywords (`format`, `pattern`, `examples`, …),
1162
+ * `const`, `nullable`, and `additionalProperties` (re-added by
1163
+ * `enforceStrictSchema` as `false`).
1164
+ * - `type: [a, b]` → `anyOf: [{type: a, …}, {type: b, …}]`, copying only the
1165
+ * keywords each variant can use (e.g. `properties` stays only on the
1166
+ * object variant).
1167
+ * - `const` → single-entry `enum`.
1168
+ * - Description carries a `(default: X)` suffix so the model still sees the
1169
+ * documented default after the keyword is stripped.
1170
+ * - `nullable: true` wraps the whole node in `anyOf:[T,{type:"null"}]`.
1171
+ *
1172
+ * Recurses into properties, items, prefixItems, combinators, and $defs. The
1173
+ * `cache` WeakMap dedupes shared subgraphs; the `epoch` is the cycle guard.
1174
+ */
1175
+ export function sanitizeSchemaForStrictMode(
1176
+ schema: Record<string, unknown>,
1177
+ epoch: number = epochNext(),
1178
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
1179
+ root: Record<string, unknown> = schema,
1180
+ ): Record<string, unknown> {
1181
+ const cached = cache.get(schema);
1182
+ if (cached) return cached;
1183
+ if (!once(schema, epoch)) return {};
1184
+
1185
+ // Pre-pass: unravel `$ref` with sibling keys by inlining the resolved def.
1186
+ // OpenAI strict mode forbids `{$ref, description, ...}`; the SDK resolves
1187
+ // and merges, with sibling keys taking precedence over the ref'd def.
1188
+ // Cite: openai-python/src/openai/lib/_pydantic.py:96-110 (`_ensure_strict_json_schema`)
1189
+ if (typeof schema.$ref === "string") {
1190
+ let hasSibling = false;
1191
+ for (const k in schema) {
1192
+ if (k !== "$ref" && Object.hasOwn(schema, k)) {
1193
+ hasSibling = true;
1194
+ break;
1195
+ }
1196
+ }
1197
+ if (hasSibling) {
1198
+ const resolved = resolveStrictRef(root, schema.$ref);
1199
+ if (resolved !== undefined) {
1200
+ // Sibling keys on the schema override keys from the resolved def.
1201
+ const merged: Record<string, unknown> = { ...resolved };
1202
+ for (const k in schema) {
1203
+ if (k === "$ref" || !Object.hasOwn(schema, k)) continue;
1204
+ merged[k] = schema[k];
1205
+ }
1206
+ const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1207
+ cache.set(schema, result);
1208
+ return result;
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ // Pre-pass: collapse single-element `allOf` by inlining its sole entry.
1214
+ // SDK semantics: `json_schema.update(ensured(all_of[0]))` — the inlined
1215
+ // entry's keys WIN over original sibling keys, then `allOf` is dropped.
1216
+ // Cite: openai-python/src/openai/lib/_pydantic.py:79-83
1217
+ {
1218
+ const allOf = schema.allOf;
1219
+ if (Array.isArray(allOf) && allOf.length === 1 && isJsonObject(allOf[0])) {
1220
+ const merged: Record<string, unknown> = { ...schema };
1221
+ delete merged.allOf;
1222
+ const sole = allOf[0] as Record<string, unknown>;
1223
+ for (const k in sole) {
1224
+ if (Object.hasOwn(sole, k)) merged[k] = sole[k];
1225
+ }
1226
+ const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1227
+ cache.set(schema, result);
1228
+ return result;
1229
+ }
1230
+ }
1231
+
1232
+ const typeValue = schema.type;
1233
+ if (Array.isArray(typeValue)) {
1234
+ const typeVariants = typeValue.filter((entry): entry is string => typeof entry === "string");
1235
+ const schemaWithoutType = { ...schema };
1236
+ delete schemaWithoutType.type;
1237
+
1238
+ const sanitizedWithoutType = sanitizeSchemaForStrictMode(schemaWithoutType, epoch, cache, root);
1239
+ if (typeVariants.length === 0) {
1240
+ cache.set(schema, sanitizedWithoutType);
1241
+ return sanitizedWithoutType;
1242
+ }
1243
+ // Build one variant schema per type. Each variant keeps only the keywords
1244
+ // relevant to that type — object-only keywords stay on the object variant,
1245
+ // array-only keywords on the array variant, etc.
1246
+ //
1247
+ // `description` is metadata that applies to the whole union, not to any
1248
+ // single type variant, so hoist it to the wrapper so both branches share
1249
+ // it without duplication. Matches the optional-property wrap in
1250
+ // `enforceStrictSchema` and the typical OpenAI strict-mode "description
1251
+ // on the union" shape.
1252
+ const { description, ...variantBase } = sanitizedWithoutType;
1253
+ const variants = typeVariants.map(variantType => {
1254
+ const variantSchema: Record<string, unknown> = { ...variantBase, type: variantType };
1255
+ if (variantType !== "object") {
1256
+ delete variantSchema.properties;
1257
+ delete variantSchema.required;
1258
+ delete variantSchema.additionalProperties;
1259
+ }
1260
+ if (variantType !== "array") {
1261
+ delete variantSchema.items;
1262
+ }
1263
+ return sanitizeSchemaForStrictMode(variantSchema, epoch, cache, root);
1264
+ });
1265
+
1266
+ if (variants.length === 1) {
1267
+ const sole = variants[0] as Record<string, unknown>;
1268
+ if (description !== undefined && !Object.hasOwn(sole, "description")) {
1269
+ sole.description = description;
1270
+ }
1271
+ cache.set(schema, sole);
1272
+ return sole;
1273
+ }
1274
+
1275
+ const result: JsonObject = { anyOf: variants };
1276
+ if (description !== undefined) result.description = description;
1277
+ cache.set(schema, result);
1278
+ return result;
1279
+ }
1280
+ // Scalar `type`: walk the keys, rewriting or stripping per strict-mode rules.
1281
+
1282
+ const sanitized: Record<string, unknown> = {};
1283
+ cache.set(schema, sanitized);
1284
+ for (const key in schema) {
1285
+ const value = schema[key];
1286
+ if (key in NON_STRUCTURAL_SCHEMA_KEYS || key === "type" || key === "const" || key === "nullable") {
1287
+ continue;
1288
+ }
1289
+ // `properties` map — recurse into each property schema.
1290
+
1291
+ if (key === "properties" && isJsonObject(value)) {
1292
+ const properties: Record<string, unknown> = {};
1293
+ for (const propertyName in value) {
1294
+ const propertySchema = value[propertyName];
1295
+ properties[propertyName] = isJsonObject(propertySchema)
1296
+ ? sanitizeSchemaForStrictMode(propertySchema, epoch, cache, root)
1297
+ : propertySchema;
1298
+ }
1299
+ sanitized.properties = properties;
1300
+ continue;
1301
+ }
1302
+ // `items` can be schema, tuple-array, or scalar boolean — recurse where applicable.
1303
+
1304
+ if (key === "items") {
1305
+ if (isJsonObject(value)) {
1306
+ sanitized.items = sanitizeSchemaForStrictMode(value, epoch, cache, root);
1307
+ } else if (Array.isArray(value)) {
1308
+ sanitized.items = value.map(entry =>
1309
+ isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1310
+ );
1311
+ } else {
1312
+ sanitized.items = value;
1313
+ }
1314
+ continue;
1315
+ }
1316
+ // `prefixItems` is always an array of schemas (draft 2020-12).
1317
+
1318
+ if (key === "prefixItems" && Array.isArray(value)) {
1319
+ sanitized.prefixItems = value.map(entry =>
1320
+ isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1321
+ );
1322
+ continue;
1323
+ }
1324
+ // `anyOf`/`oneOf`/`allOf` arrays — recurse into each branch.
1325
+
1326
+ 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,
1329
+ );
1330
+ continue;
1331
+ }
1332
+ // Definition maps — recurse into each named schema.
1333
+
1334
+ if ((key === "$defs" || key === "definitions") && isJsonObject(value)) {
1335
+ const defs: Record<string, unknown> = {};
1336
+ for (const definitionName in value) {
1337
+ const definitionSchema = value[definitionName];
1338
+ defs[definitionName] = isJsonObject(definitionSchema)
1339
+ ? sanitizeSchemaForStrictMode(definitionSchema, epoch, cache, root)
1340
+ : definitionSchema;
1341
+ }
1342
+ sanitized[key] = defs;
1343
+ continue;
1344
+ }
1345
+ // `additionalProperties` is owned by `enforceStrictSchema`, which sets it to false.
1346
+
1347
+ if (key === "additionalProperties") {
1348
+ continue;
1349
+ }
1350
+
1351
+ if (key === "description" && typeof value === "string" && schema.default !== undefined) {
1352
+ // Preserve `default:` info for strict-mode providers that strip the keyword.
1353
+ // Inline as `(default: X)` text in the description, matching the convention for
1354
+ // runtime-placeholder defaults (e.g. `cwd`) that cannot live in the keyword form.
1355
+ const defaultVal = schema.default;
1356
+ const formatted = typeof defaultVal === "string" ? defaultVal : JSON.stringify(defaultVal);
1357
+ sanitized.description = value.includes("(default:") ? value : `${value} (default: ${formatted})`;
1358
+ continue;
1359
+ }
1360
+
1361
+ sanitized[key] = value;
1362
+ }
1363
+ // Post-pass: re-derive `type` and turn dropped keywords into a representable shape.
1364
+
1365
+ if (Object.hasOwn(schema, "const")) {
1366
+ const constVal = schema.const;
1367
+ const existingEnum = Array.isArray(sanitized.enum) ? sanitized.enum : [];
1368
+ if (!existingEnum.some(v => areJsonValuesEqual(v, constVal))) {
1369
+ existingEnum.push(constVal);
1370
+ }
1371
+ sanitized.enum = existingEnum;
1372
+ }
1373
+
1374
+ // Preserve the original scalar type after the strip-and-rebuild loop.
1375
+ if (typeof typeValue === "string") {
1376
+ sanitized.type = typeValue;
1377
+ }
1378
+
1379
+ if (sanitized.type === undefined && isJsonObject(sanitized.properties)) {
1380
+ sanitized.type = "object";
1381
+ }
1382
+
1383
+ if (sanitized.type === undefined && (sanitized.items !== undefined || sanitized.prefixItems !== undefined)) {
1384
+ sanitized.type = "array";
1385
+ }
1386
+
1387
+ // Last-resort inference: a bare `enum`/`const` with homogeneous primitives gets a `type`.
1388
+ if (sanitized.type === undefined) {
1389
+ const inferred = inferStrictPrimitiveTypeFromEnumOrConst(sanitized);
1390
+ if (inferred !== undefined) sanitized.type = inferred;
1391
+ }
1392
+
1393
+ // `nullable: true` was stripped above — re-introduce it as an `anyOf` wrapper.
1394
+ // `description` hoists to the wrapper so both branches share it without
1395
+ // duplication — matches the optional-property wrap in `enforceStrictSchema`
1396
+ // and the typical OpenAI strict-mode "description on the union" shape.
1397
+ if (schema.nullable === true) {
1398
+ const { nullable: _, description, ...withoutNullable } = sanitized;
1399
+ const wrapper: JsonObject = { anyOf: [withoutNullable, { type: "null" }] };
1400
+ if (description !== undefined) wrapper.description = description;
1401
+ return wrapper;
1402
+ }
1403
+
1404
+ return sanitized;
1405
+ }
1406
+
1407
+ /**
1408
+ * Recursively enforces JSON Schema constraints required by OpenAI/OpenAI code backend strict mode:
1409
+ * - `additionalProperties: false` on every object node
1410
+ * - every key in `properties` present in `required`
1411
+ *
1412
+ * Properties absent from the original `required` array were TypeBox-optional.
1413
+ * They are made nullable (`anyOf: [T, { type: "null" }]`) so the model can
1414
+ * signal omission by outputting null rather than omitting the key entirely.
1415
+ *
1416
+ * @throws {Error} When a schema node has no `type`, array-based combinator
1417
+ * (`anyOf`/`allOf`/`oneOf`), object-based combinator (`not`), or `$ref` —
1418
+ * i.e. the node is not representable in strict mode. Prefer
1419
+ * {@link tryEnforceStrictSchema} which catches this and degrades gracefully.
1420
+ */
1421
+ export function enforceStrictSchema(
1422
+ schema: Record<string, unknown>,
1423
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
1424
+ ): Record<string, unknown> {
1425
+ if (!enter(schema)) {
1426
+ throw new Error("Schema contains a circular object graph — cannot enforce strict mode");
1427
+ }
1428
+ try {
1429
+ const cached = cache.get(schema);
1430
+ if (cached) return cached;
1431
+ const result = { ...schema };
1432
+ cache.set(schema, result);
1433
+ return enforceStrictSchemaBody(schema, result, cache);
1434
+ } finally {
1435
+ exit(schema);
1436
+ }
1437
+ }
1438
+
1439
+ function enforceStrictSchemaBody(
1440
+ _schema: Record<string, unknown>,
1441
+ result: Record<string, unknown>,
1442
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
1443
+ ): Record<string, unknown> {
1444
+ const isObjectType = result.type === "object";
1445
+ if (isObjectType) {
1446
+ result.additionalProperties = false;
1447
+ const propertiesValue = result.properties;
1448
+ const props =
1449
+ propertiesValue != null && typeof propertiesValue === "object" && !Array.isArray(propertiesValue)
1450
+ ? (propertiesValue as Record<string, unknown>)
1451
+ : {};
1452
+ const originalRequired = new Set<string>(
1453
+ Array.isArray(result.required)
1454
+ ? result.required.filter((value): value is string => typeof value === "string")
1455
+ : [],
1456
+ );
1457
+ const strictProperties: Record<string, unknown> = {};
1458
+ for (const key in props) {
1459
+ const value = props[key];
1460
+ const processed =
1461
+ value != null && typeof value === "object" && !Array.isArray(value)
1462
+ ? enforceStrictSchema(value as Record<string, unknown>, cache)
1463
+ : value;
1464
+ // Optional property — wrap as nullable so strict mode accepts it
1465
+ if (!originalRequired.has(key)) {
1466
+ // Don't double-wrap if already nullable
1467
+ if (
1468
+ isJsonObject(processed) &&
1469
+ Array.isArray(processed.anyOf) &&
1470
+ processed.anyOf.some(v => isJsonObject(v) && v.type === "null")
1471
+ ) {
1472
+ strictProperties[key] = processed;
1473
+ continue;
1474
+ }
1475
+ if (isJsonObject(processed) && typeof processed.description === "string") {
1476
+ const { description, ...withoutDescription } = processed;
1477
+ strictProperties[key] = { anyOf: [withoutDescription, { type: "null" }], description };
1478
+ continue;
1479
+ }
1480
+ strictProperties[key] = { anyOf: [processed, { type: "null" }] };
1481
+ continue;
1482
+ }
1483
+ strictProperties[key] = processed;
1484
+ }
1485
+ result.properties = strictProperties;
1486
+ result.required = Object.keys(strictProperties);
1487
+ }
1488
+ if (result.items != null && typeof result.items === "object") {
1489
+ if (Array.isArray(result.items)) {
1490
+ result.items = result.items.map(entry =>
1491
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1492
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1493
+ : entry,
1494
+ );
1495
+ } else {
1496
+ result.items = enforceStrictSchema(result.items as Record<string, unknown>, cache);
1497
+ }
1498
+ }
1499
+ if (Array.isArray(result.prefixItems)) {
1500
+ result.prefixItems = result.prefixItems.map(entry =>
1501
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1502
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1503
+ : entry,
1504
+ );
1505
+ }
1506
+ 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,
1512
+ );
1513
+ }
1514
+ }
1515
+ for (const defsKey of ["$defs", "definitions"] as const) {
1516
+ if (result[defsKey] != null && typeof result[defsKey] === "object" && !Array.isArray(result[defsKey])) {
1517
+ const defs = result[defsKey] as Record<string, unknown>;
1518
+ const nextDefs: Record<string, unknown> = {};
1519
+ for (const name in defs) {
1520
+ const def = defs[name];
1521
+ nextDefs[name] =
1522
+ def != null && typeof def === "object" && !Array.isArray(def)
1523
+ ? enforceStrictSchema(def as Record<string, unknown>, cache)
1524
+ : def;
1525
+ }
1526
+ result[defsKey] = nextDefs;
1527
+ }
1528
+ }
1529
+ // Strict mode requires every schema node to declare a concrete type (or
1530
+ // combinator / `$ref` / `not`). When `type` is missing, try to infer it
1531
+ // from a homogeneous-primitive `enum` / `const` so direct calls to
1532
+ // `enforceStrictSchema` (which bypass `sanitizeSchemaForStrictMode`'s own
1533
+ // inference pass) still produce wire-valid output.
1534
+ if (result.type === undefined) {
1535
+ const inferred = inferStrictPrimitiveTypeFromEnumOrConst(result);
1536
+ if (inferred !== undefined) result.type = inferred;
1537
+ }
1538
+ // Schemas like `{}`, `{items: {}}`, mixed-primitive enums, and non-primitive
1539
+ // consts are not representable in strict mode — `enum`/`const` are not
1540
+ // accepted as type substitutes here because they did not yield a single
1541
+ // inferable type above.
1542
+ if (
1543
+ result.type === undefined &&
1544
+ result.$ref === undefined &&
1545
+ !COMBINATOR_KEYS.some(key => Array.isArray(result[key])) &&
1546
+ !isJsonObject(result.not)
1547
+ ) {
1548
+ throw new Error("Schema node has no type, combinator, or $ref — cannot enforce strict mode");
1549
+ }
1550
+ return result;
1551
+ }
1552
+
1553
+ export function tryEnforceStrictSchema(schema: Record<string, unknown>): {
1554
+ schema: Record<string, unknown>;
1555
+ strict: boolean;
1556
+ } {
1557
+ return stamp(schema, kStrictSchema, s => {
1558
+ const upgraded = upgradeJsonSchemaTo202012(s) as Record<string, unknown>;
1559
+ if (hasUnrepresentableStrictObjectMap(upgraded)) {
1560
+ return { schema: upgraded, strict: false };
1561
+ }
1562
+ try {
1563
+ const sanitized = sanitizeSchemaForStrictMode(upgraded);
1564
+ return { schema: enforceStrictSchema(sanitized), strict: true };
1565
+ } catch {
1566
+ return { schema: upgraded, strict: false };
1567
+ }
1568
+ });
1569
+ }
1570
+
1571
+ /**
1572
+ * Resolve a JSON-pointer-style `$ref` against the root schema. Mirrors the
1573
+ * OpenAI SDK's `resolve_ref` helper: only local refs starting with `#/` are
1574
+ * supported, and each segment must dereference to a dictionary.
1575
+ * Cite: openai-python/src/openai/lib/_pydantic.py:118-129
1576
+ */
1577
+ function resolveStrictRef(root: Record<string, unknown>, ref: string): Record<string, unknown> | undefined {
1578
+ if (!ref.startsWith("#/")) return undefined;
1579
+ const segments = ref.slice(2).split("/");
1580
+ let cursor: unknown = root;
1581
+ for (const raw of segments) {
1582
+ if (!isJsonObject(cursor)) return undefined;
1583
+ // JSON Pointer unescape: ~1 → "/", ~0 → "~" (must run in that order).
1584
+ const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
1585
+ cursor = cursor[segment];
1586
+ }
1587
+ return isJsonObject(cursor) ? cursor : undefined;
1588
+ }