jeopi-ai 16.2.13

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 (598) hide show
  1. package/CHANGELOG.md +4347 -0
  2. package/README.md +1193 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth-broker/client.d.ts +73 -0
  5. package/dist/types/auth-broker/discover.d.ts +35 -0
  6. package/dist/types/auth-broker/index.d.ts +7 -0
  7. package/dist/types/auth-broker/refresher.d.ts +25 -0
  8. package/dist/types/auth-broker/remote-store.d.ts +102 -0
  9. package/dist/types/auth-broker/server.d.ts +43 -0
  10. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  11. package/dist/types/auth-broker/types.d.ts +107 -0
  12. package/dist/types/auth-broker/wire-schemas.d.ts +411 -0
  13. package/dist/types/auth-gateway/http.d.ts +39 -0
  14. package/dist/types/auth-gateway/index.d.ts +3 -0
  15. package/dist/types/auth-gateway/server.d.ts +36 -0
  16. package/dist/types/auth-gateway/types.d.ts +123 -0
  17. package/dist/types/auth-retry.d.ts +124 -0
  18. package/dist/types/auth-storage.d.ts +1026 -0
  19. package/dist/types/dialect/anthropic.d.ts +15 -0
  20. package/dist/types/dialect/catalog.d.ts +3 -0
  21. package/dist/types/dialect/coercion.d.ts +23 -0
  22. package/dist/types/dialect/deepseek.d.ts +14 -0
  23. package/dist/types/dialect/demotion.d.ts +23 -0
  24. package/dist/types/dialect/examples.d.ts +2 -0
  25. package/dist/types/dialect/factory.d.ts +3 -0
  26. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  27. package/dist/types/dialect/gemini.d.ts +17 -0
  28. package/dist/types/dialect/gemma.d.ts +15 -0
  29. package/dist/types/dialect/glm.d.ts +9 -0
  30. package/dist/types/dialect/harmony.d.ts +8 -0
  31. package/dist/types/dialect/hermes.d.ts +9 -0
  32. package/dist/types/dialect/history.d.ts +3 -0
  33. package/dist/types/dialect/index.d.ts +11 -0
  34. package/dist/types/dialect/inventory.d.ts +12 -0
  35. package/dist/types/dialect/kimi.d.ts +14 -0
  36. package/dist/types/dialect/minimax.d.ts +3 -0
  37. package/dist/types/dialect/owned-stream.d.ts +4 -0
  38. package/dist/types/dialect/qwen3.d.ts +9 -0
  39. package/dist/types/dialect/rendering.d.ts +45 -0
  40. package/dist/types/dialect/thinking.d.ts +6 -0
  41. package/dist/types/dialect/types.d.ts +69 -0
  42. package/dist/types/dialect/xml.d.ts +9 -0
  43. package/dist/types/error/abort.d.ts +14 -0
  44. package/dist/types/error/auth-classify.d.ts +16 -0
  45. package/dist/types/error/auth.d.ts +27 -0
  46. package/dist/types/error/aws.d.ts +23 -0
  47. package/dist/types/error/classes.d.ts +102 -0
  48. package/dist/types/error/finalize.d.ts +39 -0
  49. package/dist/types/error/flags.d.ts +79 -0
  50. package/dist/types/error/format.d.ts +20 -0
  51. package/dist/types/error/gateway.d.ts +20 -0
  52. package/dist/types/error/index.d.ts +13 -0
  53. package/dist/types/error/oauth.d.ts +43 -0
  54. package/dist/types/error/provider.d.ts +42 -0
  55. package/dist/types/error/rate-limit.d.ts +59 -0
  56. package/dist/types/error/retryable.d.ts +27 -0
  57. package/dist/types/error/validation.d.ts +32 -0
  58. package/dist/types/index.d.ts +49 -0
  59. package/dist/types/provider-details.d.ts +24 -0
  60. package/dist/types/providers/__tests__/google-auth.test.d.ts +1 -0
  61. package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
  62. package/dist/types/providers/__tests__/openai-codex-error.test.d.ts +1 -0
  63. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  64. package/dist/types/providers/anthropic-client.d.ts +94 -0
  65. package/dist/types/providers/anthropic-messages-server-schema.d.ts +497 -0
  66. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  67. package/dist/types/providers/anthropic-wire.d.ts +318 -0
  68. package/dist/types/providers/anthropic.d.ts +248 -0
  69. package/dist/types/providers/aws-credentials.d.ts +53 -0
  70. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  71. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  72. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  73. package/dist/types/providers/cursor.d.ts +91 -0
  74. package/dist/types/providers/devin.d.ts +12 -0
  75. package/dist/types/providers/error-message.d.ts +27 -0
  76. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  77. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  78. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  79. package/dist/types/providers/google-auth.d.ts +32 -0
  80. package/dist/types/providers/google-gemini-cli.d.ts +118 -0
  81. package/dist/types/providers/google-interactions.d.ts +65 -0
  82. package/dist/types/providers/google-shared.d.ts +203 -0
  83. package/dist/types/providers/google-types.d.ts +155 -0
  84. package/dist/types/providers/google-vertex.d.ts +7 -0
  85. package/dist/types/providers/google.d.ts +4 -0
  86. package/dist/types/providers/grammar.d.ts +1 -0
  87. package/dist/types/providers/kimi.d.ts +27 -0
  88. package/dist/types/providers/mock.d.ts +178 -0
  89. package/dist/types/providers/ollama.d.ts +7 -0
  90. package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
  91. package/dist/types/providers/openai-chat-server-schema.d.ts +695 -0
  92. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  93. package/dist/types/providers/openai-chat-wire.d.ts +644 -0
  94. package/dist/types/providers/openai-codex/request-transformer.d.ts +54 -0
  95. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  96. package/dist/types/providers/openai-codex-responses.d.ts +108 -0
  97. package/dist/types/providers/openai-completions.d.ts +45 -0
  98. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  99. package/dist/types/providers/openai-responses-server-schema.d.ts +349 -0
  100. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  101. package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
  102. package/dist/types/providers/openai-responses.d.ts +126 -0
  103. package/dist/types/providers/openai-shared.d.ts +506 -0
  104. package/dist/types/providers/pi-native-client.d.ts +13 -0
  105. package/dist/types/providers/pi-native-server.d.ts +69 -0
  106. package/dist/types/providers/register-builtins.d.ts +32 -0
  107. package/dist/types/providers/synthetic.d.ts +26 -0
  108. package/dist/types/providers/transform-messages.d.ts +11 -0
  109. package/dist/types/providers/vision-guard.d.ts +20 -0
  110. package/dist/types/registry/aimlapi.d.ts +4 -0
  111. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  112. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  113. package/dist/types/registry/anthropic.d.ts +10 -0
  114. package/dist/types/registry/api-key-login.d.ts +42 -0
  115. package/dist/types/registry/api-key-validation.d.ts +43 -0
  116. package/dist/types/registry/azure.d.ts +4 -0
  117. package/dist/types/registry/cerebras.d.ts +7 -0
  118. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  119. package/dist/types/registry/coreweave.d.ts +7 -0
  120. package/dist/types/registry/cursor.d.ts +7 -0
  121. package/dist/types/registry/deepseek.d.ts +8 -0
  122. package/dist/types/registry/derived.d.ts +5 -0
  123. package/dist/types/registry/devin.d.ts +8 -0
  124. package/dist/types/registry/firepass.d.ts +16 -0
  125. package/dist/types/registry/fireworks.d.ts +7 -0
  126. package/dist/types/registry/github-copilot.d.ts +7 -0
  127. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  128. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  129. package/dist/types/registry/google-antigravity.d.ts +9 -0
  130. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  131. package/dist/types/registry/google-vertex.d.ts +5 -0
  132. package/dist/types/registry/google.d.ts +4 -0
  133. package/dist/types/registry/groq.d.ts +4 -0
  134. package/dist/types/registry/huggingface.d.ts +7 -0
  135. package/dist/types/registry/index.d.ts +4 -0
  136. package/dist/types/registry/kagi.d.ts +14 -0
  137. package/dist/types/registry/kilo.d.ts +7 -0
  138. package/dist/types/registry/kimi-code.d.ts +7 -0
  139. package/dist/types/registry/litellm.d.ts +13 -0
  140. package/dist/types/registry/llama-cpp.d.ts +8 -0
  141. package/dist/types/registry/lm-studio.d.ts +8 -0
  142. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  143. package/dist/types/registry/minimax-code.d.ts +6 -0
  144. package/dist/types/registry/minimax.d.ts +4 -0
  145. package/dist/types/registry/mistral.d.ts +4 -0
  146. package/dist/types/registry/moonshot.d.ts +7 -0
  147. package/dist/types/registry/nanogpt.d.ts +7 -0
  148. package/dist/types/registry/nvidia.d.ts +7 -0
  149. package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
  150. package/dist/types/registry/oauth/anthropic.d.ts +23 -0
  151. package/dist/types/registry/oauth/callback-server.d.ts +72 -0
  152. package/dist/types/registry/oauth/cursor.d.ts +15 -0
  153. package/dist/types/registry/oauth/devin.d.ts +5 -0
  154. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  155. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  156. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  157. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  158. package/dist/types/registry/oauth/google-gemini-cli.d.ts +10 -0
  159. package/dist/types/registry/oauth/google-oauth-shared.d.ts +22 -0
  160. package/dist/types/registry/oauth/index.d.ts +64 -0
  161. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  162. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  163. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  164. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  165. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  166. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  167. package/dist/types/registry/oauth/types.d.ts +56 -0
  168. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  169. package/dist/types/registry/oauth/xai-oauth.d.ts +52 -0
  170. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  171. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  172. package/dist/types/registry/ollama.d.ts +12 -0
  173. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  174. package/dist/types/registry/openai-codex.d.ts +9 -0
  175. package/dist/types/registry/openai.d.ts +4 -0
  176. package/dist/types/registry/opencode-go.d.ts +6 -0
  177. package/dist/types/registry/opencode-zen.d.ts +6 -0
  178. package/dist/types/registry/openrouter.d.ts +13 -0
  179. package/dist/types/registry/parallel.d.ts +14 -0
  180. package/dist/types/registry/perplexity.d.ts +7 -0
  181. package/dist/types/registry/qianfan.d.ts +7 -0
  182. package/dist/types/registry/qwen-portal.d.ts +7 -0
  183. package/dist/types/registry/registry.d.ts +303 -0
  184. package/dist/types/registry/sakana.d.ts +7 -0
  185. package/dist/types/registry/synthetic.d.ts +6 -0
  186. package/dist/types/registry/tavily.d.ts +14 -0
  187. package/dist/types/registry/together.d.ts +6 -0
  188. package/dist/types/registry/types.d.ts +51 -0
  189. package/dist/types/registry/umans.d.ts +7 -0
  190. package/dist/types/registry/venice.d.ts +13 -0
  191. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  192. package/dist/types/registry/vllm.d.ts +7 -0
  193. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  194. package/dist/types/registry/xai-oauth.d.ts +7 -0
  195. package/dist/types/registry/xai.d.ts +4 -0
  196. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  197. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  198. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  199. package/dist/types/registry/xiaomi.d.ts +6 -0
  200. package/dist/types/registry/zai.d.ts +7 -0
  201. package/dist/types/registry/zenmux.d.ts +7 -0
  202. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  203. package/dist/types/stream.d.ts +44 -0
  204. package/dist/types/types.d.ts +715 -0
  205. package/dist/types/usage/claude.d.ts +4 -0
  206. package/dist/types/usage/gemini.d.ts +2 -0
  207. package/dist/types/usage/github-copilot.d.ts +7 -0
  208. package/dist/types/usage/google-antigravity.d.ts +15 -0
  209. package/dist/types/usage/kimi.d.ts +2 -0
  210. package/dist/types/usage/minimax-code.d.ts +2 -0
  211. package/dist/types/usage/ollama.d.ts +5 -0
  212. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  213. package/dist/types/usage/openai-codex-reset.d.ts +79 -0
  214. package/dist/types/usage/openai-codex.d.ts +3 -0
  215. package/dist/types/usage/opencode-go.d.ts +2 -0
  216. package/dist/types/usage/shared.d.ts +1 -0
  217. package/dist/types/usage/zai.d.ts +2 -0
  218. package/dist/types/usage.d.ts +346 -0
  219. package/dist/types/utils/abort.d.ts +25 -0
  220. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  221. package/dist/types/utils/block-symbols.d.ts +20 -0
  222. package/dist/types/utils/deterministic-id.d.ts +16 -0
  223. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  224. package/dist/types/utils/event-stream.d.ts +30 -0
  225. package/dist/types/utils/foundry.d.ts +1 -0
  226. package/dist/types/utils/google-validation.d.ts +2 -0
  227. package/dist/types/utils/harmony-leak.d.ts +118 -0
  228. package/dist/types/utils/http-inspector.d.ts +30 -0
  229. package/dist/types/utils/idle-iterator.d.ts +137 -0
  230. package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
  231. package/dist/types/utils/openai-http.d.ts +54 -0
  232. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  233. package/dist/types/utils/parse-bind.d.ts +23 -0
  234. package/dist/types/utils/provider-response.d.ts +3 -0
  235. package/dist/types/utils/proxy.d.ts +29 -0
  236. package/dist/types/utils/request-debug.d.ts +29 -0
  237. package/dist/types/utils/retry-after.d.ts +4 -0
  238. package/dist/types/utils/retry.d.ts +14 -0
  239. package/dist/types/utils/schema/adapt.d.ts +24 -0
  240. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  241. package/dist/types/utils/schema/dereference.d.ts +11 -0
  242. package/dist/types/utils/schema/draft.d.ts +10 -0
  243. package/dist/types/utils/schema/equality.d.ts +4 -0
  244. package/dist/types/utils/schema/fields.d.ts +54 -0
  245. package/dist/types/utils/schema/index.d.ts +15 -0
  246. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  247. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  248. package/dist/types/utils/schema/normalize.d.ts +124 -0
  249. package/dist/types/utils/schema/spill.d.ts +8 -0
  250. package/dist/types/utils/schema/stamps.d.ts +17 -0
  251. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  252. package/dist/types/utils/schema/types.d.ts +4 -0
  253. package/dist/types/utils/schema/typescript.d.ts +18 -0
  254. package/dist/types/utils/schema/wire.d.ts +92 -0
  255. package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
  256. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  257. package/dist/types/utils/sse-debug.d.ts +5 -0
  258. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  259. package/dist/types/utils/thinking-loop.d.ts +102 -0
  260. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  261. package/dist/types/utils/tool-choice.d.ts +50 -0
  262. package/dist/types/utils/validation.d.ts +42 -0
  263. package/dist/types/utils.d.ts +24 -0
  264. package/package.json +139 -0
  265. package/src/api-registry.ts +109 -0
  266. package/src/auth-broker/client.ts +359 -0
  267. package/src/auth-broker/discover.ts +222 -0
  268. package/src/auth-broker/index.ts +7 -0
  269. package/src/auth-broker/refresher.ts +117 -0
  270. package/src/auth-broker/remote-store.ts +657 -0
  271. package/src/auth-broker/server.ts +646 -0
  272. package/src/auth-broker/snapshot-cache.ts +191 -0
  273. package/src/auth-broker/types.ts +130 -0
  274. package/src/auth-broker/wire-schemas.ts +249 -0
  275. package/src/auth-gateway/http.ts +194 -0
  276. package/src/auth-gateway/index.ts +3 -0
  277. package/src/auth-gateway/server.ts +802 -0
  278. package/src/auth-gateway/types.ts +151 -0
  279. package/src/auth-retry.ts +250 -0
  280. package/src/auth-storage.ts +5576 -0
  281. package/src/dialect/anthropic.md +31 -0
  282. package/src/dialect/anthropic.ts +608 -0
  283. package/src/dialect/catalog.ts +29 -0
  284. package/src/dialect/coercion.ts +136 -0
  285. package/src/dialect/deepseek.md +24 -0
  286. package/src/dialect/deepseek.ts +609 -0
  287. package/src/dialect/demotion.ts +36 -0
  288. package/src/dialect/examples.ts +33 -0
  289. package/src/dialect/factory.ts +34 -0
  290. package/src/dialect/fenced-thinking.ts +184 -0
  291. package/src/dialect/gemini.md +44 -0
  292. package/src/dialect/gemini.ts +597 -0
  293. package/src/dialect/gemma.md +33 -0
  294. package/src/dialect/gemma.ts +387 -0
  295. package/src/dialect/glm.md +32 -0
  296. package/src/dialect/glm.ts +456 -0
  297. package/src/dialect/harmony.md +31 -0
  298. package/src/dialect/harmony.ts +346 -0
  299. package/src/dialect/hermes.md +25 -0
  300. package/src/dialect/hermes.ts +206 -0
  301. package/src/dialect/history.ts +81 -0
  302. package/src/dialect/index.ts +15 -0
  303. package/src/dialect/inventory.ts +73 -0
  304. package/src/dialect/kimi.md +24 -0
  305. package/src/dialect/kimi.ts +340 -0
  306. package/src/dialect/minimax.md +31 -0
  307. package/src/dialect/minimax.ts +95 -0
  308. package/src/dialect/owned-stream.ts +470 -0
  309. package/src/dialect/prompt-template.md +12 -0
  310. package/src/dialect/qwen3.md +28 -0
  311. package/src/dialect/qwen3.ts +240 -0
  312. package/src/dialect/rendering.ts +249 -0
  313. package/src/dialect/thinking.ts +122 -0
  314. package/src/dialect/types.ts +57 -0
  315. package/src/dialect/xml.md +22 -0
  316. package/src/dialect/xml.ts +90 -0
  317. package/src/error/abort.ts +18 -0
  318. package/src/error/auth-classify.ts +30 -0
  319. package/src/error/auth.ts +48 -0
  320. package/src/error/aws.ts +31 -0
  321. package/src/error/classes.ts +186 -0
  322. package/src/error/finalize.ts +69 -0
  323. package/src/error/flags.ts +506 -0
  324. package/src/error/format.ts +45 -0
  325. package/src/error/gateway.ts +96 -0
  326. package/src/error/index.ts +13 -0
  327. package/src/error/oauth.ts +58 -0
  328. package/src/error/provider.ts +62 -0
  329. package/src/error/rate-limit.ts +161 -0
  330. package/src/error/retryable.ts +70 -0
  331. package/src/error/validation.ts +44 -0
  332. package/src/index.ts +49 -0
  333. package/src/provider-details.ts +90 -0
  334. package/src/providers/__tests__/google-auth.test.ts +144 -0
  335. package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
  336. package/src/providers/__tests__/openai-codex-error.test.ts +84 -0
  337. package/src/providers/amazon-bedrock.ts +1042 -0
  338. package/src/providers/anthropic-client.ts +295 -0
  339. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  340. package/src/providers/anthropic-messages-server.ts +756 -0
  341. package/src/providers/anthropic-wire.ts +318 -0
  342. package/src/providers/anthropic.ts +4078 -0
  343. package/src/providers/aws-credentials.ts +586 -0
  344. package/src/providers/aws-eventstream.ts +181 -0
  345. package/src/providers/aws-sigv4.ts +218 -0
  346. package/src/providers/azure-openai-responses.ts +382 -0
  347. package/src/providers/cursor/proto/agent.proto +3526 -0
  348. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  349. package/src/providers/cursor/proto/buf.yaml +17 -0
  350. package/src/providers/cursor.ts +2695 -0
  351. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  352. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  353. package/src/providers/devin/proto/buf.yaml +17 -0
  354. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  355. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  356. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  357. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  358. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  359. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  360. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  361. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  362. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  363. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  364. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  365. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  366. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  367. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  368. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  369. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  370. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  371. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  372. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  373. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  374. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  375. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  376. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  377. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  378. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  379. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  380. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  381. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  382. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  383. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  384. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  385. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  386. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  387. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  388. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  389. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  390. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  391. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  392. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  393. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  394. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  395. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  396. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  397. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  398. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  399. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  400. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  401. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  402. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  403. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  404. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  405. package/src/providers/devin.ts +577 -0
  406. package/src/providers/error-message.ts +21 -0
  407. package/src/providers/github-copilot-headers.ts +141 -0
  408. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  409. package/src/providers/gitlab-duo-workflow.ts +3058 -0
  410. package/src/providers/gitlab-duo.ts +395 -0
  411. package/src/providers/google-auth.ts +350 -0
  412. package/src/providers/google-gemini-cli.ts +1362 -0
  413. package/src/providers/google-interactions.ts +753 -0
  414. package/src/providers/google-shared.ts +1103 -0
  415. package/src/providers/google-types.ts +180 -0
  416. package/src/providers/google-vertex.ts +183 -0
  417. package/src/providers/google.ts +87 -0
  418. package/src/providers/grammar.ts +70 -0
  419. package/src/providers/kimi.ts +52 -0
  420. package/src/providers/mock.ts +507 -0
  421. package/src/providers/ollama.ts +773 -0
  422. package/src/providers/openai-anthropic-shim.ts +152 -0
  423. package/src/providers/openai-chat-server-schema.ts +242 -0
  424. package/src/providers/openai-chat-server.ts +715 -0
  425. package/src/providers/openai-chat-wire.ts +847 -0
  426. package/src/providers/openai-codex/request-transformer.ts +295 -0
  427. package/src/providers/openai-codex/response-handler.ts +102 -0
  428. package/src/providers/openai-codex-responses.ts +3468 -0
  429. package/src/providers/openai-completions.ts +2173 -0
  430. package/src/providers/openai-reasoning-fallback.ts +269 -0
  431. package/src/providers/openai-responses-reasoning-suppression.md +1 -0
  432. package/src/providers/openai-responses-server-schema.ts +282 -0
  433. package/src/providers/openai-responses-server.ts +1280 -0
  434. package/src/providers/openai-responses-wire.ts +6391 -0
  435. package/src/providers/openai-responses.ts +1022 -0
  436. package/src/providers/openai-shared.ts +2648 -0
  437. package/src/providers/pi-native-client.ts +266 -0
  438. package/src/providers/pi-native-server.ts +242 -0
  439. package/src/providers/register-builtins.ts +475 -0
  440. package/src/providers/synthetic.ts +50 -0
  441. package/src/providers/transform-messages.ts +787 -0
  442. package/src/providers/vision-guard.ts +54 -0
  443. package/src/registry/aimlapi.ts +6 -0
  444. package/src/registry/alibaba-coding-plan.ts +95 -0
  445. package/src/registry/amazon-bedrock.ts +22 -0
  446. package/src/registry/anthropic.ts +26 -0
  447. package/src/registry/api-key-login.ts +112 -0
  448. package/src/registry/api-key-validation.ts +161 -0
  449. package/src/registry/azure.ts +6 -0
  450. package/src/registry/cerebras.ts +23 -0
  451. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  452. package/src/registry/coreweave.ts +40 -0
  453. package/src/registry/cursor.ts +20 -0
  454. package/src/registry/deepseek.ts +46 -0
  455. package/src/registry/derived.ts +9 -0
  456. package/src/registry/devin.ts +15 -0
  457. package/src/registry/firepass.ts +32 -0
  458. package/src/registry/fireworks.ts +28 -0
  459. package/src/registry/github-copilot.ts +22 -0
  460. package/src/registry/gitlab-duo-workflow.ts +20 -0
  461. package/src/registry/gitlab-duo.ts +19 -0
  462. package/src/registry/google-antigravity.ts +22 -0
  463. package/src/registry/google-gemini-cli.ts +22 -0
  464. package/src/registry/google-vertex.ts +38 -0
  465. package/src/registry/google.ts +6 -0
  466. package/src/registry/groq.ts +6 -0
  467. package/src/registry/huggingface.ts +29 -0
  468. package/src/registry/index.ts +4 -0
  469. package/src/registry/kagi.ts +46 -0
  470. package/src/registry/kilo.ts +114 -0
  471. package/src/registry/kimi-code.ts +17 -0
  472. package/src/registry/litellm.ts +45 -0
  473. package/src/registry/llama-cpp.ts +35 -0
  474. package/src/registry/lm-studio.ts +31 -0
  475. package/src/registry/minimax-code-cn.ts +12 -0
  476. package/src/registry/minimax-code.ts +12 -0
  477. package/src/registry/minimax.ts +6 -0
  478. package/src/registry/mistral.ts +6 -0
  479. package/src/registry/moonshot.ts +22 -0
  480. package/src/registry/nanogpt.ts +22 -0
  481. package/src/registry/nvidia.ts +61 -0
  482. package/src/registry/oauth/__tests__/xai-oauth.test.ts +104 -0
  483. package/src/registry/oauth/anthropic.ts +311 -0
  484. package/src/registry/oauth/callback-server.ts +315 -0
  485. package/src/registry/oauth/cursor.ts +171 -0
  486. package/src/registry/oauth/devin.ts +124 -0
  487. package/src/registry/oauth/github-copilot.ts +369 -0
  488. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  489. package/src/registry/oauth/gitlab-duo.ts +222 -0
  490. package/src/registry/oauth/google-antigravity.ts +209 -0
  491. package/src/registry/oauth/google-gemini-cli.ts +273 -0
  492. package/src/registry/oauth/google-oauth-shared.ts +125 -0
  493. package/src/registry/oauth/index.ts +269 -0
  494. package/src/registry/oauth/kimi.ts +289 -0
  495. package/src/registry/oauth/minimax-code.ts +53 -0
  496. package/src/registry/oauth/oauth.html +311 -0
  497. package/src/registry/oauth/openai-codex.ts +364 -0
  498. package/src/registry/oauth/opencode.ts +50 -0
  499. package/src/registry/oauth/perplexity.ts +228 -0
  500. package/src/registry/oauth/pkce.ts +18 -0
  501. package/src/registry/oauth/types.ts +65 -0
  502. package/src/registry/oauth/wafer.ts +24 -0
  503. package/src/registry/oauth/xai-oauth.ts +394 -0
  504. package/src/registry/oauth/xiaomi.ts +211 -0
  505. package/src/registry/ollama-cloud.ts +36 -0
  506. package/src/registry/ollama.ts +43 -0
  507. package/src/registry/openai-codex-device.ts +18 -0
  508. package/src/registry/openai-codex.ts +19 -0
  509. package/src/registry/openai.ts +6 -0
  510. package/src/registry/opencode-go.ts +12 -0
  511. package/src/registry/opencode-zen.ts +12 -0
  512. package/src/registry/openrouter.ts +28 -0
  513. package/src/registry/parallel.ts +45 -0
  514. package/src/registry/perplexity.ts +13 -0
  515. package/src/registry/qianfan.ts +27 -0
  516. package/src/registry/qwen-portal.ts +50 -0
  517. package/src/registry/registry.ts +161 -0
  518. package/src/registry/sakana.ts +22 -0
  519. package/src/registry/synthetic.ts +21 -0
  520. package/src/registry/tavily.ts +45 -0
  521. package/src/registry/together.ts +22 -0
  522. package/src/registry/types.ts +56 -0
  523. package/src/registry/umans.ts +23 -0
  524. package/src/registry/venice.ts +33 -0
  525. package/src/registry/vercel-ai-gateway.ts +38 -0
  526. package/src/registry/vllm.ts +34 -0
  527. package/src/registry/wafer-serverless.ts +12 -0
  528. package/src/registry/xai-oauth.ts +17 -0
  529. package/src/registry/xai.ts +6 -0
  530. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  531. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  532. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  533. package/src/registry/xiaomi.ts +12 -0
  534. package/src/registry/zai.ts +27 -0
  535. package/src/registry/zenmux.ts +22 -0
  536. package/src/registry/zhipu-coding-plan.ts +27 -0
  537. package/src/stream.ts +1778 -0
  538. package/src/types.ts +856 -0
  539. package/src/usage/claude.ts +485 -0
  540. package/src/usage/gemini.ts +258 -0
  541. package/src/usage/github-copilot.ts +424 -0
  542. package/src/usage/google-antigravity.ts +497 -0
  543. package/src/usage/kimi.ts +271 -0
  544. package/src/usage/minimax-code.ts +30 -0
  545. package/src/usage/ollama.ts +41 -0
  546. package/src/usage/openai-codex-base-url.ts +35 -0
  547. package/src/usage/openai-codex-reset.ts +174 -0
  548. package/src/usage/openai-codex.ts +535 -0
  549. package/src/usage/opencode-go.ts +89 -0
  550. package/src/usage/shared.ts +10 -0
  551. package/src/usage/zai.ts +321 -0
  552. package/src/usage.ts +333 -0
  553. package/src/utils/abort.ts +67 -0
  554. package/src/utils/anthropic-auth.ts +93 -0
  555. package/src/utils/block-symbols.ts +32 -0
  556. package/src/utils/deterministic-id.ts +20 -0
  557. package/src/utils/empty-completion-retry.ts +159 -0
  558. package/src/utils/event-stream.ts +171 -0
  559. package/src/utils/foundry.ts +8 -0
  560. package/src/utils/google-validation.ts +25 -0
  561. package/src/utils/harmony-leak.ts +456 -0
  562. package/src/utils/http-inspector.ts +168 -0
  563. package/src/utils/idle-iterator.ts +473 -0
  564. package/src/utils/leaked-thinking-stream.ts +294 -0
  565. package/src/utils/openai-http.ts +122 -0
  566. package/src/utils/openrouter-headers.ts +12 -0
  567. package/src/utils/parse-bind.ts +56 -0
  568. package/src/utils/provider-response.ts +30 -0
  569. package/src/utils/proxy.ts +240 -0
  570. package/src/utils/request-debug.ts +351 -0
  571. package/src/utils/retry-after.ts +110 -0
  572. package/src/utils/retry.ts +59 -0
  573. package/src/utils/schema/CONSTRAINTS.md +166 -0
  574. package/src/utils/schema/adapt.ts +36 -0
  575. package/src/utils/schema/compatibility.ts +435 -0
  576. package/src/utils/schema/dereference.ts +98 -0
  577. package/src/utils/schema/draft.ts +341 -0
  578. package/src/utils/schema/equality.ts +97 -0
  579. package/src/utils/schema/fields.ts +207 -0
  580. package/src/utils/schema/index.ts +15 -0
  581. package/src/utils/schema/json-schema-validator.ts +595 -0
  582. package/src/utils/schema/meta-validator.ts +167 -0
  583. package/src/utils/schema/normalize.ts +1901 -0
  584. package/src/utils/schema/spill.ts +43 -0
  585. package/src/utils/schema/stamps.ts +109 -0
  586. package/src/utils/schema/strict-tool-validation.ts +117 -0
  587. package/src/utils/schema/types.ts +10 -0
  588. package/src/utils/schema/typescript.ts +198 -0
  589. package/src/utils/schema/wire.ts +789 -0
  590. package/src/utils/schema/zod-decontaminate.ts +331 -0
  591. package/src/utils/sdk-stream-timeout.ts +43 -0
  592. package/src/utils/sse-debug.ts +18 -0
  593. package/src/utils/stream-markup-healing.ts +247 -0
  594. package/src/utils/thinking-loop.ts +552 -0
  595. package/src/utils/tool-call-loop-guard.ts +107 -0
  596. package/src/utils/tool-choice.ts +99 -0
  597. package/src/utils/validation.ts +1507 -0
  598. package/src/utils.ts +171 -0
