@linxiraos/pi-ai 1.0.0

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