@@ -0,0 +1,1901 @@
1
+ /**
2
+ * Provider-specific JSON Schema normalization used in the request path.
3
+ *
4
+ * Google's Schema proto, Cloud Code Assist's Claude 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 "jeopi-utils";
10
+ import * as AIError from "../../error";
11
+ import { dereferenceJsonSchema } from "./dereference";
12
+ import { upgradeJsonSchemaTo202012 } from "./draft";
13
+ import { areJsonValuesEqual, mergeCompatibleEnumSchemas, mergePropertySchemas } from "./equality";
14
+ import {
15
+ ALL_CCA_TYPE_SPECIFIC_KEYS,
16
+ CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS,
17
+ CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS,
18
+ COMBINATOR_KEYS,
19
+ LIFTABLE_TO_DESCRIPTION_FIELDS,
20
+ NON_STRUCTURAL_SCHEMA_KEYS,
21
+ UNSUPPORTED_SCHEMA_FIELDS,
22
+ } from "./fields";
23
+ import { isValidJsonSchema } from "./meta-validator";
24
+ import { type DescriptionSpillFormat, spillToDescription } from "./spill";
25
+ import { enter, epochNext, exit, once, stamp } from "./stamps";
26
+ import { isJsonObject, isJsonObjectEmpty, type JsonObject } from "./types";
27
+ import { decontaminateZodInstance } from "./zod-decontaminate";
28
+
29
+ export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners";
30
+
31
+ export interface NormalizeSchemaOptions {
32
+ unsupportedFields: (key: string) => boolean;
33
+ normalizeFieldNames: boolean;
34
+ collapseNullFields: boolean;
35
+ normalizeTypeArrayToNullable: boolean;
36
+ stripNullableKeyword: boolean;
37
+ autoPropertyOrdering: boolean;
38
+ ensureObjectProperties: boolean;
39
+ liftStrippedToDescription:
40
+ | false
41
+ | {
42
+ keys?: (key: string) => boolean;
43
+ format?: DescriptionSpillFormat;
44
+ };
45
+ mergeObjectCombiners: boolean;
46
+ collapseSameTypeCombiners: boolean;
47
+ collapseMixedTypeCombiners: boolean;
48
+ stripResidualCombinersFixpoint: boolean;
49
+ extractNullableFromUnions: boolean;
50
+ inferTypeForBareEnum: boolean;
51
+ foldOneOfIntoAnyOf: boolean;
52
+ dropNonScalarEnum: boolean;
53
+ rejectResidualIncompatibilities?: ReadonlyArray<ResidualSchemaIncompatibility>;
54
+ validateAndFallback?: { fallback: unknown };
55
+ }
56
+
57
+ interface NormalizeSchemaWalkOptions extends NormalizeSchemaOptions {
58
+ insideProperties: boolean;
59
+ }
60
+
61
+ interface ResidualIncompatibilityChecks {
62
+ typeArray: boolean;
63
+ typeNull: boolean;
64
+ nullable: boolean;
65
+ combiners: boolean;
66
+ }
67
+
68
+ const SNAKE_TO_CAMEL_RENAMES = new Map<string, string>([
69
+ ["additional_properties", "additionalProperties"],
70
+ ["any_of", "anyOf"],
71
+ ["prefix_items", "prefixItems"],
72
+ ["property_ordering", "propertyOrdering"],
73
+ ]);
74
+
75
+ const JSON_SCHEMA_COMBINERS = ["anyOf", "oneOf"] as const;
76
+ const CCA_FORBIDDEN_COMBINERS = new Set(["anyOf", "oneOf", "allOf"]);
77
+
78
+ const CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA = {
79
+ type: "object",
80
+ properties: {},
81
+ } as const;
82
+
83
+ function isGoogleUnsupportedSchemaField(key: string): boolean {
84
+ return Object.hasOwn(UNSUPPORTED_SCHEMA_FIELDS, key);
85
+ }
86
+
87
+ function isMcpUnsupportedSchemaField(key: string): boolean {
88
+ return key === "$schema";
89
+ }
90
+
91
+ function isMoonshotUnsupportedSchemaField(key: string): boolean {
92
+ // `default` is an MFJS Meta Data field (kept); everything else here is a
93
+ // validation/decorative keyword or tuple form MFJS rejects.
94
+ if (key === "default") return false;
95
+ return Object.hasOwn(NON_STRUCTURAL_SCHEMA_KEYS, key) || key === "prefixItems";
96
+ }
97
+
98
+ function isDefaultLiftableToDescriptionField(key: string): boolean {
99
+ return Object.hasOwn(LIFTABLE_TO_DESCRIPTION_FIELDS, key);
100
+ }
101
+
102
+ /**
103
+ * Returns `obj` unchanged when no renamable key is present; otherwise returns
104
+ * a fresh shallow-copy with snake_case keys rewritten. The collision rule
105
+ * matches upstream (`pop(from)` → `set(to)`): snake_case wins over an
106
+ * existing camelCase entry, matching python-genai/_transformers.py:751.
107
+ */
108
+ function applySnakeCaseRenames(obj: JsonObject): JsonObject {
109
+ let needsRename = false;
110
+ for (const k in obj) {
111
+ if (!Object.hasOwn(obj, k)) continue;
112
+ if (SNAKE_TO_CAMEL_RENAMES.has(k)) {
113
+ needsRename = true;
114
+ break;
115
+ }
116
+ }
117
+ if (!needsRename) return obj;
118
+ const out: JsonObject = {};
119
+ for (const k in obj) {
120
+ if (!Object.hasOwn(obj, k)) continue;
121
+ const renamed = SNAKE_TO_CAMEL_RENAMES.get(k);
122
+ if (renamed !== undefined) {
123
+ out[renamed] = obj[k];
124
+ } else if (!outHasOwn(out, k)) {
125
+ out[k] = obj[k];
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+
131
+ /**
132
+ * `handle_null_fields` (python-genai/_transformers.py:584-640) applied at the
133
+ * parent level BEFORE child recursion — matches upstream's call order at
134
+ * `process_schema` line 768. Returns a new object when changes apply, the
135
+ * original reference otherwise (zero-allocation fast path).
136
+ */
137
+ function preHandleNullFields(obj: JsonObject): JsonObject {
138
+ if (obj.type === "null") {
139
+ const out: JsonObject = {};
140
+ for (const k in obj) {
141
+ if (!Object.hasOwn(obj, k) || k === "type") continue;
142
+ out[k] = obj[k];
143
+ }
144
+ out.nullable = true;
145
+ return out;
146
+ }
147
+ if (!Array.isArray(obj.anyOf)) return obj;
148
+ const variants = obj.anyOf as unknown[];
149
+ let sawNull = false;
150
+ const kept: unknown[] = [];
151
+ for (const v of variants) {
152
+ if (isJsonObject(v) && v.type === "null") {
153
+ sawNull = true;
154
+ continue;
155
+ }
156
+ kept.push(v);
157
+ }
158
+ if (!sawNull) return obj;
159
+ const out: JsonObject = {};
160
+ for (const k in obj) {
161
+ if (Object.hasOwn(obj, k)) out[k] = obj[k];
162
+ }
163
+ out.nullable = true;
164
+ if (kept.length === 0) {
165
+ delete out.anyOf;
166
+ } else if (kept.length === 1 && isJsonObject(kept[0])) {
167
+ delete out.anyOf;
168
+ const only = kept[0];
169
+ for (const k in only) {
170
+ if (Object.hasOwn(only, k) && !outHasOwn(out, k)) out[k] = only[k];
171
+ }
172
+ } else {
173
+ out.anyOf = kept;
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function outHasOwn(obj: JsonObject, key: string): boolean {
179
+ return Object.hasOwn(obj, key);
180
+ }
181
+
182
+ function inferJsonSchemaTypeFromValue(value: unknown): string | undefined {
183
+ if (value === null) return "null";
184
+ if (Array.isArray(value)) return "array";
185
+ switch (typeof value) {
186
+ case "string":
187
+ return "string";
188
+ case "number":
189
+ return "number";
190
+ case "boolean":
191
+ return "boolean";
192
+ case "object":
193
+ return "object";
194
+ default:
195
+ return undefined;
196
+ }
197
+ }
198
+
199
+ function pushEnumValue(values: unknown[], value: unknown): void {
200
+ if (!values.some(existing => areJsonValuesEqual(existing, value))) {
201
+ values.push(value);
202
+ }
203
+ }
204
+
205
+ function pushStrippedDescriptionEntry(
206
+ spill: Array<[string, unknown]> | undefined,
207
+ key: string,
208
+ value: unknown,
209
+ options: NormalizeSchemaWalkOptions,
210
+ ): Array<[string, unknown]> | undefined {
211
+ const lift = options.liftStrippedToDescription;
212
+ if (!lift) return spill;
213
+ const isLiftable = lift.keys ?? isDefaultLiftableToDescriptionField;
214
+ if (!isLiftable(key)) return spill;
215
+ const next = spill ?? [];
216
+ next.push([key, value]);
217
+ return next;
218
+ }
219
+
220
+ function applyDescriptionSpill(
221
+ result: JsonObject,
222
+ spill: Array<[string, unknown]> | undefined,
223
+ options: NormalizeSchemaWalkOptions,
224
+ ): void {
225
+ const lift = options.liftStrippedToDescription;
226
+ if (!lift || spill === undefined) return;
227
+ spillToDescription(result, spill, lift.format ?? "spill");
228
+ }
229
+
230
+ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions): unknown {
231
+ if (Array.isArray(value)) {
232
+ if (!enter(value)) return [];
233
+ try {
234
+ return value.map(entry => normalizeSchemaNode(entry, options));
235
+ } finally {
236
+ exit(value);
237
+ }
238
+ }
239
+ if (!isJsonObject(value)) {
240
+ return value;
241
+ }
242
+ // `enter`/`exit` path-tracking (not a visited-set): DAG-shared subtrees are
243
+ // normalized at every occurrence; only true cycles short-circuit to `{}`.
244
+ if (!enter(value)) return {};
245
+ try {
246
+ return normalizeSchemaObjectNode(value, options);
247
+ } finally {
248
+ exit(value);
249
+ }
250
+ }
251
+
252
+ function normalizeSchemaObjectNode(value: JsonObject, options: NormalizeSchemaWalkOptions): unknown {
253
+ let obj = options.normalizeFieldNames && !options.insideProperties ? applySnakeCaseRenames(value) : value;
254
+ if (options.collapseNullFields && !options.insideProperties) {
255
+ obj = preHandleNullFields(obj);
256
+ }
257
+ const result: JsonObject = {};
258
+ let spill: Array<[string, unknown]> | undefined;
259
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
260
+ if (!Array.isArray(obj[combiner])) continue;
261
+ const variants = obj[combiner] as JsonObject[];
262
+ const allHaveConst = variants.every(v => isJsonObject(v) && "const" in v);
263
+ if (!allHaveConst || variants.length === 0) continue;
264
+
265
+ const dedupedEnum: unknown[] = [];
266
+ for (const variant of variants) {
267
+ pushEnumValue(dedupedEnum, variant.const);
268
+ }
269
+ result.enum = dedupedEnum;
270
+
271
+ const explicitTypes = variants
272
+ .map(variant => variant.type)
273
+ .filter((variantType): variantType is string => typeof variantType === "string");
274
+ const allHaveSameExplicitType =
275
+ explicitTypes.length === variants.length &&
276
+ explicitTypes.every(variantType => variantType === explicitTypes[0]);
277
+ if (allHaveSameExplicitType && explicitTypes[0]) {
278
+ result.type = explicitTypes[0];
279
+ } else {
280
+ const inferredTypes = dedupedEnum
281
+ .map(enumValue => inferJsonSchemaTypeFromValue(enumValue))
282
+ .filter((inferredType): inferredType is string => inferredType !== undefined);
283
+ const inferredTypeSet = new Set(inferredTypes);
284
+ if (inferredTypeSet.size === 1) {
285
+ result.type = inferredTypes[0];
286
+ } else {
287
+ const nonNullInferredTypes = inferredTypes.filter(inferredType => inferredType !== "null");
288
+ const nonNullTypeSet = new Set(nonNullInferredTypes);
289
+ if (inferredTypes.includes("null") && nonNullTypeSet.size === 1) {
290
+ result.type = nonNullInferredTypes[0];
291
+ if (!options.stripNullableKeyword) {
292
+ result.nullable = true;
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ for (const key in obj) {
299
+ if (!Object.hasOwn(obj, key) || key === combiner || outHasOwn(result, key)) continue;
300
+ const entry = obj[key];
301
+ if (!options.insideProperties && options.unsupportedFields(key)) {
302
+ spill = pushStrippedDescriptionEntry(spill, key, entry, options);
303
+ continue;
304
+ }
305
+ if (options.stripNullableKeyword && key === "nullable") continue;
306
+ result[key] = normalizeSchemaNode(entry, {
307
+ ...options,
308
+ insideProperties: !options.insideProperties && key === "properties",
309
+ });
310
+ }
311
+ applyDescriptionSpill(result, spill, options);
312
+ return applyNodePostProcessing(result, options);
313
+ }
314
+
315
+ let constValue: unknown;
316
+ for (const key in obj) {
317
+ if (!Object.hasOwn(obj, key)) continue;
318
+ const entry = obj[key];
319
+ if (!options.insideProperties && options.unsupportedFields(key)) {
320
+ spill = pushStrippedDescriptionEntry(spill, key, entry, options);
321
+ continue;
322
+ }
323
+ if (options.stripNullableKeyword && key === "nullable") continue;
324
+ if (key === "const") {
325
+ constValue = entry;
326
+ continue;
327
+ }
328
+ result[key] = normalizeSchemaNode(entry, {
329
+ ...options,
330
+ insideProperties: !options.insideProperties && key === "properties",
331
+ });
332
+ }
333
+
334
+ if (options.normalizeTypeArrayToNullable && Array.isArray(result.type)) {
335
+ const types = (result.type as unknown[]).filter((t): t is string => typeof t === "string");
336
+ const nonNull = types.filter(t => t !== "null");
337
+ if (types.includes("null") && !options.stripNullableKeyword) {
338
+ result.nullable = true;
339
+ }
340
+ result.type = nonNull[0] ?? types[0];
341
+ }
342
+ if (constValue !== undefined) {
343
+ const existingEnum = Array.isArray(result.enum) ? result.enum : [];
344
+ pushEnumValue(existingEnum, constValue);
345
+ result.enum = existingEnum;
346
+ if (!result.type) {
347
+ result.type = inferJsonSchemaTypeFromValue(constValue);
348
+ }
349
+ }
350
+
351
+ if (
352
+ options.inferTypeForBareEnum &&
353
+ !result.type &&
354
+ !Array.isArray(result.anyOf) &&
355
+ !Array.isArray(result.oneOf) &&
356
+ Array.isArray(result.enum) &&
357
+ result.enum.length > 0
358
+ ) {
359
+ const enumTypes = (result.enum as unknown[]).map(inferJsonSchemaTypeFromValue);
360
+ if (enumTypes.every((t): t is string => typeof t === "string") && new Set(enumTypes).size === 1) {
361
+ result.type = enumTypes[0];
362
+ }
363
+ }
364
+
365
+ if (options.collapseNullFields && result.type === "null") {
366
+ delete result.type;
367
+ if (!options.stripNullableKeyword) result.nullable = true;
368
+ }
369
+
370
+ if (
371
+ options.autoPropertyOrdering &&
372
+ result.type === "object" &&
373
+ !outHasOwn(result, "propertyOrdering") &&
374
+ isJsonObject(result.properties)
375
+ ) {
376
+ const props = result.properties;
377
+ const keys: string[] = [];
378
+ for (const k in props) {
379
+ if (Object.hasOwn(props, k)) keys.push(k);
380
+ }
381
+ if (keys.length > 1) result.propertyOrdering = keys;
382
+ }
383
+
384
+ if (options.ensureObjectProperties && result.type === "object" && !outHasOwn(result, "properties")) {
385
+ result.properties = {};
386
+ }
387
+
388
+ applyDescriptionSpill(result, spill, options);
389
+ return applyNodePostProcessing(result, options);
390
+ }
391
+
392
+ function applyNodePostProcessing(schema: JsonObject, options: NormalizeSchemaWalkOptions): JsonObject {
393
+ let current = schema;
394
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
395
+ if (options.mergeObjectCombiners) current = mergeObjectCombinerVariants(current, combiner);
396
+ if (options.collapseMixedTypeCombiners) current = collapseMixedTypeCombinerVariants(current, combiner);
397
+ if (options.collapseSameTypeCombiners) current = collapseSameTypeCombinerVariants(current, combiner);
398
+ }
399
+ if (options.foldOneOfIntoAnyOf) current = foldOneOfIntoAnyOf(current);
400
+ if (options.dropNonScalarEnum) current = dropNonScalarEnumForMfjs(current);
401
+ return current;
402
+ }
403
+
404
+ /** MFJS recognizes only `anyOf`; fold any residual `oneOf` into it (merging when both are present). */
405
+ function foldOneOfIntoAnyOf(schema: JsonObject): JsonObject {
406
+ if (!Array.isArray(schema.oneOf)) return schema;
407
+ const rest = copySchemaWithout(schema, "oneOf");
408
+ const existing = Array.isArray(rest.anyOf) ? (rest.anyOf as unknown[]) : [];
409
+ rest.anyOf = [...existing, ...(schema.oneOf as unknown[])];
410
+ return rest;
411
+ }
412
+
413
+ /** MFJS `enum` admits only string/number literals; drop an enum carrying other types, keeping the inferred `type`. */
414
+ function dropNonScalarEnumForMfjs(schema: JsonObject): JsonObject {
415
+ if (!Array.isArray(schema.enum)) return schema;
416
+ const allScalar = (schema.enum as unknown[]).every(v => typeof v === "string" || typeof v === "number");
417
+ if (allScalar) return schema;
418
+ return copySchemaWithout(schema, "enum");
419
+ }
420
+
421
+ /** Copy all keys from a schema except the specified combiner key. */
422
+ export function copySchemaWithout(schema: JsonObject, combiner: string): JsonObject {
423
+ const { [combiner]: _, ...rest } = schema;
424
+ return rest;
425
+ }
426
+
427
+ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
428
+ const variantsRaw = schema[combiner];
429
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
430
+ return schema;
431
+ }
432
+
433
+ const variants: JsonObject[] = [];
434
+ for (const entry of variantsRaw) {
435
+ if (!isJsonObject(entry)) {
436
+ return schema;
437
+ }
438
+ const variantType = entry.type;
439
+ const hasObjectShape =
440
+ isJsonObject(entry.properties) ||
441
+ Array.isArray(entry.required) ||
442
+ Object.hasOwn(entry, "additionalProperties");
443
+ if (variantType === undefined && !hasObjectShape) {
444
+ return schema;
445
+ }
446
+ if (variantType !== undefined && variantType !== "object") {
447
+ return schema;
448
+ }
449
+ if (entry.properties !== undefined && !isJsonObject(entry.properties)) {
450
+ return schema;
451
+ }
452
+ if (entry.required !== undefined && !Array.isArray(entry.required)) {
453
+ return schema;
454
+ }
455
+ variants.push(entry);
456
+ }
457
+
458
+ const mergedProperties: JsonObject = {};
459
+ const ownProperties = isJsonObject(schema.properties) ? schema.properties : {};
460
+ for (const name in ownProperties) {
461
+ if (Object.hasOwn(ownProperties, name)) mergedProperties[name] = ownProperties[name];
462
+ }
463
+
464
+ for (const variant of variants) {
465
+ const properties = isJsonObject(variant.properties) ? variant.properties : {};
466
+ for (const name in properties) {
467
+ if (!Object.hasOwn(properties, name)) continue;
468
+ const propertySchema = properties[name];
469
+ const existingSchema = mergedProperties[name];
470
+ mergedProperties[name] =
471
+ existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema);
472
+ }
473
+ }
474
+
475
+ const nextSchema = copySchemaWithout(schema, combiner);
476
+ nextSchema.type = "object";
477
+ nextSchema.properties = mergedProperties;
478
+
479
+ let requiredIntersection: string[] | undefined;
480
+ for (const variant of variants) {
481
+ const variantRequired = Array.isArray(variant.required)
482
+ ? variant.required.filter((r): r is string => typeof r === "string")
483
+ : [];
484
+ if (requiredIntersection === undefined) {
485
+ requiredIntersection = [...variantRequired];
486
+ } else {
487
+ const reqSet = new Set(variantRequired);
488
+ requiredIntersection = requiredIntersection.filter(r => reqSet.has(r));
489
+ }
490
+ }
491
+ const parentRequired = Array.isArray(schema.required)
492
+ ? schema.required.filter((r): r is string => typeof r === "string")
493
+ : [];
494
+ const safeRequired = new Set<string>();
495
+ for (const name of requiredIntersection ?? []) {
496
+ if (Object.hasOwn(mergedProperties, name)) safeRequired.add(name);
497
+ }
498
+ for (const name of parentRequired) {
499
+ if (Object.hasOwn(ownProperties, name) && Object.hasOwn(mergedProperties, name)) {
500
+ safeRequired.add(name);
501
+ }
502
+ }
503
+ const requiredInPropertyOrder: string[] = [];
504
+ for (const name in mergedProperties) {
505
+ if (Object.hasOwn(mergedProperties, name) && safeRequired.has(name)) requiredInPropertyOrder.push(name);
506
+ }
507
+ if (requiredInPropertyOrder.length > 0) {
508
+ nextSchema.required = requiredInPropertyOrder;
509
+ } else {
510
+ delete nextSchema.required;
511
+ }
512
+
513
+ return nextSchema;
514
+ }
515
+
516
+ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
517
+ const variantsRaw = schema[combiner];
518
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
519
+ return schema;
520
+ }
521
+
522
+ const seenTypes = new Set<string>();
523
+ const variantTypes: string[] = [];
524
+ const mergedVariantFields: JsonObject = {};
525
+ for (const entry of variantsRaw) {
526
+ if (!isJsonObject(entry) || typeof entry.type !== "string") {
527
+ return schema;
528
+ }
529
+
530
+ const variantType = entry.type;
531
+ if (seenTypes.has(variantType)) {
532
+ return schema;
533
+ }
534
+
535
+ const allowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[variantType];
536
+ if (!allowedKeys) {
537
+ return schema;
538
+ }
539
+
540
+ for (const key in entry) {
541
+ if (!Object.hasOwn(entry, key)) continue;
542
+ const variantValue = entry[key];
543
+ if (key === "type") continue;
544
+ if (!Object.hasOwn(allowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) {
545
+ return schema;
546
+ }
547
+
548
+ const existingValue = mergedVariantFields[key];
549
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, variantValue)) {
550
+ return schema;
551
+ }
552
+ mergedVariantFields[key] = variantValue;
553
+ }
554
+
555
+ seenTypes.add(variantType);
556
+ variantTypes.push(variantType);
557
+ }
558
+
559
+ if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
560
+ return schema;
561
+ }
562
+ const nextSchema = copySchemaWithout(schema, combiner);
563
+ const nonNullTypes = variantTypes.filter(t => t !== "null");
564
+ const chosenType: string = nonNullTypes[0] ?? variantTypes[0];
565
+ nextSchema.type = chosenType;
566
+ const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[chosenType] ?? {};
567
+
568
+ // Strip sibling keys that were copied from the parent and belong to a
569
+ // different type (e.g. `items` sibling on a now-string-typed schema).
570
+ for (const key in nextSchema) {
571
+ if (!Object.hasOwn(nextSchema, key)) continue;
572
+ if (key === "type") continue;
573
+ if (
574
+ Object.hasOwn(ALL_CCA_TYPE_SPECIFIC_KEYS, key) &&
575
+ !Object.hasOwn(chosenTypeAllowedKeys, key) &&
576
+ !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)
577
+ ) {
578
+ delete nextSchema[key];
579
+ }
580
+ }
581
+
582
+ for (const key in mergedVariantFields) {
583
+ if (!Object.hasOwn(mergedVariantFields, key)) continue;
584
+ // Drop type-specific keys that don't belong to the chosen type
585
+ if (!Object.hasOwn(chosenTypeAllowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) {
586
+ continue;
587
+ }
588
+ const value = mergedVariantFields[key];
589
+ const existingValue = nextSchema[key];
590
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
591
+ return schema;
592
+ }
593
+ if (existingValue === undefined) {
594
+ nextSchema[key] = value;
595
+ }
596
+ }
597
+ return nextSchema;
598
+ }
599
+
600
+ function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
601
+ const variantsRaw = schema[combiner];
602
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) return schema;
603
+ let commonType: string | undefined;
604
+ const variants: JsonObject[] = [];
605
+ for (const entry of variantsRaw) {
606
+ if (!isJsonObject(entry) || typeof entry.type !== "string") return schema;
607
+ if (commonType === undefined) commonType = entry.type;
608
+ else if (entry.type !== commonType) return schema;
609
+ variants.push(entry);
610
+ }
611
+ const firstEntry = variants[0];
612
+ if (!firstEntry) return schema;
613
+
614
+ // Same-type collapse otherwise keeps only the first variant's keys, silently
615
+ // dropping the other branches' `enum` members (e.g. an anyOf of two string
616
+ // enums collapsing to just the first).
617
+ const enumVariantCount = variants.reduce((n, variant) => n + (Array.isArray(variant.enum) ? 1 : 0), 0);
618
+
619
+ let collapsed: JsonObject;
620
+ if (enumVariantCount === variants.length) {
621
+ // Every branch is an `enum` schema: fold them with
622
+ // `mergeCompatibleEnumSchemas`, which unions the members only when the
623
+ // branches agree on `type` and every non-`enum` field, returning null
624
+ // otherwise. Bail to the untouched schema on any disagreement so the
625
+ // residual-combiner fallback handles it instead of mislabeling.
626
+ let merged: JsonObject | null = firstEntry;
627
+ for (let i = 1; i < variants.length && merged !== null; i++) {
628
+ merged = mergeCompatibleEnumSchemas(merged, variants[i]);
629
+ }
630
+ if (merged === null) return schema;
631
+ collapsed = merged;
632
+ } else if (enumVariantCount > 0) {
633
+ // Mixed branches: at least one is unconstrained by `enum` and is therefore
634
+ // broader. Collapse onto the first such branch so the result keeps its
635
+ // (broader) keys — never narrowing to an enum branch's members or leaking
636
+ // its metadata (description/default).
637
+ collapsed = variants.find(variant => !Array.isArray(variant.enum)) ?? firstEntry;
638
+ } else {
639
+ // No `enum` branches: keep the original first-wins behavior.
640
+ collapsed = firstEntry;
641
+ }
642
+
643
+ const nextSchema = copySchemaWithout(schema, combiner);
644
+ for (const key in collapsed) {
645
+ if (Object.hasOwn(collapsed, key) && !outHasOwn(nextSchema, key)) nextSchema[key] = collapsed[key];
646
+ }
647
+ return nextSchema;
648
+ }
649
+
650
+ /**
651
+ * Recursively strip any remaining anyOf/oneOf that same-type or mixed-type
652
+ * collapse can handle. This is needed because object-combiner merging can
653
+ * create new anyOf in merged subtrees after child normalization already ran.
654
+ */
655
+ export function stripResidualCombiners(value: unknown, epoch: number = epochNext()): unknown {
656
+ if (Array.isArray(value)) {
657
+ if (!once(value, epoch)) return [];
658
+ return value.map(entry => stripResidualCombiners(entry, epoch));
659
+ }
660
+ if (!isJsonObject(value)) return value;
661
+ if (!once(value, epoch)) return {};
662
+ const result: JsonObject = {};
663
+ for (const key in value) {
664
+ if (Object.hasOwn(value, key)) result[key] = stripResidualCombiners(value[key], epoch);
665
+ }
666
+ let current: JsonObject = result;
667
+ let changed = true;
668
+ while (changed) {
669
+ changed = false;
670
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
671
+ const sameType = collapseSameTypeCombinerVariants(current, combiner);
672
+ if (sameType !== current) {
673
+ current = sameType;
674
+ changed = true;
675
+ }
676
+ const mixed = collapseMixedTypeCombinerVariants(current, combiner);
677
+ if (mixed !== current) {
678
+ current = mixed;
679
+ changed = true;
680
+ }
681
+ }
682
+ }
683
+ return current;
684
+ }
685
+
686
+ interface NullableExtractionResult {
687
+ schema: unknown;
688
+ nullable: boolean;
689
+ }
690
+
691
+ function extractNullableUnionSchema(schema: unknown): NullableExtractionResult {
692
+ if (!isJsonObject(schema)) {
693
+ return { schema, nullable: false };
694
+ }
695
+
696
+ if (schema.nullable === true) {
697
+ const nextSchema = { ...schema };
698
+ delete nextSchema.nullable;
699
+ return { schema: nextSchema, nullable: true };
700
+ }
701
+
702
+ if (Array.isArray(schema.type)) {
703
+ const typeVariants = schema.type.filter((entry): entry is string => typeof entry === "string");
704
+ const nonNullTypes = typeVariants.filter(entry => entry !== "null");
705
+ if (typeVariants.includes("null") && nonNullTypes.length === 1) {
706
+ const nextSchema = { ...schema, type: nonNullTypes[0] };
707
+ return { schema: nextSchema, nullable: true };
708
+ }
709
+ }
710
+
711
+ for (const combiner of JSON_SCHEMA_COMBINERS) {
712
+ const variantsRaw = schema[combiner];
713
+ if (!Array.isArray(variantsRaw)) continue;
714
+
715
+ let hasNullVariant = false;
716
+ const nonNullVariants: unknown[] = [];
717
+ for (const variant of variantsRaw) {
718
+ if (isJsonObject(variant) && variant.type === "null") {
719
+ let keyCount = 0;
720
+ for (const k in variant) {
721
+ if (!Object.hasOwn(variant, k)) continue;
722
+ if (++keyCount > 1) break;
723
+ }
724
+ if (keyCount === 1) {
725
+ hasNullVariant = true;
726
+ continue;
727
+ }
728
+ }
729
+ nonNullVariants.push(variant);
730
+ }
731
+
732
+ if (!hasNullVariant || nonNullVariants.length !== 1 || !isJsonObject(nonNullVariants[0])) {
733
+ continue;
734
+ }
735
+
736
+ const nextSchema = copySchemaWithout(schema, combiner);
737
+ const nonNullVariant = nonNullVariants[0];
738
+ for (const key in nonNullVariant) {
739
+ if (!Object.hasOwn(nonNullVariant, key)) continue;
740
+ const value = nonNullVariant[key];
741
+ const existingValue = nextSchema[key];
742
+ if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
743
+ return { schema, nullable: false };
744
+ }
745
+ if (existingValue === undefined) {
746
+ nextSchema[key] = value;
747
+ }
748
+ }
749
+ return { schema: nextSchema, nullable: true };
750
+ }
751
+
752
+ return { schema, nullable: false };
753
+ }
754
+
755
+ interface NullableNormalizationResult {
756
+ schema: unknown;
757
+ nullable: boolean;
758
+ }
759
+
760
+ function normalizeNullablePropertiesForCloudCodeAssist(
761
+ value: unknown,
762
+ isPropertySchema = false,
763
+ epoch: number = epochNext(),
764
+ ): NullableNormalizationResult {
765
+ if (Array.isArray(value)) {
766
+ if (!once(value, epoch)) {
767
+ return { schema: [], nullable: false };
768
+ }
769
+ return {
770
+ schema: value.map(entry => normalizeNullablePropertiesForCloudCodeAssist(entry, false, epoch).schema),
771
+ nullable: false,
772
+ };
773
+ }
774
+ if (!isJsonObject(value)) {
775
+ return { schema: value, nullable: false };
776
+ }
777
+ if (!once(value, epoch)) {
778
+ return { schema: {}, nullable: false };
779
+ }
780
+
781
+ const normalized: JsonObject = {};
782
+ for (const key in value) {
783
+ if (Object.hasOwn(value, key))
784
+ normalized[key] = normalizeNullablePropertiesForCloudCodeAssist(value[key], false, epoch).schema;
785
+ }
786
+
787
+ if (isJsonObject(normalized.properties)) {
788
+ const properties = normalized.properties;
789
+ const required = new Set(
790
+ Array.isArray(normalized.required)
791
+ ? normalized.required.filter((entry): entry is string => typeof entry === "string")
792
+ : [],
793
+ );
794
+ const nextProperties: JsonObject = {};
795
+ for (const name in properties) {
796
+ if (!Object.hasOwn(properties, name)) continue;
797
+ const normalizedProperty = normalizeNullablePropertiesForCloudCodeAssist(properties[name], true, epoch);
798
+ nextProperties[name] = normalizedProperty.schema;
799
+ if (normalizedProperty.nullable) {
800
+ required.delete(name);
801
+ }
802
+ }
803
+ normalized.properties = nextProperties;
804
+ if (Array.isArray(normalized.required)) {
805
+ normalized.required = Array.from(required);
806
+ }
807
+ }
808
+
809
+ if (!isPropertySchema) {
810
+ return { schema: normalized, nullable: false };
811
+ }
812
+
813
+ return extractNullableUnionSchema(normalized);
814
+ }
815
+
816
+ function createResidualIncompatibilityChecks(
817
+ checks: ReadonlyArray<ResidualSchemaIncompatibility> | undefined,
818
+ ): ResidualIncompatibilityChecks | undefined {
819
+ if (!checks || checks.length === 0) return undefined;
820
+ const result: ResidualIncompatibilityChecks = {
821
+ typeArray: false,
822
+ typeNull: false,
823
+ nullable: false,
824
+ combiners: false,
825
+ };
826
+ for (const check of checks) {
827
+ switch (check) {
828
+ case "type-array":
829
+ result.typeArray = true;
830
+ break;
831
+ case "type-null":
832
+ result.typeNull = true;
833
+ break;
834
+ case "nullable":
835
+ result.nullable = true;
836
+ break;
837
+ case "combiners":
838
+ result.combiners = true;
839
+ break;
840
+ }
841
+ }
842
+ return result;
843
+ }
844
+
845
+ function hasResidualSchemaIncompatibilities(
846
+ value: unknown,
847
+ checks: ResidualIncompatibilityChecks,
848
+ epoch: number = epochNext(),
849
+ ): boolean {
850
+ if (Array.isArray(value)) {
851
+ if (!once(value, epoch)) return false;
852
+ return value.some(entry => hasResidualSchemaIncompatibilities(entry, checks, epoch));
853
+ }
854
+ if (!isJsonObject(value)) {
855
+ return false;
856
+ }
857
+ if (!once(value, epoch)) {
858
+ return false;
859
+ }
860
+
861
+ if (checks.typeArray && Array.isArray(value.type)) return true;
862
+ if (checks.typeNull && value.type === "null") return true;
863
+ if (checks.nullable && Object.hasOwn(value, "nullable")) return true;
864
+ if (checks.combiners) {
865
+ for (const combiner of CCA_FORBIDDEN_COMBINERS) {
866
+ if (Array.isArray(value[combiner])) return true;
867
+ }
868
+ }
869
+ for (const k in value) {
870
+ if (!Object.hasOwn(value, k)) continue;
871
+ if (hasResidualSchemaIncompatibilities(value[k], checks, epoch)) {
872
+ return true;
873
+ }
874
+ }
875
+ return false;
876
+ }
877
+
878
+ export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions): unknown {
879
+ const detoxified = decontaminateZodInstance(value);
880
+ const upgraded = upgradeJsonSchemaTo202012(detoxified);
881
+ const dereferenced = dereferenceJsonSchema(upgraded);
882
+ let normalized = normalizeSchemaNode(dereferenced, {
883
+ ...options,
884
+ insideProperties: false,
885
+ });
886
+ if (options.stripResidualCombinersFixpoint) {
887
+ normalized = stripResidualCombiners(normalized);
888
+ }
889
+ if (options.extractNullableFromUnions) {
890
+ normalized = normalizeNullablePropertiesForCloudCodeAssist(normalized).schema;
891
+ }
892
+ const residualChecks = createResidualIncompatibilityChecks(options.rejectResidualIncompatibilities);
893
+ if (residualChecks && hasResidualSchemaIncompatibilities(normalized, residualChecks)) {
894
+ logger.debug("Schema has residual provider incompatibilities, using fallback");
895
+ return options.validateAndFallback?.fallback ?? normalized;
896
+ }
897
+ if (options.validateAndFallback && !isValidJsonSchema(normalized)) {
898
+ logger.debug("Schema failed validation, using fallback");
899
+ return options.validateAndFallback.fallback;
900
+ }
901
+ return normalized;
902
+ }
903
+
904
+ export function normalizeSchemaForGoogle(value: unknown): unknown {
905
+ return normalizeSchema(value, {
906
+ unsupportedFields: isGoogleUnsupportedSchemaField,
907
+ normalizeFieldNames: true,
908
+ collapseNullFields: true,
909
+ normalizeTypeArrayToNullable: true,
910
+ stripNullableKeyword: false,
911
+ autoPropertyOrdering: true,
912
+ ensureObjectProperties: true,
913
+ liftStrippedToDescription: { format: "spill" },
914
+ mergeObjectCombiners: false,
915
+ collapseSameTypeCombiners: false,
916
+ collapseMixedTypeCombiners: false,
917
+ stripResidualCombinersFixpoint: false,
918
+ extractNullableFromUnions: false,
919
+ inferTypeForBareEnum: true,
920
+ dropNonScalarEnum: false,
921
+ foldOneOfIntoAnyOf: false,
922
+ });
923
+ }
924
+
925
+ export function normalizeSchemaForCCA(value: unknown): unknown {
926
+ return normalizeSchema(value, {
927
+ unsupportedFields: isGoogleUnsupportedSchemaField,
928
+ normalizeFieldNames: true,
929
+ collapseNullFields: false,
930
+ normalizeTypeArrayToNullable: true,
931
+ stripNullableKeyword: true,
932
+ autoPropertyOrdering: false,
933
+ ensureObjectProperties: true,
934
+ liftStrippedToDescription: { format: "spill" },
935
+ mergeObjectCombiners: true,
936
+ collapseSameTypeCombiners: true,
937
+ collapseMixedTypeCombiners: true,
938
+ stripResidualCombinersFixpoint: true,
939
+ extractNullableFromUnions: true,
940
+ inferTypeForBareEnum: true,
941
+ dropNonScalarEnum: false,
942
+ foldOneOfIntoAnyOf: false,
943
+ rejectResidualIncompatibilities: ["type-array", "type-null", "nullable", "combiners"],
944
+ validateAndFallback: { fallback: CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA },
945
+ });
946
+ }
947
+
948
+ export function normalizeSchemaForMCP(value: unknown): unknown {
949
+ return normalizeSchema(value, {
950
+ unsupportedFields: isMcpUnsupportedSchemaField,
951
+ normalizeFieldNames: false,
952
+ collapseNullFields: false,
953
+ normalizeTypeArrayToNullable: false,
954
+ foldOneOfIntoAnyOf: false,
955
+ stripNullableKeyword: true,
956
+ autoPropertyOrdering: false,
957
+ ensureObjectProperties: false,
958
+ liftStrippedToDescription: false,
959
+ mergeObjectCombiners: false,
960
+ collapseSameTypeCombiners: false,
961
+ collapseMixedTypeCombiners: false,
962
+ stripResidualCombinersFixpoint: false,
963
+ extractNullableFromUnions: false,
964
+ inferTypeForBareEnum: false,
965
+ dropNonScalarEnum: false,
966
+ });
967
+ }
968
+
969
+ /**
970
+ * Moonshot Flavored JSON Schema (MFJS) — the stricter subset Moonshot/Kimi
971
+ * native hosts (api.moonshot.ai, api.kimi.com) validate
972
+ * `tools.function.parameters` against. It rejects standard JSON Schema
973
+ * constructs that OpenAI-compatible hosts accept, returning HTTP 400
974
+ * `tools.function.parameters is not a valid moonshot flavored json schema`.
975
+ * Differences this normalizer reconciles:
976
+ *
977
+ * - `const` (incl. `anyOf`/`oneOf` whose every branch is a bare `const`) is
978
+ * rejected; collapse to `enum` with an inferred scalar `type`.
979
+ * - `oneOf` is not an MFJS combinator (only `anyOf` is); residual `oneOf` is
980
+ * folded into `anyOf`.
981
+ * - `type` must be a scalar string; `type: [...]` arrays are reduced to a
982
+ * single scalar (the `null` branch is dropped — `nullable` is unsupported).
983
+ * - Enum-bearing nodes get an inferred `type` (the idiomatic MFJS form; a bare
984
+ * `enum` is valid too) so `anyOf` branches always carry a `type`.
985
+ * - Validation/decorative keywords (`minItems`, `maxItems`, `maxLength`,
986
+ * `pattern`, `format`, `title`, …) and tuple `prefixItems` are rejected and
987
+ * stripped, spilling human-meaningful ones into the sibling `description`.
988
+ * `default` and `description` are MFJS Meta Data fields and are preserved.
989
+ * - `additionalProperties` (boolean or schema) and `type: "null"` (incl.
990
+ * inside `anyOf`) are kept.
991
+ *
992
+ * Out of scope (absent from the built-in tool surface, spec-ambiguous to
993
+ * rewrite blindly): `allOf` intersection merging, external/recursive `$ref`,
994
+ * and the depth-10 limit.
995
+ */
996
+ export function normalizeSchemaForMoonshot(value: unknown): unknown {
997
+ return normalizeSchema(value, {
998
+ unsupportedFields: isMoonshotUnsupportedSchemaField,
999
+ normalizeFieldNames: false,
1000
+ collapseNullFields: false,
1001
+ normalizeTypeArrayToNullable: true,
1002
+ stripNullableKeyword: true,
1003
+ autoPropertyOrdering: false,
1004
+ ensureObjectProperties: false,
1005
+ liftStrippedToDescription: { format: "spill" },
1006
+ mergeObjectCombiners: false,
1007
+ collapseSameTypeCombiners: false,
1008
+ collapseMixedTypeCombiners: false,
1009
+ stripResidualCombinersFixpoint: false,
1010
+ extractNullableFromUnions: false,
1011
+ inferTypeForBareEnum: true,
1012
+ dropNonScalarEnum: true,
1013
+ foldOneOfIntoAnyOf: true,
1014
+ });
1015
+ }
1016
+
1017
+ // ---------------------------------------------------------------------------
1018
+ // OpenAI Responses — schema-valued normalization
1019
+ // ---------------------------------------------------------------------------
1020
+
1021
+ const OPENAI_RESPONSES_SCHEMA_ARRAY_KEYS = new Set(["anyOf", "oneOf", "allOf", "prefixItems"]);
1022
+ const OPENAI_RESPONSES_SCHEMA_MAP_KEYS = new Set([
1023
+ "properties",
1024
+ "patternProperties",
1025
+ // `dependencies` is the Draft-04..07 schema-valued form; older MCP servers
1026
+ // still emit `{ dependencies: { foo: { type: "object" } } }`. String-array
1027
+ // branches per key pass through `normalizeOpenAIResponsesSchemaNode`
1028
+ // untouched because non-objects return as-is.
1029
+ "dependencies",
1030
+ "dependentSchemas",
1031
+ "$defs",
1032
+ "definitions",
1033
+ ]);
1034
+ const OPENAI_RESPONSES_SCHEMA_VALUE_KEYS = new Set([
1035
+ "items",
1036
+ "additionalItems",
1037
+ "contains",
1038
+ "contentSchema",
1039
+ "propertyNames",
1040
+ "if",
1041
+ "then",
1042
+ "else",
1043
+ "not",
1044
+ "additionalProperties",
1045
+ "unevaluatedItems",
1046
+ "unevaluatedProperties",
1047
+ ]);
1048
+
1049
+ /**
1050
+ * OpenAI Responses rejects `oneOf` in tool schemas even when strict mode is
1051
+ * disabled, and rejects every schema node with `type: "object"` unless it has
1052
+ * a `properties` member. Normalize only schema-valued positions so literal
1053
+ * payloads under `enum`, `const`, `default`, and `examples` remain unchanged.
1054
+ *
1055
+ * Identity-preserving: returns the input reference unchanged when no rewrite
1056
+ * occurred so callers can dedupe via reference equality (and the strict-mode
1057
+ * cache stays warm). If a node has both `oneOf` and `anyOf`, the two are
1058
+ * concatenated (the wire payload accepts a single union; preserving both
1059
+ * would not survive).
1060
+ */
1061
+ export function sanitizeSchemaForOpenAIResponses(schema: JsonObject): JsonObject {
1062
+ return normalizeOpenAIResponsesSchemaNode(schema, new WeakMap()) as JsonObject;
1063
+ }
1064
+
1065
+ /**
1066
+ * Alias for {@link sanitizeSchemaForOpenAIResponses} matching the
1067
+ * `normalizeSchemaFor*` dispatcher naming used elsewhere in this module.
1068
+ */
1069
+ export const normalizeSchemaForOpenAIResponses: (schema: JsonObject) => JsonObject = sanitizeSchemaForOpenAIResponses;
1070
+ const OPENAI_UNSUPPORTED_REGEX_LOOKAROUNDS = new Set(["=", "!", "<=", "<!"]);
1071
+ const OPENAI_RESPONSES_PATTERN_PROPERTIES_FALLBACK = ".*";
1072
+
1073
+ function hasOpenAIUnsupportedRegexLookaround(pattern: string): boolean {
1074
+ let groupStart = pattern.indexOf("(?");
1075
+ while (groupStart !== -1) {
1076
+ let escapes = 0;
1077
+ for (let i = groupStart - 1; i >= 0 && pattern[i] === "\\"; i--) escapes++;
1078
+ if (escapes % 2 === 0) {
1079
+ const operator =
1080
+ pattern[groupStart + 2] === "<" ? pattern.slice(groupStart + 2, groupStart + 4) : pattern[groupStart + 2];
1081
+ if (OPENAI_UNSUPPORTED_REGEX_LOOKAROUNDS.has(operator)) return true;
1082
+ }
1083
+ groupStart = pattern.indexOf("(?", groupStart + 2);
1084
+ }
1085
+ return false;
1086
+ }
1087
+
1088
+ function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonObject, unknown>): unknown {
1089
+ if (!isJsonObject(value)) return value;
1090
+
1091
+ // `{}` (empty JSON Schema) ≡ `true` (JSON Schema draft 2020-12 §4.3.1).
1092
+ // Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
1093
+ // "generate an empty object" rather than "any JSON value" (issue #1179).
1094
+ // `toolWireSchema` already runs `normalizeEmptySchemas` upstream, but this
1095
+ // guard remains as a safety net for callers that invoke
1096
+ // `sanitizeSchemaForOpenAIResponses` directly on a schema that bypassed
1097
+ // the wire-schema pipeline (e.g. provider-specific fixtures, debug paths).
1098
+ if (isJsonObjectEmpty(value)) return true;
1099
+
1100
+ const cached = cache.get(value);
1101
+ if (cached) return cached;
1102
+
1103
+ // Seed the cache with the in-flight `output` BEFORE recursing so that a
1104
+ // child re-entering this node mid-walk gets the partial back instead of
1105
+ // triggering an infinite recursion. A cycle hitting this seeded entry
1106
+ // forces `changed = true` below (the cached partial is referentially
1107
+ // distinct from `value`), which is why the final `cache.set(value, result)`
1108
+ // never silently overwrites the seed with `value` on a cyclic input.
1109
+ const output: JsonObject = {};
1110
+ cache.set(value, output);
1111
+
1112
+ let changed = false;
1113
+ for (const key in value) {
1114
+ if (!Object.hasOwn(value, key)) continue;
1115
+ // Drop only well-formed `oneOf` arrays here; they are re-emitted as
1116
+ // `anyOf` after the loop so any neighboring `anyOf` entries can be
1117
+ // concatenated. A non-array `oneOf` is malformed for the wire but
1118
+ // still preserved verbatim so callers can see the original payload
1119
+ // instead of having it silently disappear.
1120
+ if (key === "oneOf" && Array.isArray(value.oneOf)) {
1121
+ changed = true;
1122
+ continue;
1123
+ }
1124
+ if (
1125
+ key === "pattern" &&
1126
+ typeof value.pattern === "string" &&
1127
+ hasOpenAIUnsupportedRegexLookaround(value.pattern)
1128
+ ) {
1129
+ changed = true;
1130
+ continue;
1131
+ }
1132
+
1133
+ const child = value[key];
1134
+ let next: unknown = child;
1135
+ if (key === "patternProperties" && isJsonObject(child)) {
1136
+ next = normalizeOpenAIResponsesSchemaMap(child, cache, true);
1137
+ } else if (OPENAI_RESPONSES_SCHEMA_MAP_KEYS.has(key) && isJsonObject(child)) {
1138
+ next = normalizeOpenAIResponsesSchemaMap(child, cache, false);
1139
+ } else if (OPENAI_RESPONSES_SCHEMA_ARRAY_KEYS.has(key) && Array.isArray(child)) {
1140
+ next = normalizeOpenAIResponsesSchemaArray(child, cache);
1141
+ } else if (OPENAI_RESPONSES_SCHEMA_VALUE_KEYS.has(key) && isJsonObject(child)) {
1142
+ next = normalizeOpenAIResponsesSchemaNode(child, cache);
1143
+ }
1144
+
1145
+ if (next !== child) changed = true;
1146
+ output[key] = next;
1147
+ }
1148
+
1149
+ if (Array.isArray(value.oneOf)) {
1150
+ const rewrittenOneOf = normalizeOpenAIResponsesSchemaArray(value.oneOf, cache);
1151
+ const existingAnyOf = output.anyOf;
1152
+ output.anyOf = Array.isArray(existingAnyOf)
1153
+ ? [...existingAnyOf, ...(rewrittenOneOf as unknown[])]
1154
+ : rewrittenOneOf;
1155
+ }
1156
+
1157
+ // Draft 2020-12 lets `type` be an array (e.g. `["object", "null"]`); treat
1158
+ // any variant that includes "object" as an object position for the
1159
+ // properties requirement.
1160
+ if (declaresObjectType(value.type) && !Object.hasOwn(value, "properties")) {
1161
+ output.properties = {};
1162
+ changed = true;
1163
+ }
1164
+
1165
+ // Safe to overwrite the seed: any cyclic re-entry above already observed
1166
+ // the seeded partial and set `changed = true` for that node, so a node
1167
+ // that finishes with `changed === false` is provably non-cyclic and
1168
+ // referentially equal to its input.
1169
+ const result = changed ? (isJsonObjectEmpty(output) ? true : output) : value;
1170
+ cache.set(value, result);
1171
+ return result;
1172
+ }
1173
+
1174
+ function declaresObjectType(type: unknown): boolean {
1175
+ if (type === "object") return true;
1176
+ if (!Array.isArray(type)) return false;
1177
+ for (const variant of type) {
1178
+ if (variant === "object") return true;
1179
+ }
1180
+ return false;
1181
+ }
1182
+
1183
+ function normalizeOpenAIResponsesSchemaArray(value: unknown[], cache: WeakMap<JsonObject, unknown>): unknown[] {
1184
+ let changed = false;
1185
+ const output = value.map(item => {
1186
+ const next = normalizeOpenAIResponsesSchemaNode(item, cache);
1187
+ if (next !== item) changed = true;
1188
+ return next;
1189
+ });
1190
+ return changed ? output : value;
1191
+ }
1192
+
1193
+ function normalizeOpenAIResponsesSchemaMap(
1194
+ schemaMap: JsonObject,
1195
+ cache: WeakMap<JsonObject, unknown>,
1196
+ stripUnsupportedRegexKeys: boolean,
1197
+ ): JsonObject {
1198
+ let changed = false;
1199
+ const output: JsonObject = {};
1200
+ for (const key in schemaMap) {
1201
+ if (!Object.hasOwn(schemaMap, key)) continue;
1202
+ const child = schemaMap[key];
1203
+ const next = normalizeOpenAIResponsesSchemaNode(child, cache);
1204
+ if (next !== child) changed = true;
1205
+ if (stripUnsupportedRegexKeys && hasOpenAIUnsupportedRegexLookaround(key)) {
1206
+ changed = true;
1207
+ appendOpenAIResponsesFallbackPatternProperty(output, next);
1208
+ continue;
1209
+ }
1210
+ output[key] = next;
1211
+ }
1212
+ return changed ? output : schemaMap;
1213
+ }
1214
+
1215
+ function appendOpenAIResponsesFallbackPatternProperty(output: JsonObject, schema: unknown): void {
1216
+ const existing = output[OPENAI_RESPONSES_PATTERN_PROPERTIES_FALLBACK];
1217
+ if (existing === undefined) {
1218
+ output[OPENAI_RESPONSES_PATTERN_PROPERTIES_FALLBACK] = schema;
1219
+ return;
1220
+ }
1221
+ if (isJsonObject(existing) && Array.isArray(existing.anyOf) && Object.keys(existing).length === 1) {
1222
+ existing.anyOf = [...existing.anyOf, schema];
1223
+ return;
1224
+ }
1225
+ output[OPENAI_RESPONSES_PATTERN_PROPERTIES_FALLBACK] = { anyOf: [existing, schema] };
1226
+ }
1227
+
1228
+ // ---------------------------------------------------------------------------
1229
+ // OpenAI strict mode — sanitize + enforce
1230
+ // ---------------------------------------------------------------------------
1231
+
1232
+ /**
1233
+ * Single primitive JSON Schema `type` keyword. Strict mode treats these
1234
+ * scalar types as concrete-enough; aggregate shapes (object, array) are not
1235
+ * included because they're not derivable from a single `enum`/`const` value.
1236
+ */
1237
+ type StrictPrimitiveType = "null" | "string" | "number" | "boolean";
1238
+
1239
+ function primitiveJsonTypeOf(value: unknown): StrictPrimitiveType | undefined {
1240
+ if (value === null) return "null";
1241
+ switch (typeof value) {
1242
+ case "string":
1243
+ return "string";
1244
+ case "number":
1245
+ return "number";
1246
+ case "boolean":
1247
+ return "boolean";
1248
+ default:
1249
+ return undefined;
1250
+ }
1251
+ }
1252
+ function jsonSchemaTypeAcceptsValue(type: string, value: unknown): boolean {
1253
+ switch (type) {
1254
+ case "null":
1255
+ return value === null;
1256
+ case "string":
1257
+ return typeof value === "string";
1258
+ case "number":
1259
+ return typeof value === "number";
1260
+ case "integer":
1261
+ return typeof value === "number" && Number.isInteger(value);
1262
+ case "boolean":
1263
+ return typeof value === "boolean";
1264
+ case "array":
1265
+ return Array.isArray(value);
1266
+ case "object":
1267
+ return isJsonObject(value);
1268
+ default:
1269
+ return true;
1270
+ }
1271
+ }
1272
+
1273
+ function narrowEnumToType(schema: Record<string, unknown>, type: string): boolean {
1274
+ const enumValues = schema.enum;
1275
+ if (!Array.isArray(enumValues)) return true;
1276
+
1277
+ const narrowed = enumValues.filter(value => jsonSchemaTypeAcceptsValue(type, value));
1278
+ if (narrowed.length === 0) return false;
1279
+ if (narrowed.length !== enumValues.length) schema.enum = narrowed;
1280
+ return true;
1281
+ }
1282
+
1283
+ /**
1284
+ * Returns the primitive `type` keyword that fully describes the constraint
1285
+ * expressed by this node's `enum` (or `const`), or `undefined` when the
1286
+ * constraint cannot be reduced to a single primitive type.
1287
+ *
1288
+ * Strict mode requires every schema node to declare a concrete `type`. When
1289
+ * the author wrote `{enum:[...]}` or `{const:X}` without a `type`, we can
1290
+ * infer one — but only when every value reduces to the same primitive type.
1291
+ * Mixed-primitive enums (`[1, "two", null]`), enums containing non-primitives
1292
+ * (`[{a:1}]`), and non-primitive consts (`{a:1}`, `[1,2,3]`) all return
1293
+ * undefined: those shapes cannot be described by a single `type` keyword, so
1294
+ * strict mode cannot represent them and the caller must fall back.
1295
+ */
1296
+ function inferStrictPrimitiveTypeFromEnumOrConst(node: Record<string, unknown>): StrictPrimitiveType | undefined {
1297
+ const values: unknown[] = Array.isArray(node.enum) ? node.enum : Object.hasOwn(node, "const") ? [node.const] : [];
1298
+ if (values.length === 0) return undefined;
1299
+ let inferred: StrictPrimitiveType | undefined;
1300
+ for (const value of values) {
1301
+ const t = primitiveJsonTypeOf(value);
1302
+ if (t === undefined) return undefined; // non-primitive (object/array) — strict can't represent
1303
+ if (inferred === undefined) inferred = t;
1304
+ else if (inferred !== t) return undefined; // mixed primitives
1305
+ }
1306
+ return inferred;
1307
+ }
1308
+
1309
+ /**
1310
+ * Per-schema-object memoization slot. The result of `tryEnforceStrictSchema`
1311
+ * is stamped directly onto the input via `stamp(target, kStrictSchema, …)`
1312
+ * so repeated calls (different providers, retries, batching) reuse the same
1313
+ * computed pair without re-walking the tree.
1314
+ */
1315
+ const kStrictSchema = Symbol("pi.schema.strict");
1316
+
1317
+ /**
1318
+ * A boolean schema (`true`/`false`) or the empty object schema `{}`: an
1319
+ * unconstrained branch with no declared type. Strict providers (OpenAI/Codex)
1320
+ * reject these, and `enforceStrictSchema` would otherwise wave a non-object
1321
+ * branch through as `strict: true`, so they disqualify a schema from strict mode
1322
+ * wherever they sit in a combinator or `items`/`prefixItems` position.
1323
+ */
1324
+ function isUnrepresentableStrictBranch(value: unknown): boolean {
1325
+ return typeof value === "boolean" || (isJsonObject(value) && isJsonObjectEmpty(value));
1326
+ }
1327
+
1328
+ /**
1329
+ * Detect schemas that strict mode *cannot* represent.
1330
+ *
1331
+ * Strict mode requires closed object shapes — every property is declared in
1332
+ * `properties` and listed in `required`. That is incompatible with:
1333
+ * - `patternProperties` (open keyset matched by regex),
1334
+ * - `additionalProperties: true` or `additionalProperties: <schema>` (open
1335
+ * keyset with optional further constraint).
1336
+ * - boolean schemas (`true`/`false`) inside `anyOf`/`oneOf`/`allOf`/`items`/
1337
+ * `prefixItems` — strict providers (OpenAI/Codex) reject the unconstrained
1338
+ * branch, and `enforceStrictSchema` would otherwise wave the non-object
1339
+ * branch through as `strict: true` (the `T | undefined` → `anyOf: [<T>, {}]`
1340
+ * → `[<T>, true]` encoding is the canonical offender).
1341
+ *
1342
+ * This check recurses into every place a child schema may live (properties,
1343
+ * items/prefixItems, combinator branches, $defs) so a single offender deep
1344
+ * in the tree disqualifies the whole schema. Used to fail-open early in
1345
+ * `tryEnforceStrictSchema` rather than throwing during enforcement.
1346
+ */
1347
+ function hasUnrepresentableStrictObjectMap(schema: Record<string, unknown>, epoch: number = epochNext()): boolean {
1348
+ if (!once(schema, epoch)) return false;
1349
+
1350
+ let hasPatternProperties = false;
1351
+ if (isJsonObject(schema.patternProperties)) {
1352
+ for (const _ in schema.patternProperties) {
1353
+ hasPatternProperties = true;
1354
+ break;
1355
+ }
1356
+ }
1357
+ const additionalPropertiesValue = schema.additionalProperties;
1358
+ const hasSchemaAdditionalProperties = additionalPropertiesValue === true || isJsonObject(additionalPropertiesValue);
1359
+ if (hasPatternProperties || hasSchemaAdditionalProperties) {
1360
+ return true;
1361
+ }
1362
+
1363
+ if (isJsonObject(schema.properties)) {
1364
+ const properties = schema.properties;
1365
+ for (const k in properties) {
1366
+ const propertySchema = properties[k];
1367
+ if (isUnrepresentableStrictBranch(propertySchema)) return true;
1368
+ if (isJsonObject(propertySchema) && hasUnrepresentableStrictObjectMap(propertySchema, epoch)) {
1369
+ return true;
1370
+ }
1371
+ }
1372
+ }
1373
+
1374
+ if (isUnrepresentableStrictBranch(schema.items)) {
1375
+ return true;
1376
+ }
1377
+ if (isJsonObject(schema.items)) {
1378
+ if (hasUnrepresentableStrictObjectMap(schema.items, epoch)) {
1379
+ return true;
1380
+ }
1381
+ } else if (Array.isArray(schema.items)) {
1382
+ for (const itemSchema of schema.items) {
1383
+ if (isUnrepresentableStrictBranch(itemSchema)) return true;
1384
+ if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1385
+ return true;
1386
+ }
1387
+ }
1388
+ }
1389
+ if (Array.isArray(schema.prefixItems)) {
1390
+ for (const itemSchema of schema.prefixItems) {
1391
+ if (isUnrepresentableStrictBranch(itemSchema)) return true;
1392
+ if (isJsonObject(itemSchema) && hasUnrepresentableStrictObjectMap(itemSchema, epoch)) {
1393
+ return true;
1394
+ }
1395
+ }
1396
+ }
1397
+
1398
+ for (const key of COMBINATOR_KEYS) {
1399
+ const variants = schema[key];
1400
+ if (!Array.isArray(variants)) continue;
1401
+ for (const variant of variants) {
1402
+ if (isUnrepresentableStrictBranch(variant)) return true;
1403
+ if (isJsonObject(variant) && hasUnrepresentableStrictObjectMap(variant, epoch)) {
1404
+ return true;
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ for (const defsKey of ["$defs", "definitions"] as const) {
1410
+ const defs = schema[defsKey];
1411
+ if (!isJsonObject(defs)) continue;
1412
+ for (const k in defs) {
1413
+ const defSchema = defs[k];
1414
+ if (isUnrepresentableStrictBranch(defSchema)) return true;
1415
+ if (isJsonObject(defSchema) && hasUnrepresentableStrictObjectMap(defSchema, epoch)) {
1416
+ return true;
1417
+ }
1418
+ }
1419
+ }
1420
+
1421
+ return false;
1422
+ }
1423
+
1424
+ /**
1425
+ * First pass of strict-mode preparation.
1426
+ *
1427
+ * Rewrites everything strict mode forbids into something it accepts:
1428
+ * - Drops non-structural keywords (`format`, `pattern`, `examples`, …),
1429
+ * `const`, `nullable`, and `additionalProperties` (re-added by
1430
+ * `enforceStrictSchema` as `false`).
1431
+ * - `type: [a, b]` → `anyOf: [{type: a, …}, {type: b, …}]`, copying only the
1432
+ * keywords each variant can use (e.g. `properties` stays only on the
1433
+ * object variant).
1434
+ * - `const` → single-entry `enum`.
1435
+ * - Description carries a `(default: X)` suffix so the model still sees the
1436
+ * documented default after the keyword is stripped.
1437
+ * - `nullable: true` wraps the whole node in `anyOf:[T,{type:"null"}]`.
1438
+ *
1439
+ * Recurses into properties, items, prefixItems, combinators, and $defs. The
1440
+ * `cache` WeakMap dedupes shared subgraphs; the `epoch` is the cycle guard.
1441
+ */
1442
+ export function sanitizeSchemaForStrictMode(
1443
+ schema: Record<string, unknown>,
1444
+ epoch: number = epochNext(),
1445
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
1446
+ root: Record<string, unknown> = schema,
1447
+ ): Record<string, unknown> {
1448
+ const cached = cache.get(schema);
1449
+ if (cached) return cached;
1450
+ if (!once(schema, epoch)) return {};
1451
+
1452
+ // Pre-pass: unravel `$ref` with sibling keys by inlining the resolved def.
1453
+ // OpenAI strict mode forbids `{$ref, description, ...}`; the SDK resolves
1454
+ // and merges, with sibling keys taking precedence over the ref'd def.
1455
+ // Cite: openai-python/src/openai/lib/_pydantic.py:96-110 (`_ensure_strict_json_schema`)
1456
+ if (typeof schema.$ref === "string") {
1457
+ let hasSibling = false;
1458
+ for (const k in schema) {
1459
+ if (k !== "$ref" && Object.hasOwn(schema, k)) {
1460
+ hasSibling = true;
1461
+ break;
1462
+ }
1463
+ }
1464
+ if (hasSibling) {
1465
+ const resolved = resolveStrictRef(root, schema.$ref);
1466
+ if (resolved !== undefined) {
1467
+ // Sibling keys on the schema override keys from the resolved def.
1468
+ const merged: Record<string, unknown> = { ...resolved };
1469
+ for (const k in schema) {
1470
+ if (k === "$ref" || !Object.hasOwn(schema, k)) continue;
1471
+ merged[k] = schema[k];
1472
+ }
1473
+ const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1474
+ cache.set(schema, result);
1475
+ return result;
1476
+ }
1477
+ }
1478
+ }
1479
+
1480
+ // Pre-pass: collapse single-element `allOf` by inlining its sole entry.
1481
+ // SDK semantics: `json_schema.update(ensured(all_of[0]))` — the inlined
1482
+ // entry's keys WIN over original sibling keys, then `allOf` is dropped.
1483
+ // Cite: openai-python/src/openai/lib/_pydantic.py:79-83
1484
+ {
1485
+ const allOf = schema.allOf;
1486
+ if (Array.isArray(allOf) && allOf.length === 1 && isJsonObject(allOf[0])) {
1487
+ const merged: Record<string, unknown> = { ...schema };
1488
+ delete merged.allOf;
1489
+ const sole = allOf[0] as Record<string, unknown>;
1490
+ for (const k in sole) {
1491
+ if (Object.hasOwn(sole, k)) merged[k] = sole[k];
1492
+ }
1493
+ const result = sanitizeSchemaForStrictMode(merged, epoch, cache, root);
1494
+ cache.set(schema, result);
1495
+ return result;
1496
+ }
1497
+ }
1498
+
1499
+ const typeValue = schema.type;
1500
+ if (Array.isArray(typeValue)) {
1501
+ const typeVariants = typeValue.filter((entry): entry is string => typeof entry === "string");
1502
+ const schemaWithoutType = { ...schema };
1503
+ delete schemaWithoutType.type;
1504
+
1505
+ const sanitizedWithoutType = sanitizeSchemaForStrictMode(schemaWithoutType, epoch, cache, root);
1506
+ if (typeVariants.length === 0) {
1507
+ cache.set(schema, sanitizedWithoutType);
1508
+ return sanitizedWithoutType;
1509
+ }
1510
+ // Build one variant schema per type. Each variant keeps only the keywords
1511
+ // relevant to that type — object-only keywords stay on the object variant,
1512
+ // array-only keywords on the array variant, etc.
1513
+ //
1514
+ // `description` is metadata that applies to the whole union, not to any
1515
+ // single type variant, so hoist it to the wrapper so both branches share
1516
+ // it without duplication. Matches the optional-property wrap in
1517
+ // `enforceStrictSchema` and the typical OpenAI strict-mode "description
1518
+ // on the union" shape.
1519
+ const { description, ...variantBase } = sanitizedWithoutType;
1520
+ const variants: Record<string, unknown>[] = [];
1521
+ for (const variantType of typeVariants) {
1522
+ const variantSchema: Record<string, unknown> = { ...variantBase, type: variantType };
1523
+ if (variantType !== "object") {
1524
+ delete variantSchema.properties;
1525
+ delete variantSchema.required;
1526
+ delete variantSchema.additionalProperties;
1527
+ }
1528
+ if (variantType !== "array") {
1529
+ delete variantSchema.items;
1530
+ }
1531
+ if (!narrowEnumToType(variantSchema, variantType)) continue;
1532
+ variants.push(sanitizeSchemaForStrictMode(variantSchema, epoch, cache, root));
1533
+ }
1534
+
1535
+ if (variants.length === 0) {
1536
+ cache.set(schema, sanitizedWithoutType);
1537
+ return sanitizedWithoutType;
1538
+ }
1539
+
1540
+ if (variants.length === 1) {
1541
+ const sole = variants[0] as Record<string, unknown>;
1542
+ if (description !== undefined && !Object.hasOwn(sole, "description")) {
1543
+ sole.description = description;
1544
+ }
1545
+ cache.set(schema, sole);
1546
+ return sole;
1547
+ }
1548
+
1549
+ const result: JsonObject = { anyOf: variants };
1550
+ if (description !== undefined) result.description = description;
1551
+ cache.set(schema, result);
1552
+ return result;
1553
+ }
1554
+ // Scalar `type`: walk the keys, rewriting or stripping per strict-mode rules.
1555
+
1556
+ const sanitized: Record<string, unknown> = {};
1557
+ cache.set(schema, sanitized);
1558
+ for (const key in schema) {
1559
+ const value = schema[key];
1560
+ if (key in NON_STRUCTURAL_SCHEMA_KEYS || key === "type" || key === "const" || key === "nullable") {
1561
+ continue;
1562
+ }
1563
+ // `properties` map — recurse into each property schema.
1564
+
1565
+ if (key === "properties" && isJsonObject(value)) {
1566
+ const properties: Record<string, unknown> = {};
1567
+ for (const propertyName in value) {
1568
+ const propertySchema = value[propertyName];
1569
+ properties[propertyName] = isJsonObject(propertySchema)
1570
+ ? sanitizeSchemaForStrictMode(propertySchema, epoch, cache, root)
1571
+ : propertySchema;
1572
+ }
1573
+ sanitized.properties = properties;
1574
+ continue;
1575
+ }
1576
+ // `items` can be schema, tuple-array, or scalar boolean — recurse where applicable.
1577
+
1578
+ if (key === "items") {
1579
+ if (isJsonObject(value)) {
1580
+ sanitized.items = sanitizeSchemaForStrictMode(value, epoch, cache, root);
1581
+ } else if (Array.isArray(value)) {
1582
+ sanitized.items = value.map(entry =>
1583
+ isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1584
+ );
1585
+ } else {
1586
+ sanitized.items = value;
1587
+ }
1588
+ continue;
1589
+ }
1590
+ // `prefixItems` is always an array of schemas (draft 2020-12).
1591
+
1592
+ if (key === "prefixItems" && Array.isArray(value)) {
1593
+ sanitized.prefixItems = value.map(entry =>
1594
+ isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1595
+ );
1596
+ continue;
1597
+ }
1598
+ // `anyOf`/`oneOf`/`allOf` arrays — recurse into each branch.
1599
+
1600
+ if (COMBINATOR_KEYS.includes(key as (typeof COMBINATOR_KEYS)[number]) && Array.isArray(value)) {
1601
+ sanitized[key] = value.map(entry =>
1602
+ isJsonObject(entry) ? sanitizeSchemaForStrictMode(entry, epoch, cache, root) : entry,
1603
+ );
1604
+ continue;
1605
+ }
1606
+ // Definition maps — recurse into each named schema.
1607
+
1608
+ if ((key === "$defs" || key === "definitions") && isJsonObject(value)) {
1609
+ const defs: Record<string, unknown> = {};
1610
+ for (const definitionName in value) {
1611
+ const definitionSchema = value[definitionName];
1612
+ defs[definitionName] = isJsonObject(definitionSchema)
1613
+ ? sanitizeSchemaForStrictMode(definitionSchema, epoch, cache, root)
1614
+ : definitionSchema;
1615
+ }
1616
+ sanitized[key] = defs;
1617
+ continue;
1618
+ }
1619
+ // `additionalProperties` is owned by `enforceStrictSchema`, which sets it to false.
1620
+
1621
+ if (key === "additionalProperties") {
1622
+ continue;
1623
+ }
1624
+
1625
+ if (key === "description" && typeof value === "string" && schema.default !== undefined) {
1626
+ // Preserve `default:` info for strict-mode providers that strip the keyword.
1627
+ // Inline as `(default: X)` text in the description, matching the convention for
1628
+ // runtime-placeholder defaults (e.g. `cwd`) that cannot live in the keyword form.
1629
+ const defaultVal = schema.default;
1630
+ const formatted = typeof defaultVal === "string" ? defaultVal : JSON.stringify(defaultVal);
1631
+ sanitized.description = value.includes("(default:") ? value : `${value} (default: ${formatted})`;
1632
+ continue;
1633
+ }
1634
+
1635
+ sanitized[key] = value;
1636
+ }
1637
+ // Post-pass: re-derive `type` and turn dropped keywords into a representable shape.
1638
+
1639
+ if (Object.hasOwn(schema, "const")) {
1640
+ const constVal = schema.const;
1641
+ const existingEnum = Array.isArray(sanitized.enum) ? sanitized.enum : [];
1642
+ if (!existingEnum.some(v => areJsonValuesEqual(v, constVal))) {
1643
+ existingEnum.push(constVal);
1644
+ }
1645
+ sanitized.enum = existingEnum;
1646
+ }
1647
+
1648
+ // Preserve the original scalar type after the strip-and-rebuild loop.
1649
+ if (typeof typeValue === "string") {
1650
+ sanitized.type = typeValue;
1651
+ }
1652
+
1653
+ if (sanitized.type === undefined && isJsonObject(sanitized.properties)) {
1654
+ sanitized.type = "object";
1655
+ }
1656
+
1657
+ if (sanitized.type === undefined && (sanitized.items !== undefined || sanitized.prefixItems !== undefined)) {
1658
+ sanitized.type = "array";
1659
+ }
1660
+
1661
+ // Last-resort inference: a bare `enum`/`const` with homogeneous primitives gets a `type`.
1662
+ if (sanitized.type === undefined) {
1663
+ const inferred = inferStrictPrimitiveTypeFromEnumOrConst(sanitized);
1664
+ if (inferred !== undefined) sanitized.type = inferred;
1665
+ }
1666
+
1667
+ // `nullable: true` was stripped above — re-introduce it as an `anyOf` wrapper.
1668
+ // `description` hoists to the wrapper so both branches share it without
1669
+ // duplication — matches the optional-property wrap in `enforceStrictSchema`
1670
+ // and the typical OpenAI strict-mode "description on the union" shape.
1671
+ if (schema.nullable === true) {
1672
+ const { nullable: _, description, ...withoutNullable } = sanitized;
1673
+ const wrapper: JsonObject = { anyOf: [withoutNullable, { type: "null" }] };
1674
+ if (description !== undefined) wrapper.description = description;
1675
+ return wrapper;
1676
+ }
1677
+
1678
+ return sanitized;
1679
+ }
1680
+
1681
+ /**
1682
+ * A node whose only constraining keyword is `anyOf` (annotations like
1683
+ * `description` aside). Only such nodes can be merged into an enclosing
1684
+ * union without changing semantics: sibling keywords (`type`, `enum`,
1685
+ * `properties`, …) apply conjunctively with `anyOf`, so spreading the
1686
+ * branches of a non-pure node would drop those constraints.
1687
+ */
1688
+ function isPureAnyOfNode(value: unknown): value is Record<string, unknown> & { anyOf: unknown[] } {
1689
+ if (!isJsonObject(value) || !Array.isArray(value.anyOf)) return false;
1690
+ for (const key in value) {
1691
+ if (key !== "anyOf" && key !== "description") return false;
1692
+ }
1693
+ return true;
1694
+ }
1695
+
1696
+ /**
1697
+ * Recursively enforces JSON Schema constraints required by OpenAI/Codex strict mode:
1698
+ * - `additionalProperties: false` on every object node
1699
+ * - every key in `properties` present in `required`
1700
+ *
1701
+ * Properties absent from the original `required` array were TypeBox-optional.
1702
+ * They are made nullable (`anyOf: [T, { type: "null" }]`) so the model can
1703
+ * signal omission by outputting null rather than omitting the key entirely.
1704
+ *
1705
+ * @throws {Error} When a schema node has no `type`, array-based combinator
1706
+ * (`anyOf`/`allOf`/`oneOf`), object-based combinator (`not`), or `$ref` —
1707
+ * i.e. the node is not representable in strict mode. Prefer
1708
+ * {@link tryEnforceStrictSchema} which catches this and degrades gracefully.
1709
+ */
1710
+ export function enforceStrictSchema(
1711
+ schema: Record<string, unknown>,
1712
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
1713
+ ): Record<string, unknown> {
1714
+ if (!enter(schema)) {
1715
+ throw new AIError.ValidationError("Schema contains a circular object graph — cannot enforce strict mode");
1716
+ }
1717
+ try {
1718
+ const cached = cache.get(schema);
1719
+ if (cached) return cached;
1720
+ const result = { ...schema };
1721
+ cache.set(schema, result);
1722
+ return enforceStrictSchemaBody(schema, result, cache);
1723
+ } finally {
1724
+ exit(schema);
1725
+ }
1726
+ }
1727
+
1728
+ function enforceStrictSchemaBody(
1729
+ _schema: Record<string, unknown>,
1730
+ result: Record<string, unknown>,
1731
+ cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
1732
+ ): Record<string, unknown> {
1733
+ const isObjectType = result.type === "object";
1734
+ if (isObjectType) {
1735
+ result.additionalProperties = false;
1736
+ const propertiesValue = result.properties;
1737
+ const props =
1738
+ propertiesValue != null && typeof propertiesValue === "object" && !Array.isArray(propertiesValue)
1739
+ ? (propertiesValue as Record<string, unknown>)
1740
+ : {};
1741
+ const originalRequired = new Set<string>(
1742
+ Array.isArray(result.required)
1743
+ ? result.required.filter((value): value is string => typeof value === "string")
1744
+ : [],
1745
+ );
1746
+ const strictProperties: Record<string, unknown> = {};
1747
+ for (const key in props) {
1748
+ const value = props[key];
1749
+ const processed =
1750
+ value != null && typeof value === "object" && !Array.isArray(value)
1751
+ ? enforceStrictSchema(value as Record<string, unknown>, cache)
1752
+ : value;
1753
+ // Optional property — wrap as nullable so strict mode accepts it
1754
+ if (!originalRequired.has(key)) {
1755
+ // Don't double-wrap if already nullable
1756
+ if (
1757
+ isJsonObject(processed) &&
1758
+ Array.isArray(processed.anyOf) &&
1759
+ processed.anyOf.some(v => isJsonObject(v) && v.type === "null")
1760
+ ) {
1761
+ strictProperties[key] = processed;
1762
+ continue;
1763
+ }
1764
+ if (isPureAnyOfNode(processed)) {
1765
+ strictProperties[key] = { ...processed, anyOf: [...processed.anyOf, { type: "null" }] };
1766
+ continue;
1767
+ }
1768
+ if (isJsonObject(processed) && typeof processed.description === "string") {
1769
+ const { description, ...withoutDescription } = processed;
1770
+ strictProperties[key] = { anyOf: [withoutDescription, { type: "null" }], description };
1771
+ continue;
1772
+ }
1773
+ strictProperties[key] = { anyOf: [processed, { type: "null" }] };
1774
+ continue;
1775
+ }
1776
+ strictProperties[key] = processed;
1777
+ }
1778
+ result.properties = strictProperties;
1779
+ result.required = Object.keys(strictProperties);
1780
+ }
1781
+ if (result.items != null && typeof result.items === "object") {
1782
+ if (Array.isArray(result.items)) {
1783
+ result.items = result.items.map(entry =>
1784
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1785
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1786
+ : entry,
1787
+ );
1788
+ } else {
1789
+ result.items = enforceStrictSchema(result.items as Record<string, unknown>, cache);
1790
+ }
1791
+ }
1792
+ if (Array.isArray(result.prefixItems)) {
1793
+ result.prefixItems = result.prefixItems.map(entry =>
1794
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1795
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1796
+ : entry,
1797
+ );
1798
+ }
1799
+ for (const key of COMBINATOR_KEYS) {
1800
+ if (Array.isArray(result[key])) {
1801
+ result[key] = (result[key] as unknown[]).map(entry =>
1802
+ entry != null && typeof entry === "object" && !Array.isArray(entry)
1803
+ ? enforceStrictSchema(entry as Record<string, unknown>, cache)
1804
+ : entry,
1805
+ );
1806
+ }
1807
+ }
1808
+ // Splice nested pure unions into the parent `anyOf`: `(A ∨ B) ∨ C` ≡ `A ∨ B ∨ C`.
1809
+ // Some strict-mode validators (e.g. DeepSeek behind OpenRouter) reject anyOf
1810
+ // branches that carry no `type`, which is exactly what a nested combinator
1811
+ // node looks like (#2270). Branch recursion above already flattened deeper
1812
+ // levels bottom-up, so a single pass suffices.
1813
+ if (Array.isArray(result.anyOf) && result.anyOf.some(isPureAnyOfNode)) {
1814
+ const flattened: unknown[] = [];
1815
+ for (const branch of result.anyOf) {
1816
+ if (!isPureAnyOfNode(branch)) {
1817
+ flattened.push(branch);
1818
+ continue;
1819
+ }
1820
+ flattened.push(...branch.anyOf);
1821
+ // Keep the inner annotation when the parent has none.
1822
+ if (typeof branch.description === "string" && result.description === undefined) {
1823
+ result.description = branch.description;
1824
+ }
1825
+ }
1826
+ result.anyOf = flattened;
1827
+ }
1828
+ for (const defsKey of ["$defs", "definitions"] as const) {
1829
+ if (result[defsKey] != null && typeof result[defsKey] === "object" && !Array.isArray(result[defsKey])) {
1830
+ const defs = result[defsKey] as Record<string, unknown>;
1831
+ const nextDefs: Record<string, unknown> = {};
1832
+ for (const name in defs) {
1833
+ const def = defs[name];
1834
+ nextDefs[name] =
1835
+ def != null && typeof def === "object" && !Array.isArray(def)
1836
+ ? enforceStrictSchema(def as Record<string, unknown>, cache)
1837
+ : def;
1838
+ }
1839
+ result[defsKey] = nextDefs;
1840
+ }
1841
+ }
1842
+ // Strict mode requires every schema node to declare a concrete type (or
1843
+ // combinator / `$ref` / `not`). When `type` is missing, try to infer it
1844
+ // from a homogeneous-primitive `enum` / `const` so direct calls to
1845
+ // `enforceStrictSchema` (which bypass `sanitizeSchemaForStrictMode`'s own
1846
+ // inference pass) still produce wire-valid output.
1847
+ if (result.type === undefined) {
1848
+ const inferred = inferStrictPrimitiveTypeFromEnumOrConst(result);
1849
+ if (inferred !== undefined) result.type = inferred;
1850
+ }
1851
+ // Schemas like `{}`, `{items: {}}`, mixed-primitive enums, and non-primitive
1852
+ // consts are not representable in strict mode — `enum`/`const` are not
1853
+ // accepted as type substitutes here because they did not yield a single
1854
+ // inferable type above.
1855
+ if (
1856
+ result.type === undefined &&
1857
+ result.$ref === undefined &&
1858
+ !COMBINATOR_KEYS.some(key => Array.isArray(result[key])) &&
1859
+ !isJsonObject(result.not)
1860
+ ) {
1861
+ throw new AIError.ValidationError("Schema node has no type, combinator, or $ref — cannot enforce strict mode");
1862
+ }
1863
+ return result;
1864
+ }
1865
+
1866
+ export function tryEnforceStrictSchema(schema: Record<string, unknown>): {
1867
+ schema: Record<string, unknown>;
1868
+ strict: boolean;
1869
+ } {
1870
+ return stamp(schema, kStrictSchema, s => {
1871
+ const upgraded = upgradeJsonSchemaTo202012(s) as Record<string, unknown>;
1872
+ if (hasUnrepresentableStrictObjectMap(upgraded)) {
1873
+ return { schema: upgraded, strict: false };
1874
+ }
1875
+ try {
1876
+ const sanitized = sanitizeSchemaForStrictMode(upgraded);
1877
+ return { schema: enforceStrictSchema(sanitized), strict: true };
1878
+ } catch {
1879
+ return { schema: upgraded, strict: false };
1880
+ }
1881
+ });
1882
+ }
1883
+
1884
+ /**
1885
+ * Resolve a JSON-pointer-style `$ref` against the root schema. Mirrors the
1886
+ * OpenAI SDK's `resolve_ref` helper: only local refs starting with `#/` are
1887
+ * supported, and each segment must dereference to a dictionary.
1888
+ * Cite: openai-python/src/openai/lib/_pydantic.py:118-129
1889
+ */
1890
+ function resolveStrictRef(root: Record<string, unknown>, ref: string): Record<string, unknown> | undefined {
1891
+ if (!ref.startsWith("#/")) return undefined;
1892
+ const segments = ref.slice(2).split("/");
1893
+ let cursor: unknown = root;
1894
+ for (const raw of segments) {
1895
+ if (!isJsonObject(cursor)) return undefined;
1896
+ // JSON Pointer unescape: ~1 → "/", ~0 → "~" (must run in that order).
1897
+ const segment = raw.replace(/~1/g, "/").replace(/~0/g, "~");
1898
+ cursor = cursor[segment];
1899
+ }
1900
+ return isJsonObject(cursor) ? cursor : undefined;
1901
+ }