@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,1932 @@
1
+ /**
2
+ * Tool-call argument validation pipeline.
3
+ *
4
+ * Tools may declare ArkType schemas or plain JSON Schema. This module builds a
5
+ * cached validation context, normalizes common LLM quirks against the wire
6
+ * schema, validates, performs conservative schema-directed coercions, and
7
+ * returns parsed arguments while preserving unknown root fields.
8
+ *
9
+ * The goal is to be conservative: every coercion is a structural rewrite that
10
+ * keeps the schema in charge of acceptance — we never invent values, only
11
+ * massage shapes the LLM almost got right.
12
+ */
13
+
14
+ import { type Type, type } from "@linxiraos/pi-omptype";
15
+ import { structuredCloneJSON } from "@linxiraos/pi-utils";
16
+ import * as AIError from "../error";
17
+ import type { Tool, ToolCall } from "../types";
18
+ import { upgradeJsonSchemaTo202012 } from "./schema/draft";
19
+ import {
20
+ isJsonSchemaValueValid,
21
+ type JsonSchemaValidationIssue,
22
+ validateJsonSchemaValue,
23
+ } from "./schema/json-schema-validator";
24
+ import { stamp } from "./schema/stamps";
25
+ import { arkToWireSchema, isArkSchema } from "./schema/wire";
26
+
27
+ // ============================================================================
28
+ // Type Coercion Utilities
29
+ // ============================================================================
30
+ //
31
+ // LLMs sometimes produce tool arguments where a value has the right meaning but
32
+ // the wrong JSON type. For example, an array parameter might arrive as
33
+ // `"[1, 2, 3]"`, a boolean as `"yes"` or `1`, or a string field as a structured
34
+ // object that should be embedded verbatim.
35
+ //
36
+ // Rather than rejecting these outright, validate against the declared schema
37
+ // and perform only schema-directed rewrites for reported type errors.
38
+ //
39
+ // This is intentionally conservative: each rewrite is small and validation
40
+ // remains the source of truth for whether the result is accepted.
41
+ // ============================================================================
42
+
43
+ /** Regex matching valid JSON number literals (integers, decimals, scientific notation) */
44
+ const JSON_NUMBER_PATTERN = /^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
45
+
46
+ /** Regex matching numeric strings (allows leading zeros) */
47
+ const NUMERIC_STRING_PATTERN = /^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
48
+
49
+ /**
50
+ * Checks if a value matches any of the expected JSON Schema types.
51
+ * Used to verify that a parsed JSON value is actually what the schema wants.
52
+ */
53
+ function matchesExpectedType(value: unknown, expectedTypes: string[]): boolean {
54
+ return expectedTypes.some(type => {
55
+ switch (type) {
56
+ case "string":
57
+ return typeof value === "string";
58
+ case "number":
59
+ return typeof value === "number" && Number.isFinite(value);
60
+ case "integer":
61
+ return typeof value === "number" && Number.isInteger(value);
62
+ case "boolean":
63
+ return typeof value === "boolean";
64
+ case "null":
65
+ return value === null;
66
+ case "array":
67
+ return Array.isArray(value);
68
+ case "object":
69
+ return value !== null && typeof value === "object" && !Array.isArray(value);
70
+ default:
71
+ return false;
72
+ }
73
+ });
74
+ }
75
+
76
+ function tryParseNumberString(value: string, expectedTypes: string[]): { value: unknown; changed: boolean } {
77
+ if (!expectedTypes.includes("number") && !expectedTypes.includes("integer")) {
78
+ return { value, changed: false };
79
+ }
80
+
81
+ const trimmed = value.trim();
82
+ if (!trimmed || !NUMERIC_STRING_PATTERN.test(trimmed)) {
83
+ return { value, changed: false };
84
+ }
85
+
86
+ const parsed = Number(trimmed);
87
+ if (!Number.isFinite(parsed)) {
88
+ return { value, changed: false };
89
+ }
90
+
91
+ if (!matchesExpectedType(parsed, expectedTypes)) {
92
+ return { value, changed: false };
93
+ }
94
+
95
+ return { value: parsed, changed: true };
96
+ }
97
+
98
+ function tryCoerceBoolean(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
99
+ if (!expectedTypes.includes("boolean")) {
100
+ return { value, changed: false };
101
+ }
102
+
103
+ if (typeof value === "number") {
104
+ if (value === 0) return { value: false, changed: true };
105
+ if (value === 1) return { value: true, changed: true };
106
+ return { value, changed: false };
107
+ }
108
+
109
+ if (typeof value !== "string") {
110
+ return { value, changed: false };
111
+ }
112
+
113
+ switch (value.trim().toLowerCase()) {
114
+ case "true":
115
+ case "1":
116
+ case "yes":
117
+ case "on":
118
+ return { value: true, changed: true };
119
+ case "false":
120
+ case "0":
121
+ case "no":
122
+ case "off":
123
+ return { value: false, changed: true };
124
+ default:
125
+ return { value, changed: false };
126
+ }
127
+ }
128
+
129
+ function tryCoerceBooleanToNumber(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
130
+ if (!expectedTypes.includes("number") && !expectedTypes.includes("integer")) {
131
+ return { value, changed: false };
132
+ }
133
+ if (typeof value !== "boolean") {
134
+ return { value, changed: false };
135
+ }
136
+ return { value: value ? 1 : 0, changed: true };
137
+ }
138
+
139
+ function tryCoerceString(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
140
+ if (!expectedTypes.includes("string") || typeof value === "string" || value === null || value === undefined) {
141
+ return { value, changed: false };
142
+ }
143
+
144
+ if (Array.isArray(value) || typeof value === "object") {
145
+ try {
146
+ const stringified = JSON.stringify(value);
147
+ if (stringified === undefined) return { value, changed: false };
148
+ return { value: stringified, changed: true };
149
+ } catch {
150
+ return { value, changed: false };
151
+ }
152
+ }
153
+
154
+ if (typeof value === "function") {
155
+ return { value, changed: false };
156
+ }
157
+
158
+ return { value: String(value), changed: true };
159
+ }
160
+
161
+ function tryCoerceForExpectedTypes(value: unknown, expectedTypes: string[]): { value: unknown; changed: boolean } {
162
+ if (typeof value === "string") {
163
+ const parsed = tryParseJsonForTypes(value, expectedTypes);
164
+ if (parsed.changed) return parsed;
165
+ return tryCoerceBoolean(value, expectedTypes);
166
+ }
167
+
168
+ const booleanCoercion = tryCoerceBoolean(value, expectedTypes);
169
+ if (booleanCoercion.changed) return booleanCoercion;
170
+
171
+ const numericCoercion = tryCoerceBooleanToNumber(value, expectedTypes);
172
+ if (numericCoercion.changed) return numericCoercion;
173
+
174
+ return tryCoerceString(value, expectedTypes);
175
+ }
176
+
177
+ function tryParseLeadingJsonContainer(value: string): unknown | undefined {
178
+ const firstChar = value[0];
179
+ const closingChar = firstChar === "{" ? "}" : firstChar === "[" ? "]" : undefined;
180
+ if (!closingChar) return undefined;
181
+
182
+ let depth = 0;
183
+ let inString = false;
184
+ let escaped = false;
185
+
186
+ for (let index = 0; index < value.length; index += 1) {
187
+ const char = value[index];
188
+
189
+ if (inString) {
190
+ if (escaped) {
191
+ escaped = false;
192
+ continue;
193
+ }
194
+ if (char === "\\") {
195
+ escaped = true;
196
+ continue;
197
+ }
198
+ if (char === '"') inString = false;
199
+ continue;
200
+ }
201
+
202
+ if (char === '"') {
203
+ inString = true;
204
+ continue;
205
+ }
206
+
207
+ if (char === firstChar) {
208
+ depth += 1;
209
+ continue;
210
+ }
211
+
212
+ if (char !== closingChar) continue;
213
+ depth -= 1;
214
+ if (depth !== 0) continue;
215
+
216
+ const prefix = value.slice(0, index + 1);
217
+ try {
218
+ return JSON.parse(prefix) as unknown;
219
+ } catch {
220
+ // LLMs sometimes emit literal `\n` or `\t` between JSON tokens
221
+ // (e.g. `[{...}\n]`). Convert these to real whitespace and retry.
222
+ const cleaned = cleanLiteralEscapes(prefix);
223
+ if (cleaned !== prefix) {
224
+ try {
225
+ return JSON.parse(cleaned) as unknown;
226
+ } catch {}
227
+ }
228
+ // Try escaping raw control chars that appear inside string literals.
229
+ const escapedControls = escapeRawControlsInJsonStrings(prefix);
230
+ if (escapedControls !== prefix) {
231
+ try {
232
+ return JSON.parse(escapedControls) as unknown;
233
+ } catch {}
234
+ }
235
+ // Also try single-char healing on the extracted prefix.
236
+ return tryHealMalformedJson(prefix);
237
+ }
238
+ }
239
+
240
+ return undefined;
241
+ }
242
+
243
+ /**
244
+ * Replace literal `\n`, `\t`, `\r` sequences that appear OUTSIDE of JSON
245
+ * strings with actual whitespace. LLMs sometimes produce these when they
246
+ * confuse the tool-call encoding with the content encoding.
247
+ */
248
+ function cleanLiteralEscapes(value: string): string {
249
+ let result = "";
250
+ let inString = false;
251
+ let i = 0;
252
+ while (i < value.length) {
253
+ const ch = value[i];
254
+ if (inString) {
255
+ if (ch === "\\" && i + 1 < value.length) {
256
+ result += ch + value[i + 1];
257
+ i += 2;
258
+ continue;
259
+ }
260
+ if (ch === '"') inString = false;
261
+ result += ch;
262
+ i += 1;
263
+ continue;
264
+ }
265
+ if (ch === '"') {
266
+ inString = true;
267
+ result += ch;
268
+ i += 1;
269
+ continue;
270
+ }
271
+ // Outside a string: replace literal \n, \t, \r with whitespace
272
+ if (ch === "\\" && i + 1 < value.length) {
273
+ const next = value[i + 1];
274
+ if (next === "n" || next === "t" || next === "r") {
275
+ result += " ";
276
+ i += 2;
277
+ continue;
278
+ }
279
+ }
280
+ result += ch;
281
+ i += 1;
282
+ }
283
+ return result;
284
+ }
285
+
286
+ /**
287
+ * Escape raw control characters (0x00–0x1F) that appear *inside* JSON string
288
+ * literals. LLMs sometimes emit literal newlines/tabs/etc. inside string
289
+ * content instead of `\n` / `\t` escape sequences, which `JSON.parse` rejects
290
+ * even though the surrounding structure is valid.
291
+ *
292
+ * This function only rewrites characters while inside a string; structural
293
+ * whitespace outside of strings is preserved unchanged.
294
+ */
295
+ function escapeRawControlsInJsonStrings(value: string): string {
296
+ let result = "";
297
+ let inString = false;
298
+ let escaped = false;
299
+ let changed = false;
300
+ for (let i = 0; i < value.length; i += 1) {
301
+ const ch = value[i];
302
+ if (inString) {
303
+ if (escaped) {
304
+ result += ch;
305
+ escaped = false;
306
+ continue;
307
+ }
308
+ if (ch === "\\") {
309
+ result += ch;
310
+ escaped = true;
311
+ continue;
312
+ }
313
+ if (ch === '"') {
314
+ result += ch;
315
+ inString = false;
316
+ continue;
317
+ }
318
+ const code = ch.charCodeAt(0);
319
+ if (code < 0x20) {
320
+ changed = true;
321
+ switch (ch) {
322
+ case "\n":
323
+ result += "\\n";
324
+ break;
325
+ case "\r":
326
+ result += "\\r";
327
+ break;
328
+ case "\t":
329
+ result += "\\t";
330
+ break;
331
+ case "\b":
332
+ result += "\\b";
333
+ break;
334
+ case "\f":
335
+ result += "\\f";
336
+ break;
337
+ default:
338
+ result += `\\u${code.toString(16).padStart(4, "0")}`;
339
+ }
340
+ continue;
341
+ }
342
+ result += ch;
343
+ continue;
344
+ }
345
+ if (ch === '"') {
346
+ inString = true;
347
+ }
348
+ result += ch;
349
+ }
350
+ return changed ? result : value;
351
+ }
352
+
353
+ /** Maximum single-character edits to attempt when healing malformed JSON. */
354
+ const MAX_HEAL_DISTANCE = 3;
355
+ const BRACKET_CHARS = ["[", "]", "{", "}"] as const;
356
+
357
+ /**
358
+ * Attempts to heal near-valid JSON by applying single-character edits near the
359
+ * end of the string. LLMs (especially smaller ones) sometimes produce JSON with
360
+ * a single misplaced, extra, or wrong bracket at the end — e.g. `"}]"` becomes
361
+ * `"]}"` or gets an extra `}` appended. This function tries:
362
+ * 1. Removing a single character from the last few positions
363
+ * 2. Replacing a single character in the last few positions with each bracket type
364
+ *
365
+ * Returns the parsed value on success, undefined on failure.
366
+ */
367
+ function tryHealMalformedJson(value: string): unknown | undefined {
368
+ // Verify it actually fails to parse
369
+ try {
370
+ return JSON.parse(value) as unknown;
371
+ } catch {}
372
+
373
+ // Only attempt edits within the last few characters — the error is always
374
+ // a bracket issue at the tail for the class of LLM mistakes this targets.
375
+ const tailStart = Math.max(0, value.length - (MAX_HEAL_DISTANCE * 2 + 1));
376
+
377
+ // Strategy 1: remove a single character from the tail
378
+ for (let i = tailStart; i < value.length; i += 1) {
379
+ const candidate = value.slice(0, i) + value.slice(i + 1);
380
+ try {
381
+ return JSON.parse(candidate) as unknown;
382
+ } catch {}
383
+ }
384
+
385
+ // Strategy 2: replace a single character in the tail with each bracket type
386
+ for (let i = tailStart; i < value.length; i += 1) {
387
+ const original = value[i];
388
+ for (const replacement of BRACKET_CHARS) {
389
+ if (replacement === original) continue;
390
+ const candidate = value.slice(0, i) + replacement + value.slice(i + 1);
391
+ try {
392
+ return JSON.parse(candidate) as unknown;
393
+ } catch {}
394
+ }
395
+ }
396
+
397
+ return undefined;
398
+ }
399
+
400
+ const MAX_NESTED_JSON_STRING_PARSE_DEPTH = 3;
401
+
402
+ function acceptParsedJsonForTypes(
403
+ parsed: unknown,
404
+ source: string,
405
+ expectedTypes: string[],
406
+ depth: number,
407
+ ): { value: unknown; changed: boolean } {
408
+ if (parsed === null && source.trim() === "null") {
409
+ return { value: null, changed: true };
410
+ }
411
+ if (matchesExpectedType(parsed, expectedTypes)) {
412
+ return { value: parsed, changed: true };
413
+ }
414
+ if (typeof parsed === "string" && !expectedTypes.includes("string") && depth < MAX_NESTED_JSON_STRING_PARSE_DEPTH) {
415
+ return tryParseJsonForTypes(parsed, expectedTypes, depth + 1);
416
+ }
417
+ return { value: source, changed: false };
418
+ }
419
+
420
+ function looksLikeJsonContainerString(value: unknown): boolean {
421
+ if (typeof value !== "string") return false;
422
+ const trimmed = value.trimStart();
423
+ if (trimmed.startsWith("{")) {
424
+ const body = trimmed.slice(1);
425
+ return body.trimStart().startsWith('"') || body.includes(":") || body.trimStart().startsWith("}");
426
+ }
427
+ if (!trimmed.startsWith("[")) return false;
428
+ const firstItem = trimmed.slice(1).trimStart();
429
+ return (
430
+ firstItem.startsWith("{") ||
431
+ firstItem.startsWith("[") ||
432
+ firstItem.startsWith('"') ||
433
+ firstItem.startsWith("]") ||
434
+ firstItem.startsWith("true") ||
435
+ firstItem.startsWith("false") ||
436
+ firstItem.startsWith("null") ||
437
+ /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?(?:\s*(?:,|\]|$))/.test(firstItem)
438
+ );
439
+ }
440
+
441
+ /**
442
+ * Attempts to parse a string as JSON if it looks like a JSON literal and
443
+ * the parsed result matches one of the expected types.
444
+ *
445
+ * Only attempts parsing for strings that syntactically look like JSON:
446
+ * - Objects: `{...}`
447
+ * - Arrays: `[...]`
448
+ * - Literals: `true`, `false`, `null`, or numeric strings
449
+ *
450
+ * Returns `{ changed: true }` only if parsing succeeded AND the result
451
+ * matches an expected type. This prevents false positives like parsing
452
+ * the string `"123"` when the schema actually wants a string.
453
+ */
454
+ function tryParseJsonForTypes(value: string, expectedTypes: string[], depth = 0): { value: unknown; changed: boolean } {
455
+ const trimmed = value.trim();
456
+ if (!trimmed) return { value, changed: false };
457
+
458
+ const numberCoercion = tryParseNumberString(trimmed, expectedTypes);
459
+ if (numberCoercion.changed) {
460
+ return numberCoercion;
461
+ }
462
+
463
+ // Quick syntactic checks to avoid unnecessary parse attempts
464
+ const looksJsonObject = trimmed.startsWith("{") && looksLikeJsonContainerString(trimmed);
465
+ const looksJsonArray = trimmed.startsWith("[") && looksLikeJsonContainerString(trimmed);
466
+ const looksJsonString = trimmed.startsWith('"') && !expectedTypes.includes("string");
467
+ const looksJsonLiteral =
468
+ trimmed === "true" || trimmed === "false" || trimmed === "null" || JSON_NUMBER_PATTERN.test(trimmed);
469
+
470
+ if (!looksJsonObject && !looksJsonArray && !looksJsonString && !looksJsonLiteral) {
471
+ return { value, changed: false };
472
+ }
473
+
474
+ try {
475
+ const parsed = JSON.parse(trimmed) as unknown;
476
+ const accepted = acceptParsedJsonForTypes(parsed, trimmed, expectedTypes, depth);
477
+ if (accepted.changed) return accepted;
478
+ } catch {
479
+ if (looksJsonObject || looksJsonArray) {
480
+ // Try escaping raw control chars inside string literals (LLMs sometimes
481
+ // emit literal newlines/tabs inside string content rather than `\n`/`\t`).
482
+ const escapedControls = escapeRawControlsInJsonStrings(trimmed);
483
+ if (escapedControls !== trimmed) {
484
+ try {
485
+ const parsed = JSON.parse(escapedControls) as unknown;
486
+ const accepted = acceptParsedJsonForTypes(parsed, escapedControls, expectedTypes, depth);
487
+ if (accepted.changed) return accepted;
488
+ } catch {}
489
+ }
490
+ // Try extracting a valid JSON prefix (handles trailing junk after balanced container)
491
+ const leading = tryParseLeadingJsonContainer(trimmed);
492
+ if (leading !== undefined) {
493
+ const accepted = acceptParsedJsonForTypes(leading, trimmed, expectedTypes, depth);
494
+ if (accepted.changed) return accepted;
495
+ }
496
+ // Try healing single-character bracket errors near the end of the string
497
+ const healed = tryHealMalformedJson(trimmed);
498
+ if (healed !== undefined) {
499
+ const accepted = acceptParsedJsonForTypes(healed, trimmed, expectedTypes, depth);
500
+ if (accepted.changed) return accepted;
501
+ }
502
+ }
503
+ return { value, changed: false };
504
+ }
505
+
506
+ return { value, changed: false };
507
+ }
508
+
509
+ // ============================================================================
510
+ // JSON Pointer Utilities (RFC 6901)
511
+ // ============================================================================
512
+ //
513
+ // Error locations use JSON Pointer syntax so coercion can read and write
514
+ // validator-reported paths uniformly.
515
+ // ============================================================================
516
+
517
+ /** Encode a structured issue path as a JSON Pointer. */
518
+ function pathToPointer(path: ReadonlyArray<PropertyKey>): string {
519
+ if (path.length === 0) return "";
520
+ return `/${path.map(seg => String(seg).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
521
+ }
522
+
523
+ /**
524
+ * Decodes a JSON Pointer string into path segments.
525
+ * Handles RFC 6901 escape sequences: ~1 -> /, ~0 -> ~
526
+ */
527
+ function decodeJsonPointer(pointer: string): string[] {
528
+ if (!pointer) return [];
529
+ return pointer
530
+ .split("/")
531
+ .slice(1) // Remove leading empty segment from initial "/"
532
+ .map(segment => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
533
+ }
534
+
535
+ /**
536
+ * Retrieves a value from a nested object/array structure using a JSON Pointer.
537
+ * Returns undefined if the path doesn't exist or traversal fails.
538
+ */
539
+ function getValueAtPointer(root: unknown, pointer: string): unknown {
540
+ if (!pointer) return root;
541
+ const segments = decodeJsonPointer(pointer);
542
+ let current: unknown = root;
543
+
544
+ for (const segment of segments) {
545
+ if (current === null || current === undefined) return undefined;
546
+ if (Array.isArray(current)) {
547
+ const index = Number(segment);
548
+ if (!Number.isInteger(index)) return undefined;
549
+ current = current[index];
550
+ continue;
551
+ }
552
+ if (typeof current !== "object") return undefined;
553
+ current = (current as Record<string, unknown>)[segment];
554
+ }
555
+
556
+ return current;
557
+ }
558
+
559
+ /**
560
+ * Sets a value in a nested object/array structure using a JSON Pointer.
561
+ * Mutates the structure in-place. Returns the root (possibly unchanged if
562
+ * the path was invalid).
563
+ */
564
+ function setValueAtPointer(root: unknown, pointer: string, value: unknown): unknown {
565
+ if (!pointer) return value;
566
+ const segments = decodeJsonPointer(pointer);
567
+ let current: unknown = root;
568
+
569
+ // Navigate to the parent of the target location
570
+ for (let index = 0; index < segments.length - 1; index += 1) {
571
+ const segment = segments[index];
572
+ if (current === null || current === undefined) return root;
573
+ if (Array.isArray(current)) {
574
+ const arrayIndex = Number(segment);
575
+ if (!Number.isInteger(arrayIndex)) return root;
576
+ current = current[arrayIndex];
577
+ continue;
578
+ }
579
+ if (typeof current !== "object") return root;
580
+ current = (current as Record<string, unknown>)[segment];
581
+ }
582
+
583
+ // Set the value at the final segment
584
+ const lastSegment = segments[segments.length - 1];
585
+ if (Array.isArray(current)) {
586
+ const arrayIndex = Number(lastSegment);
587
+ if (!Number.isInteger(arrayIndex)) return root;
588
+ current[arrayIndex] = value;
589
+ return root;
590
+ }
591
+
592
+ if (typeof current !== "object" || current === null) return root;
593
+ (current as Record<string, unknown>)[lastSegment] = value;
594
+ return root;
595
+ }
596
+
597
+ /**
598
+ * Returns a new structure with the key at `pointer` removed. Only the
599
+ * containers along the path are shallow-cloned (`O(depth)` allocations);
600
+ * every sibling subtree is shared with the input. Returns the input
601
+ * reference unchanged when the pointer is empty, the path is invalid, or
602
+ * the final key is absent — so callers can detect a no-op via identity.
603
+ */
604
+ function deleteValueAtPointer(root: unknown, pointer: string): unknown {
605
+ if (!pointer) return root;
606
+ const segments = decodeJsonPointer(pointer);
607
+ if (segments.length === 0) return root;
608
+ return deleteAtSegment(root, segments, 0);
609
+ }
610
+
611
+ function deleteAtSegment(node: unknown, segments: string[], depth: number): unknown {
612
+ const segment = segments[depth];
613
+ const isLeaf = depth === segments.length - 1;
614
+
615
+ if (Array.isArray(node)) {
616
+ const index = Number(segment);
617
+ if (!Number.isInteger(index) || index < 0 || index >= node.length) return node;
618
+ if (isLeaf) {
619
+ const next = node.slice();
620
+ next.splice(index, 1);
621
+ return next;
622
+ }
623
+ const child = deleteAtSegment(node[index], segments, depth + 1);
624
+ if (child === node[index]) return node;
625
+ const next = node.slice();
626
+ next[index] = child;
627
+ return next;
628
+ }
629
+
630
+ if (typeof node !== "object" || node === null) return node;
631
+ const obj = node as Record<string, unknown>;
632
+ if (!Object.hasOwn(obj, segment)) return node;
633
+ if (isLeaf) {
634
+ const { [segment]: _omit, ...rest } = obj;
635
+ return rest;
636
+ }
637
+ const child = deleteAtSegment(obj[segment], segments, depth + 1);
638
+ if (child === obj[segment]) return node;
639
+ return { ...obj, [segment]: child };
640
+ }
641
+
642
+ // ============================================================================
643
+ // JSON-Schema-driven normalization passes (LLM quirks).
644
+ // ============================================================================
645
+
646
+ /**
647
+ * Test a JSON-Schema branch during nullable normalization. Kept deliberately
648
+ * small and synchronous so validation does not need to compile legacy schemas
649
+ * into another schema language.
650
+ */
651
+ function branchMatchesSchema(branch: unknown, value: unknown): boolean {
652
+ return isJsonSchemaValueValid(branch, value);
653
+ }
654
+
655
+ function normalizeOptionalNullsForSchema(
656
+ schema: unknown,
657
+ value: unknown,
658
+ isRoot = true,
659
+ ): { value: unknown; changed: boolean } {
660
+ if (value === null || value === undefined) return { value, changed: false };
661
+ if (schema === null || typeof schema !== "object") return { value, changed: false };
662
+
663
+ const schemaObject = schema as Record<string, unknown>;
664
+
665
+ const normalizeAnyOfLike = (keyword: "anyOf" | "oneOf"): { value: unknown; changed: boolean } => {
666
+ const branches = schemaObject[keyword];
667
+ if (!Array.isArray(branches)) return { value, changed: false };
668
+
669
+ let changedCandidate: { value: unknown; changed: true } | null = null;
670
+
671
+ for (const branch of branches) {
672
+ const normalized = normalizeOptionalNullsForSchema(branch, value, isRoot);
673
+ if (!normalized.changed) continue;
674
+
675
+ if (branchMatchesSchema(branch, normalized.value)) {
676
+ return normalized;
677
+ }
678
+
679
+ if (!changedCandidate) {
680
+ changedCandidate = { value: normalized.value, changed: true };
681
+ }
682
+ }
683
+
684
+ return changedCandidate ?? { value, changed: false };
685
+ };
686
+
687
+ const anyOfNormalization = normalizeAnyOfLike("anyOf");
688
+ if (anyOfNormalization.changed) return anyOfNormalization;
689
+
690
+ const oneOfNormalization = normalizeAnyOfLike("oneOf");
691
+ if (oneOfNormalization.changed) return oneOfNormalization;
692
+
693
+ if (Array.isArray(schemaObject.allOf)) {
694
+ let changed = false;
695
+ let nextValue: unknown = value;
696
+ for (const branch of schemaObject.allOf) {
697
+ const normalized = normalizeOptionalNullsForSchema(branch, nextValue, isRoot);
698
+ if (!normalized.changed) continue;
699
+ nextValue = normalized.value;
700
+ changed = true;
701
+ }
702
+ if (changed) return { value: nextValue, changed: true };
703
+ }
704
+
705
+ if (Array.isArray(value)) {
706
+ const itemSchema = schemaObject.items;
707
+ if (itemSchema === null || typeof itemSchema !== "object" || Array.isArray(itemSchema)) {
708
+ return { value, changed: false };
709
+ }
710
+
711
+ let changed = false;
712
+ let nextValue = value;
713
+ for (let i = 0; i < value.length; i += 1) {
714
+ const normalized = normalizeOptionalNullsForSchema(itemSchema, value[i], false);
715
+ if (!normalized.changed) continue;
716
+ if (!changed) {
717
+ nextValue = [...value];
718
+ changed = true;
719
+ }
720
+ nextValue[i] = normalized.value;
721
+ }
722
+ return { value: changed ? nextValue : value, changed };
723
+ }
724
+
725
+ // Coerce string → number/integer when the schema branch declares those types.
726
+ // This fixes anyOf:[{type:"number"},{type:"null"}] (i.e. Optional<number>) where
727
+ // the validator reports an "anyOf" error rather than a "type" error.
728
+ if ((schemaObject.type === "number" || schemaObject.type === "integer") && typeof value === "string") {
729
+ return tryParseNumberString(value, [schemaObject.type as string]);
730
+ }
731
+
732
+ if (schemaObject.type !== "object") return { value, changed: false };
733
+ if (typeof value !== "object" || value === null) return { value, changed: false };
734
+ if (Array.isArray(value)) return { value, changed: false };
735
+ if (schemaObject.properties === null || typeof schemaObject.properties !== "object") {
736
+ return { value, changed: false };
737
+ }
738
+
739
+ const properties = schemaObject.properties as Record<string, unknown>;
740
+ const required = new Set(Array.isArray(schemaObject.required) ? (schemaObject.required as string[]) : []);
741
+
742
+ let changed = false;
743
+ let nextValue = value as Record<string, unknown>;
744
+
745
+ for (const [key, propertySchema] of Object.entries(properties)) {
746
+ if (!(key in nextValue)) continue;
747
+ const currentValue = nextValue[key];
748
+ const isNullish = currentValue === null || currentValue === "null";
749
+ const isInvalidEmptyString =
750
+ currentValue === "" && !required.has(key) && !branchMatchesSchema(propertySchema, currentValue);
751
+
752
+ // Strip null/string "null" from optional fields, and strip empty
753
+ // strings only when the property schema would reject the explicit value.
754
+ // LLMs sometimes output these placeholders to mean "no value".
755
+ if ((isNullish || isInvalidEmptyString) && !required.has(key)) {
756
+ if (!changed) {
757
+ nextValue = { ...nextValue };
758
+ changed = true;
759
+ }
760
+ delete nextValue[key];
761
+ continue;
762
+ }
763
+
764
+ // Substitute the schema-supplied default when a required field arrives
765
+ // as null/"null". LLMs commonly emit null for "I have nothing to say
766
+ // here"; if the schema documents a default, honor it instead of
767
+ // rejecting the whole call. The default is cloned so mutations on the
768
+ // validated value never bleed back into the schema.
769
+ if (isNullish && propertySchema && typeof propertySchema === "object") {
770
+ const propertyObject = propertySchema as Record<string, unknown>;
771
+ if ("default" in propertyObject) {
772
+ if (!changed) {
773
+ nextValue = { ...nextValue };
774
+ changed = true;
775
+ }
776
+ nextValue[key] = structuredCloneJSON(propertyObject.default);
777
+ continue;
778
+ }
779
+ }
780
+ const normalized = normalizeOptionalNullsForSchema(propertySchema, currentValue, false);
781
+ if (!normalized.changed) continue;
782
+
783
+ if (!changed) {
784
+ nextValue = { ...nextValue };
785
+ changed = true;
786
+ }
787
+ nextValue[key] = normalized.value;
788
+ }
789
+
790
+ // Strip unknown keys with null/"null" values when the schema forbids extras.
791
+ // LLMs sometimes hallucinate verbs alongside valid ones (e.g. `split: null`,
792
+ // `original: null`). Rejecting the entire tool call wastes a turn; treating
793
+ // these the same as null on known optional fields is a safer fallback. Keys
794
+ // with non-null unknown values are left intact so genuine schema mistakes
795
+ // still surface as validation errors.
796
+ //
797
+ // At the root level unknown null-valued keys stay intact; the
798
+ // post-validation `preserveUnknownRootFields` pass re-attaches root extras.
799
+ if (!isRoot && schemaObject.additionalProperties === false) {
800
+ const knownKeys = new Set(Object.keys(properties));
801
+ for (const key of Object.keys(nextValue)) {
802
+ if (knownKeys.has(key)) continue;
803
+ const v = nextValue[key];
804
+ if (v !== null && v !== "null") continue;
805
+ if (!changed) {
806
+ nextValue = { ...nextValue };
807
+ changed = true;
808
+ }
809
+ delete nextValue[key];
810
+ }
811
+ }
812
+
813
+ return { value: changed ? nextValue : value, changed };
814
+ }
815
+
816
+ function decodeJsonPointerToken(token: string): string {
817
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
818
+ }
819
+
820
+ function resolveLocalJsonSchemaRef(root: unknown, ref: string): unknown | undefined {
821
+ if (ref === "#") return root;
822
+ if (!ref.startsWith("#/")) return undefined;
823
+ let current: unknown = root;
824
+ for (const rawToken of ref.slice(2).split("/")) {
825
+ const token = decodeJsonPointerToken(rawToken);
826
+ if (current === null || typeof current !== "object") return undefined;
827
+ current = (current as Record<string, unknown>)[token];
828
+ }
829
+ return current;
830
+ }
831
+
832
+ function normalizeEnumStringWhitespace(
833
+ schema: unknown,
834
+ value: unknown,
835
+ root: unknown = schema,
836
+ refs: ReadonlySet<string> = new Set(),
837
+ ): { value: unknown; changed: boolean } {
838
+ if (value === null || value === undefined) return { value, changed: false };
839
+ if (schema === null || typeof schema !== "object") return { value, changed: false };
840
+
841
+ const schemaObject = schema as Record<string, unknown>;
842
+ const ref = schemaObject.$ref;
843
+ if (typeof ref === "string") {
844
+ if (refs.has(ref)) return { value, changed: false };
845
+ const resolved = resolveLocalJsonSchemaRef(root, ref);
846
+ if (resolved === undefined) return { value, changed: false };
847
+ return normalizeEnumStringWhitespace(resolved, value, root, new Set([...refs, ref]));
848
+ }
849
+
850
+ const branchMatches = (branch: unknown, candidate: unknown): boolean => {
851
+ if (branch !== null && typeof branch === "object") {
852
+ const branchRef = (branch as Record<string, unknown>).$ref;
853
+ if (typeof branchRef === "string" && !refs.has(branchRef)) {
854
+ const resolved = resolveLocalJsonSchemaRef(root, branchRef);
855
+ if (resolved !== undefined) return branchMatchesSchema(resolved, candidate);
856
+ }
857
+ }
858
+ return branchMatchesSchema(branch, candidate);
859
+ };
860
+
861
+ const normalizeAnyOfLike = (keyword: "anyOf" | "oneOf"): { value: unknown; changed: boolean } => {
862
+ const branches = schemaObject[keyword];
863
+ if (!Array.isArray(branches)) return { value, changed: false };
864
+ if (branches.some(branch => branchMatches(branch, value))) return { value, changed: false };
865
+
866
+ for (const branch of branches) {
867
+ const normalized = normalizeEnumStringWhitespace(branch, value, root, refs);
868
+ if (!normalized.changed) continue;
869
+ if (branchMatches(branch, normalized.value)) return normalized;
870
+ }
871
+ return { value, changed: false };
872
+ };
873
+
874
+ const anyOfNormalization = normalizeAnyOfLike("anyOf");
875
+ if (anyOfNormalization.changed) return anyOfNormalization;
876
+
877
+ const oneOfNormalization = normalizeAnyOfLike("oneOf");
878
+ if (oneOfNormalization.changed) return oneOfNormalization;
879
+
880
+ if (Array.isArray(schemaObject.allOf)) {
881
+ let changed = false;
882
+ let nextValue: unknown = value;
883
+ for (const branch of schemaObject.allOf) {
884
+ const normalized = normalizeEnumStringWhitespace(branch, nextValue, root, refs);
885
+ if (!normalized.changed) continue;
886
+ nextValue = normalized.value;
887
+ changed = true;
888
+ }
889
+ if (changed) return { value: nextValue, changed: true };
890
+ }
891
+
892
+ if (typeof value === "string") {
893
+ const trimmed = value.trim();
894
+ if (trimmed !== value) {
895
+ const enumValues = schemaObject.enum;
896
+ if (Array.isArray(enumValues) && !enumValues.includes(value) && enumValues.includes(trimmed)) {
897
+ return { value: trimmed, changed: true };
898
+ }
899
+ const constValue = schemaObject.const;
900
+ if (typeof constValue === "string" && trimmed === constValue) {
901
+ return { value: trimmed, changed: true };
902
+ }
903
+ }
904
+ return { value, changed: false };
905
+ }
906
+
907
+ if (Array.isArray(value)) {
908
+ let changed = false;
909
+ let nextValue = value;
910
+ const prefixItems = schemaObject.prefixItems;
911
+ if (Array.isArray(prefixItems)) {
912
+ for (let i = 0; i < value.length && i < prefixItems.length; i += 1) {
913
+ const itemSchema = prefixItems[i];
914
+ const normalized = normalizeEnumStringWhitespace(itemSchema, value[i], root, refs);
915
+ if (!normalized.changed) continue;
916
+ if (!changed) {
917
+ nextValue = [...value];
918
+ changed = true;
919
+ }
920
+ nextValue[i] = normalized.value;
921
+ }
922
+ }
923
+
924
+ const itemSchema = schemaObject.items;
925
+ if (itemSchema !== null && typeof itemSchema === "object" && !Array.isArray(itemSchema)) {
926
+ for (let i = 0; i < value.length; i += 1) {
927
+ if (Array.isArray(prefixItems) && i < prefixItems.length) continue;
928
+ const normalized = normalizeEnumStringWhitespace(itemSchema, nextValue[i], root, refs);
929
+ if (!normalized.changed) continue;
930
+ if (!changed) {
931
+ nextValue = [...value];
932
+ changed = true;
933
+ }
934
+ nextValue[i] = normalized.value;
935
+ }
936
+ }
937
+ return { value: changed ? nextValue : value, changed };
938
+ }
939
+
940
+ if (typeof value !== "object") return { value, changed: false };
941
+ const properties = schemaObject.properties;
942
+ if (!properties || typeof properties !== "object") return { value, changed: false };
943
+
944
+ const propsObject = properties as Record<string, unknown>;
945
+ const valueObject = value as Record<string, unknown>;
946
+ let changed = false;
947
+ let nextValue = valueObject;
948
+ for (const [key, propertySchema] of Object.entries(propsObject)) {
949
+ if (!(key in nextValue)) continue;
950
+ const normalized = normalizeEnumStringWhitespace(propertySchema, nextValue[key], root, refs);
951
+ if (!normalized.changed) continue;
952
+ if (!changed) {
953
+ nextValue = { ...nextValue };
954
+ changed = true;
955
+ }
956
+ nextValue[key] = normalized.value;
957
+ }
958
+ return { value: changed ? nextValue : valueObject, changed };
959
+ }
960
+
961
+ // ============================================================================
962
+ // Identifier-string trailing-whitespace normalization (LLM quirk).
963
+ // ============================================================================
964
+ //
965
+ // LLMs sometimes emit tool arguments with a trailing newline dangling off a
966
+ // short identifier — a path, URL, or a display label like `title`. These
967
+ // values are never legitimately terminated by line breaks, so we strip trailing
968
+ // line terminators from string values on the well-known keys below before the
969
+ // tool ever sees them. Content-carrying properties (`content`, `input`, `body`,
970
+ // `text`, `command`, `code`) are intentionally not traversed or trimmed so
971
+ // genuine trailing whitespace survives on writes, patches, shell commands, and
972
+ // eval snippets.
973
+ // ============================================================================
974
+
975
+ /**
976
+ * Property names whose values are treated as short identifiers — filesystem
977
+ * paths, URLs, URIs, or display labels. The trim only fires on strings sitting
978
+ * under one of these keys, so `path: "docs/report "` still targets the file
979
+ * whose name ends in a space.
980
+ */
981
+ const IDENTIFIER_STRING_KEYS: ReadonlySet<string> = new Set([
982
+ "path",
983
+ "paths",
984
+ "file",
985
+ "file_path",
986
+ "filePath",
987
+ "filepath",
988
+ "url",
989
+ "uri",
990
+ "title",
991
+ "label",
992
+ ]);
993
+
994
+ const CONTENT_CARRYING_KEYS: ReadonlySet<string> = new Set(["content", "input", "body", "text", "command", "code"]);
995
+
996
+ const TRAILING_LINE_TERMINATOR_RE = /[\r\n]+$/;
997
+
998
+ function trimTrailingLineTerminators(input: string): string {
999
+ if (!TRAILING_LINE_TERMINATOR_RE.test(input)) return input;
1000
+ return input.replace(TRAILING_LINE_TERMINATOR_RE, "");
1001
+ }
1002
+
1003
+ function trimIdentifierStringLeaf(input: unknown): unknown {
1004
+ if (typeof input === "string") {
1005
+ const trimmed = trimTrailingLineTerminators(input);
1006
+ return trimmed === input ? input : trimmed;
1007
+ }
1008
+ if (Array.isArray(input)) {
1009
+ let changed = false;
1010
+ let next = input;
1011
+ for (let i = 0; i < input.length; i += 1) {
1012
+ const item = input[i];
1013
+ if (typeof item !== "string") continue;
1014
+ const trimmed = trimTrailingLineTerminators(item);
1015
+ if (trimmed === item) continue;
1016
+ if (!changed) {
1017
+ next = input.slice();
1018
+ changed = true;
1019
+ }
1020
+ next[i] = trimmed;
1021
+ }
1022
+ return changed ? next : input;
1023
+ }
1024
+ return input;
1025
+ }
1026
+
1027
+ /**
1028
+ * Recursively strip trailing line terminators from string values whose property
1029
+ * key matches {@link IDENTIFIER_STRING_KEYS}. Runs by property name only so it
1030
+ * fires uniformly across ArkType and plain JSON Schema tools.
1031
+ */
1032
+ function normalizeIdentifierStringWhitespace(value: unknown): { value: unknown; changed: boolean } {
1033
+ if (Array.isArray(value)) {
1034
+ let changed = false;
1035
+ let next = value;
1036
+ for (let i = 0; i < value.length; i += 1) {
1037
+ const normalized = normalizeIdentifierStringWhitespace(value[i]);
1038
+ if (!normalized.changed) continue;
1039
+ if (!changed) {
1040
+ next = [...value];
1041
+ changed = true;
1042
+ }
1043
+ next[i] = normalized.value;
1044
+ }
1045
+ return { value: changed ? next : value, changed };
1046
+ }
1047
+
1048
+ if (value === null || typeof value !== "object") return { value, changed: false };
1049
+
1050
+ const source = value as Record<string, unknown>;
1051
+ let changed = false;
1052
+ let out: Record<string, unknown> = source;
1053
+ for (const [key, entry] of Object.entries(source)) {
1054
+ let nextEntry = entry;
1055
+ if (CONTENT_CARRYING_KEYS.has(key)) continue;
1056
+ if (IDENTIFIER_STRING_KEYS.has(key)) {
1057
+ const trimmed = trimIdentifierStringLeaf(entry);
1058
+ if (trimmed !== entry) nextEntry = trimmed;
1059
+ }
1060
+ const nested = normalizeIdentifierStringWhitespace(nextEntry);
1061
+ if (nested.changed) nextEntry = nested.value;
1062
+ if (nextEntry === entry) continue;
1063
+ if (!changed) {
1064
+ out = { ...source };
1065
+ changed = true;
1066
+ }
1067
+ out[key] = nextEntry;
1068
+ }
1069
+ return { value: changed ? out : value, changed };
1070
+ }
1071
+
1072
+ // ============================================================================
1073
+ // Double-encoded object-key normalization (LLM quirk).
1074
+ // ============================================================================
1075
+ //
1076
+ // LLMs occasionally serialize an object key one time too many, so the property
1077
+ // NAME arrives as the JSON encoding of the real name — literal quote characters
1078
+ // and all (e.g. `{ "\"op\"": "done" }` decodes to the JS key `"op"`). The
1079
+ // schema never matches such a key, so it reads as an unrecognized extra and is
1080
+ // dropped by the unrecognized-key repair, later surfacing as a spurious
1081
+ // missing-required error. We walk the whole value (arrays + nested objects)
1082
+ // and rename any key that is itself the JSON encoding of a plain string back to
1083
+ // that string.
1084
+ // ============================================================================
1085
+
1086
+ /** Max layers of accidental JSON-encoding to peel off a single object key. */
1087
+ const MAX_KEY_DECODE_DEPTH = 3;
1088
+
1089
+ /**
1090
+ * If `key` is the JSON encoding of a plain string (quote-wrapped and
1091
+ * `JSON.parse`s to a string), return the decoded string; otherwise null. Peels
1092
+ * up to {@link MAX_KEY_DECODE_DEPTH} nested encodings so multiply-encoded keys
1093
+ * collapse in one pass. Conservative: any key that is not a quote-wrapped JSON
1094
+ * string literal is left untouched.
1095
+ */
1096
+ function decodeDoubleEncodedKey(key: string): string | null {
1097
+ let current = key;
1098
+ let decoded: string | null = null;
1099
+ for (let depth = 0; depth < MAX_KEY_DECODE_DEPTH; depth += 1) {
1100
+ if (current.length < 2 || current[0] !== '"' || current[current.length - 1] !== '"') break;
1101
+ let parsed: unknown;
1102
+ try {
1103
+ parsed = JSON.parse(current);
1104
+ } catch {
1105
+ break;
1106
+ }
1107
+ if (typeof parsed !== "string") break;
1108
+ current = parsed;
1109
+ decoded = current;
1110
+ }
1111
+ return decoded;
1112
+ }
1113
+
1114
+ /**
1115
+ * Recursively unwrap object keys that were accidentally JSON-encoded an extra
1116
+ * time. Schema-agnostic by design: such keys are dropped before any schema pass
1117
+ * can map them, so this runs first. A key is only renamed when the decoded name
1118
+ * differs and does not already exist on the same object — renaming would
1119
+ * otherwise clobber a sibling and silently lose data.
1120
+ */
1121
+ function normalizeDoubleEncodedKeys(value: unknown): { value: unknown; changed: boolean } {
1122
+ if (Array.isArray(value)) {
1123
+ let changed = false;
1124
+ let next = value;
1125
+ for (let i = 0; i < value.length; i += 1) {
1126
+ const normalized = normalizeDoubleEncodedKeys(value[i]);
1127
+ if (!normalized.changed) continue;
1128
+ if (!changed) {
1129
+ next = [...value];
1130
+ changed = true;
1131
+ }
1132
+ next[i] = normalized.value;
1133
+ }
1134
+ return { value: changed ? next : value, changed };
1135
+ }
1136
+
1137
+ if (value === null || typeof value !== "object") return { value, changed: false };
1138
+
1139
+ const source = value as Record<string, unknown>;
1140
+ let changed = false;
1141
+ const out: Record<string, unknown> = {};
1142
+ for (const [key, entry] of Object.entries(source)) {
1143
+ const normalizedChild = normalizeDoubleEncodedKeys(entry);
1144
+ const nextChild = normalizedChild.changed ? normalizedChild.value : entry;
1145
+
1146
+ const decodedKey = decodeDoubleEncodedKey(key);
1147
+ // `Object.hasOwn` (not `in`) so a decoded `constructor`/`toString` is not
1148
+ // mistaken for a collision via the prototype chain.
1149
+ const targetKey =
1150
+ decodedKey !== null &&
1151
+ decodedKey !== key &&
1152
+ !Object.hasOwn(source, decodedKey) &&
1153
+ !Object.hasOwn(out, decodedKey)
1154
+ ? decodedKey
1155
+ : key;
1156
+
1157
+ if (targetKey !== key || normalizedChild.changed) changed = true;
1158
+ // `defineProperty` so a decoded `__proto__` key becomes an own property
1159
+ // instead of mutating the result object's prototype.
1160
+ Object.defineProperty(out, targetKey, {
1161
+ value: nextChild,
1162
+ writable: true,
1163
+ enumerable: true,
1164
+ configurable: true,
1165
+ });
1166
+ }
1167
+
1168
+ return { value: changed ? out : value, changed };
1169
+ }
1170
+
1171
+ // ============================================================================
1172
+ // String-encoded array coercion for union(string, array) schemas.
1173
+ // ============================================================================
1174
+
1175
+ /**
1176
+ * Detects whether a schema node accepts BOTH the `string` and `array` JSON
1177
+ * Schema types. Recognizes:
1178
+ * - `{ "type": ["string", "array"] }` (multi-type),
1179
+ * - `{ "anyOf": [...] }` / `{ "oneOf": [...] }` with at least one string
1180
+ * branch and one array branch.
1181
+ */
1182
+ function schemaAcceptsStringAndArray(schema: Record<string, unknown>): boolean {
1183
+ if (Array.isArray(schema.type) && schema.type.includes("string") && schema.type.includes("array")) {
1184
+ return true;
1185
+ }
1186
+
1187
+ for (const key of ["anyOf", "oneOf"] as const) {
1188
+ const branches = schema[key];
1189
+ if (!Array.isArray(branches)) continue;
1190
+ let hasString = false;
1191
+ let hasArray = false;
1192
+ for (const branch of branches) {
1193
+ if (!branch || typeof branch !== "object") continue;
1194
+ const branchType = (branch as Record<string, unknown>).type;
1195
+ if (branchType === "string" || (Array.isArray(branchType) && branchType.includes("string"))) {
1196
+ hasString = true;
1197
+ }
1198
+ if (branchType === "array" || (Array.isArray(branchType) && branchType.includes("array"))) {
1199
+ hasArray = true;
1200
+ }
1201
+ if (hasString && hasArray) return true;
1202
+ }
1203
+ }
1204
+ return false;
1205
+ }
1206
+
1207
+ function schemaNodeAcceptsArray(schema: unknown): schema is Record<string, unknown> {
1208
+ if (!schema || typeof schema !== "object") return false;
1209
+ const schemaObject = schema as Record<string, unknown>;
1210
+ const schemaType = schemaObject.type;
1211
+ return schemaType === "array" || (Array.isArray(schemaType) && schemaType.includes("array"));
1212
+ }
1213
+
1214
+ function parsedArrayMatchesArrayBranch(schema: Record<string, unknown>, value: unknown[]): boolean {
1215
+ if (schemaNodeAcceptsArray(schema)) {
1216
+ return isJsonSchemaValueValid(schema, value);
1217
+ }
1218
+
1219
+ for (const key of ["anyOf", "oneOf"] as const) {
1220
+ const branches = schema[key];
1221
+ if (!Array.isArray(branches)) continue;
1222
+ const branchList: unknown[] = branches;
1223
+ for (const branch of branchList) {
1224
+ if (!schemaNodeAcceptsArray(branch)) continue;
1225
+ if (isJsonSchemaValueValid(branch, value)) return true;
1226
+ }
1227
+ }
1228
+ return false;
1229
+ }
1230
+
1231
+ /**
1232
+ * Pre-validation normalization: when a schema field accepts BOTH `string` and
1233
+ * `array`, providers that double-serialize tool arguments can deliver array
1234
+ * values as JSON-encoded strings like `'["a","b"]'`. A string-or-array union
1235
+ * accepts that value against the string branch before issue-driven coercion.
1236
+ *
1237
+ * Walk the schema; when both shapes are accepted AND the incoming value is a
1238
+ * JSON-array-shaped string, substitute the parsed array only if it validates
1239
+ * against the schema's array branch. Conservative: array-shaped strings like
1240
+ * `"[1]"` stay on the string branch when the array branch is `string[]`.
1241
+ *
1242
+ * See https://github.com/can1357/oh-my-pi/issues/1788.
1243
+ */
1244
+ function normalizeStringEncodedArrayUnions(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
1245
+ if (value === null || value === undefined) return { value, changed: false };
1246
+ if (schema === null || typeof schema !== "object") return { value, changed: false };
1247
+
1248
+ const schemaObject = schema as Record<string, unknown>;
1249
+
1250
+ // Leaf case: this schema node accepts both string and array.
1251
+ if (typeof value === "string" && schemaAcceptsStringAndArray(schemaObject)) {
1252
+ const trimmed = value.trim();
1253
+ if (!trimmed.startsWith("[")) return { value, changed: false };
1254
+ try {
1255
+ const parsed = JSON.parse(trimmed) as unknown;
1256
+ if (Array.isArray(parsed)) {
1257
+ // Unwrap any double-encoded object keys inside the parsed array
1258
+ // before the branch-match check; otherwise an `array<object>`
1259
+ // branch fails to validate and the value silently stays on the
1260
+ // string branch.
1261
+ const candidate = normalizeDoubleEncodedKeys(parsed).value as unknown[];
1262
+ if (parsedArrayMatchesArrayBranch(schemaObject, candidate)) {
1263
+ return { value: candidate, changed: true };
1264
+ }
1265
+ }
1266
+ } catch {
1267
+ // Not valid JSON — leave the string alone for the validator to handle.
1268
+ }
1269
+ return { value, changed: false };
1270
+ }
1271
+
1272
+ // Recurse into array items.
1273
+ if (Array.isArray(value)) {
1274
+ const itemSchema = schemaObject.items;
1275
+ if (!itemSchema || typeof itemSchema !== "object" || Array.isArray(itemSchema)) {
1276
+ return { value, changed: false };
1277
+ }
1278
+ let changed = false;
1279
+ let nextValue = value;
1280
+ for (let i = 0; i < value.length; i += 1) {
1281
+ const normalized = normalizeStringEncodedArrayUnions(itemSchema, value[i]);
1282
+ if (!normalized.changed) continue;
1283
+ if (!changed) {
1284
+ nextValue = [...value];
1285
+ changed = true;
1286
+ }
1287
+ nextValue[i] = normalized.value;
1288
+ }
1289
+ return { value: changed ? nextValue : value, changed };
1290
+ }
1291
+
1292
+ // Recurse into object properties.
1293
+ if (schemaObject.type !== "object") return { value, changed: false };
1294
+ if (typeof value !== "object" || value === null) return { value, changed: false };
1295
+ const properties = schemaObject.properties;
1296
+ if (!properties || typeof properties !== "object") return { value, changed: false };
1297
+
1298
+ const propsObject = properties as Record<string, unknown>;
1299
+ const valueObject = value as Record<string, unknown>;
1300
+ let changed = false;
1301
+ let nextValue = valueObject;
1302
+ for (const [key, propertySchema] of Object.entries(propsObject)) {
1303
+ if (!(key in nextValue)) continue;
1304
+ const normalized = normalizeStringEncodedArrayUnions(propertySchema, nextValue[key]);
1305
+ if (!normalized.changed) continue;
1306
+ if (!changed) {
1307
+ nextValue = { ...nextValue };
1308
+ changed = true;
1309
+ }
1310
+ nextValue[key] = normalized.value;
1311
+ }
1312
+ return { value: changed ? nextValue : valueObject, changed };
1313
+ }
1314
+
1315
+ /**
1316
+ * Name of the sole property when a schema declares exactly one required string
1317
+ * field, else `undefined`. Recognizes the closed single-argument tool shape
1318
+ * (`{ type: "object", properties: { X: { type: "string" } }, required: ["X"] }`).
1319
+ */
1320
+ function singleRequiredStringKey(schema: unknown): string | undefined {
1321
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return undefined;
1322
+ const obj = schema as Record<string, unknown>;
1323
+ if (obj.type !== "object") return undefined;
1324
+ const properties = obj.properties;
1325
+ if (!properties || typeof properties !== "object") return undefined;
1326
+ const keys = Object.keys(properties as Record<string, unknown>);
1327
+ if (keys.length !== 1) return undefined;
1328
+ const key = keys[0];
1329
+ const required = obj.required;
1330
+ if (!Array.isArray(required) || required.length !== 1 || required[0] !== key) return undefined;
1331
+ const propertySchema = (properties as Record<string, unknown>)[key];
1332
+ if (!propertySchema || typeof propertySchema !== "object") return undefined;
1333
+ return (propertySchema as Record<string, unknown>).type === "string" ? key : undefined;
1334
+ }
1335
+
1336
+ /**
1337
+ * LLM-quirk repair for single-argument tools. When a tool declares exactly one
1338
+ * property — a required string — some providers deliver the payload under a
1339
+ * different key (e.g. the `edit` tool's patch arriving as `input`/`_input`, or
1340
+ * any single-string tool whose argument the model mislabels). When the declared
1341
+ * key is absent but another field holds a string, adopt the first such string
1342
+ * as the declared key so the call validates instead of failing with "<key> was
1343
+ * missing". A present-but-wrong-type value is left alone so its real type error
1344
+ * still surfaces.
1345
+ */
1346
+ function normalizeSingleStringField(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
1347
+ const key = singleRequiredStringKey(schema);
1348
+ if (key === undefined) return { value, changed: false };
1349
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return { value, changed: false };
1350
+ const record = value as Record<string, unknown>;
1351
+ if (record[key] !== undefined) return { value, changed: false };
1352
+ for (const candidate in record) {
1353
+ if (candidate === key || !Object.hasOwn(record, candidate)) continue;
1354
+ const candidateValue = record[candidate];
1355
+ if (typeof candidateValue !== "string") continue;
1356
+ const next = { ...record, [key]: candidateValue };
1357
+ delete next[candidate];
1358
+ return { value: next, changed: true };
1359
+ }
1360
+ return { value, changed: false };
1361
+ }
1362
+
1363
+ // Validation issue → coercion bridge
1364
+
1365
+ interface FlatIssue {
1366
+ keyword: "type" | "unrecognized" | "other";
1367
+ instancePath: string;
1368
+ expectedTypes: string[];
1369
+ unionBranch: boolean;
1370
+ }
1371
+
1372
+ /**
1373
+ * Repair issues raised by the validator before we surface them to the caller.
1374
+ *
1375
+ * Two kinds of repair are applied:
1376
+ * - **type**: when a value has a common LLM-produced shape mismatch, rewrite
1377
+ * it only in the direction requested by the schema: parse JSON strings,
1378
+ * accept boolean spellings, stringify non-null values for string fields,
1379
+ * map booleans to numeric 0/1, and wrap singleton array values for non-union
1380
+ * array expectations.
1381
+ * - **unrecognized**: when a closed object received an extra key
1382
+ * (`additionalProperties: false`), drop that key so re-validation succeeds.
1383
+ * This effectively coerces object schemas to loose semantics recursively.
1384
+ *
1385
+ * The function is safe and conservative:
1386
+ * - Only processes "type" and "unrecognized" issues
1387
+ * - Only attempts schema-directed coercions for the expected type
1388
+ * - Only wraps singleton array values for non-union type expectations
1389
+ * - Clones the args object before mutation (copy-on-write)
1390
+ */
1391
+ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unknown; changed: boolean } {
1392
+ if (issues.length === 0) return { value: args, changed: false };
1393
+
1394
+ let changed = false;
1395
+ // Tracks whether `nextArgs` is a fully owned deep copy (safe to mutate
1396
+ // leaves). The unrecognized-key path uses path-shallow immutable updates
1397
+ // and does NOT require ownership, so we only pay for the deep clone when
1398
+ // a type coercion actually needs to write into a leaf.
1399
+ let owned = false;
1400
+ let nextArgs: unknown = args;
1401
+
1402
+ for (const issue of issues) {
1403
+ if (issue.keyword === "unrecognized") {
1404
+ const previous = nextArgs;
1405
+ nextArgs = deleteValueAtPointer(nextArgs, issue.instancePath);
1406
+ if (nextArgs !== previous) changed = true;
1407
+ continue;
1408
+ }
1409
+ if (issue.keyword !== "type") continue;
1410
+ if (issue.expectedTypes.length === 0) continue;
1411
+
1412
+ const currentValue = getValueAtPointer(nextArgs, issue.instancePath);
1413
+ const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes);
1414
+ let coercedValue = result.changed ? result.value : undefined;
1415
+ if (
1416
+ coercedValue === undefined &&
1417
+ issue.expectedTypes.includes("array") &&
1418
+ !issue.unionBranch &&
1419
+ currentValue !== undefined &&
1420
+ !Array.isArray(currentValue)
1421
+ ) {
1422
+ const objectCoercion =
1423
+ typeof currentValue === "string"
1424
+ ? tryParseJsonForTypes(currentValue, ["object"])
1425
+ : { value: currentValue, changed: false };
1426
+ if (objectCoercion.changed || !looksLikeJsonContainerString(currentValue)) {
1427
+ coercedValue = [objectCoercion.changed ? objectCoercion.value : currentValue];
1428
+ }
1429
+ }
1430
+ if (coercedValue === undefined) continue;
1431
+
1432
+ if (!owned) {
1433
+ nextArgs = structuredCloneJSON(nextArgs);
1434
+ owned = true;
1435
+ changed = true;
1436
+ }
1437
+ nextArgs = setValueAtPointer(nextArgs, issue.instancePath, coercedValue);
1438
+ }
1439
+
1440
+ return { value: changed ? nextArgs : args, changed };
1441
+ }
1442
+
1443
+ // ============================================================================
1444
+ // Public API
1445
+ // ============================================================================
1446
+
1447
+ type ValidationContext =
1448
+ | {
1449
+ kind: "arktype";
1450
+ ark: Type;
1451
+ json: Record<string, unknown>;
1452
+ }
1453
+ | {
1454
+ kind: "json";
1455
+ json: Record<string, unknown>;
1456
+ };
1457
+
1458
+ /**
1459
+ * Cache the validation context derived from a tool's parameters schema.
1460
+ * Keyed by the parameters object identity (stable across tool registrations),
1461
+ * via {@link stamp} so callable ArkType schemas — and any frozen host — degrade
1462
+ * to recompute-on-call instead of throwing on assignment.
1463
+ */
1464
+ const kValidationContext = Symbol("ai.validationContext");
1465
+ function getValidationContext(tool: Tool): ValidationContext {
1466
+ return stamp(tool.parameters as object, kValidationContext, params =>
1467
+ isArkSchema(params)
1468
+ ? { kind: "arktype", ark: params, json: arkToWireSchema(params) }
1469
+ : { kind: "json", json: upgradeJsonSchemaTo202012(params) as Record<string, unknown> },
1470
+ );
1471
+ }
1472
+
1473
+ type ContextValidationResult =
1474
+ | { success: true; value: unknown }
1475
+ | { success: false; flatIssues: FlatIssue[]; messages: string[] };
1476
+
1477
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
1478
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1479
+ }
1480
+
1481
+ function preserveUnknownRootFields(input: unknown, parsed: unknown): unknown {
1482
+ if (!isPlainRecord(input) || !isPlainRecord(parsed)) return parsed;
1483
+ return { ...input, ...parsed };
1484
+ }
1485
+
1486
+ function flattenJsonSchemaIssues(issues: ReadonlyArray<JsonSchemaValidationIssue>): FlatIssue[] {
1487
+ return issues.map(issue => {
1488
+ const unionBranch = issue.fromUnionBranch === true;
1489
+ if (issue.keyword === "additionalProperties") {
1490
+ return {
1491
+ keyword: "unrecognized",
1492
+ instancePath: pathToPointer(issue.path),
1493
+ expectedTypes: [],
1494
+ unionBranch,
1495
+ };
1496
+ }
1497
+ return {
1498
+ keyword: issue.keyword === "type" ? "type" : "other",
1499
+ instancePath: pathToPointer(issue.path),
1500
+ expectedTypes: issue.expectedTypes ?? [],
1501
+ unionBranch,
1502
+ };
1503
+ });
1504
+ }
1505
+
1506
+ function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
1507
+ return path.length === 0 ? "root" : path.map(seg => String(seg)).join("/");
1508
+ }
1509
+
1510
+ function validateContext(ctx: ValidationContext, value: unknown): ContextValidationResult {
1511
+ if (ctx.kind === "arktype") {
1512
+ const out = ctx.ark(value);
1513
+ if (!(out instanceof type.errors)) {
1514
+ return { success: true, value: preserveUnknownRootFields(value, out) };
1515
+ }
1516
+ // A `.narrow()`/cross-field failure can have ArkType reject while the wire
1517
+ // JSON (its predicate dropped by the toJsonSchema fallback) accepts — then
1518
+ // there are no json issues to coerce and we fall through to the formatted
1519
+ // error built from ArkType's own messages.
1520
+ const jr = validateJsonSchemaValue(ctx.json, value);
1521
+ const flatIssues = jr.success ? [] : flattenJsonSchemaIssues(jr.issues);
1522
+ return {
1523
+ success: false,
1524
+ flatIssues,
1525
+ messages: out.map(e => ` - ${formatIssuePath(e.path)}: ${e.message}`),
1526
+ };
1527
+ }
1528
+
1529
+ const result = validateJsonSchemaValue(ctx.json, value);
1530
+ if (result.success) return { success: true, value };
1531
+ return {
1532
+ success: false,
1533
+ flatIssues: flattenJsonSchemaIssues(result.issues),
1534
+ messages: result.issues.map(issue => ` - ${formatIssuePath(issue.path)}: ${issue.message}`),
1535
+ };
1536
+ }
1537
+
1538
+ // In-band `arg_key`/`arg_value` tool-call syntax that leaks into native
1539
+ // tool-call arguments when a provider parses the model's owned format
1540
+ // server-side and the model botches an `</arg_value>` closer.
1541
+ const SPILL_KEY_OPEN = "<arg_key>";
1542
+ const SPILL_KEY_CLOSE = "</arg_key>";
1543
+ const SPILL_VALUE_OPEN = "<arg_value>";
1544
+ const SPILL_VALUE_CLOSE = "</arg_value>";
1545
+ const SPILL_TOOL_CLOSE = "</tool_call>";
1546
+ /** Plausible spilled argument names; anything else is ordinary content. */
1547
+ const SPILL_KEY_PATTERN = /^[\w.$-]{1,128}$/;
1548
+
1549
+ interface SpillSplit {
1550
+ head: string;
1551
+ pairs: [string, string][];
1552
+ }
1553
+
1554
+ function skipSpillWhitespace(text: string, from: number): number {
1555
+ let at = from;
1556
+ while (at < text.length && " \n\t\r".includes(text[at]!)) at++;
1557
+ return at;
1558
+ }
1559
+
1560
+ /** Whether a well-formed `<arg_key>NAME</arg_key>…<arg_value>` pair starts at `at`. */
1561
+ function isSpillPairStart(text: string, at: number): boolean {
1562
+ if (!text.startsWith(SPILL_KEY_OPEN, at)) return false;
1563
+ const keyStart = at + SPILL_KEY_OPEN.length;
1564
+ const keyEnd = text.indexOf(SPILL_KEY_CLOSE, keyStart);
1565
+ if (keyEnd === -1 || !SPILL_KEY_PATTERN.test(text.slice(keyStart, keyEnd))) return false;
1566
+ const valueAt = skipSpillWhitespace(text, keyEnd + SPILL_KEY_CLOSE.length);
1567
+ return text.startsWith(SPILL_VALUE_OPEN, valueAt);
1568
+ }
1569
+
1570
+ /**
1571
+ * Finds where a spilled `<arg_value>` body ends: the legit closer, a
1572
+ * mistyped `</arg_key>` closer (validated by its follow-up), the start of the
1573
+ * next pair when the closer is missing entirely, or end of input (the
1574
+ * provider's parser consumed the terminating closer).
1575
+ */
1576
+ function findSpillValueEnd(text: string, from: number): { end: number; next: number } {
1577
+ const close = text.indexOf(SPILL_VALUE_CLOSE, from);
1578
+ let wrong = text.indexOf(SPILL_KEY_CLOSE, from);
1579
+ let open = text.indexOf(SPILL_KEY_OPEN, from);
1580
+ while (true) {
1581
+ const candidates = [close, wrong, open].filter(index => index !== -1);
1582
+ if (candidates.length === 0) return { end: text.length, next: text.length };
1583
+ const at = Math.min(...candidates);
1584
+ if (at === close) return { end: at, next: at + SPILL_VALUE_CLOSE.length };
1585
+ if (at === wrong) {
1586
+ const follow = skipSpillWhitespace(text, at + SPILL_KEY_CLOSE.length);
1587
+ if (
1588
+ follow >= text.length ||
1589
+ text.startsWith(SPILL_KEY_OPEN, follow) ||
1590
+ text.startsWith(SPILL_TOOL_CLOSE, follow)
1591
+ ) {
1592
+ return { end: at, next: at + SPILL_KEY_CLOSE.length };
1593
+ }
1594
+ wrong = text.indexOf(SPILL_KEY_CLOSE, at + 1);
1595
+ continue;
1596
+ }
1597
+ if (isSpillPairStart(text, at)) {
1598
+ let end = at;
1599
+ while (end > from && " \n\t\r".includes(text[end - 1]!)) end--;
1600
+ return { end, next: at };
1601
+ }
1602
+ open = text.indexOf(SPILL_KEY_OPEN, at + 1);
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Strictly parses a spill tail as `<arg_key>…</arg_key><arg_value>…` pairs,
1608
+ * tolerating a trailing `</tool_call>`. Returns null on any shape that is not
1609
+ * pure pair syntax — the caller then treats the text as ordinary content.
1610
+ */
1611
+ function parseSpilledPairs(text: string): [string, string][] | null {
1612
+ const pairs: [string, string][] = [];
1613
+ let at = skipSpillWhitespace(text, 0);
1614
+ while (at < text.length) {
1615
+ if (text.startsWith(SPILL_TOOL_CLOSE, at)) {
1616
+ at = skipSpillWhitespace(text, at + SPILL_TOOL_CLOSE.length);
1617
+ return at >= text.length ? pairs : null;
1618
+ }
1619
+ if (!text.startsWith(SPILL_KEY_OPEN, at)) return null;
1620
+ const keyStart = at + SPILL_KEY_OPEN.length;
1621
+ const keyEnd = text.indexOf(SPILL_KEY_CLOSE, keyStart);
1622
+ if (keyEnd === -1) return null;
1623
+ const key = text.slice(keyStart, keyEnd);
1624
+ if (!SPILL_KEY_PATTERN.test(key)) return null;
1625
+ at = skipSpillWhitespace(text, keyEnd + SPILL_KEY_CLOSE.length);
1626
+ if (!text.startsWith(SPILL_VALUE_OPEN, at)) return null;
1627
+ at += SPILL_VALUE_OPEN.length;
1628
+ const { end, next } = findSpillValueEnd(text, at);
1629
+ pairs.push([key, text.slice(at, end)]);
1630
+ at = skipSpillWhitespace(text, next);
1631
+ }
1632
+ return pairs;
1633
+ }
1634
+
1635
+ /**
1636
+ * Splits a contaminated string value at the earliest spill boundary: a
1637
+ * mistyped `</arg_key>` closer or an inlined next pair. Returns null when no
1638
+ * boundary yields a cleanly parseable tail.
1639
+ */
1640
+ function splitSpilledValue(text: string): SpillSplit | null {
1641
+ let wrong = text.indexOf(SPILL_KEY_CLOSE);
1642
+ let open = text.indexOf(SPILL_KEY_OPEN);
1643
+ while (wrong !== -1 || open !== -1) {
1644
+ if (wrong !== -1 && (open === -1 || wrong < open)) {
1645
+ const pairs = parseSpilledPairs(text.slice(wrong + SPILL_KEY_CLOSE.length));
1646
+ if (pairs) return { head: text.slice(0, wrong), pairs };
1647
+ wrong = text.indexOf(SPILL_KEY_CLOSE, wrong + 1);
1648
+ continue;
1649
+ }
1650
+ if (isSpillPairStart(text, open)) {
1651
+ const pairs = parseSpilledPairs(text.slice(open));
1652
+ if (pairs && pairs.length > 0) return { head: text.slice(0, open).trimEnd(), pairs };
1653
+ }
1654
+ open = text.indexOf(SPILL_KEY_OPEN, open + 1);
1655
+ }
1656
+ return null;
1657
+ }
1658
+
1659
+ /**
1660
+ * Repairs native tool-call arguments contaminated by in-band
1661
+ * `<arg_key>`/`<arg_value>` syntax. Some providers parse owned tool-call
1662
+ * formats server-side; when the model mistypes or omits an `</arg_value>`
1663
+ * closer, every following pair is swallowed into one string argument, e.g.
1664
+ * `op: "done</arg_key>\n<arg_key>task</arg_key>\n<arg_value>…"`. Truncates
1665
+ * each contaminated top-level string at its spill boundary and restores the
1666
+ * swallowed pairs as sibling arguments (never overwriting existing keys).
1667
+ *
1668
+ * Only invoked after validation and every coercion pass fail, so valid calls
1669
+ * whose string content legitimately contains tag-like text are never touched.
1670
+ */
1671
+ function healInbandArgSpill(value: unknown): { value: unknown; changed: boolean } {
1672
+ if (!isPlainRecord(value)) return { value, changed: false };
1673
+ let changed = false;
1674
+ const out: Record<string, unknown> = { ...value };
1675
+ const recovered: [string, string][] = [];
1676
+ for (const key in value) {
1677
+ const entry = value[key];
1678
+ if (typeof entry !== "string") continue;
1679
+ if (!entry.includes(SPILL_KEY_OPEN) && !entry.includes(SPILL_KEY_CLOSE)) continue;
1680
+ const split = splitSpilledValue(entry);
1681
+ if (!split) continue;
1682
+ out[key] = split.head;
1683
+ recovered.push(...split.pairs);
1684
+ changed = true;
1685
+ }
1686
+ if (!changed) return { value, changed: false };
1687
+ for (const [key, entry] of recovered) {
1688
+ if (!(key in out)) out[key] = entry;
1689
+ }
1690
+ return { value: out, changed: true };
1691
+ }
1692
+
1693
+ const MAX_COERCION_PASSES = 5;
1694
+
1695
+ /**
1696
+ * Finds a tool by name and validates the tool call arguments against its schema.
1697
+ * @param tools Array of tool definitions
1698
+ * @param toolCall The tool call from the LLM
1699
+ * @returns The validated arguments
1700
+ * @throws Error if tool is not found or validation fails
1701
+ */
1702
+ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"] {
1703
+ const tool = tools.find(t => t.name === toolCall.name);
1704
+ if (!tool) {
1705
+ throw new AIError.ToolNotFoundError(toolCall.name);
1706
+ }
1707
+ return validateToolArguments(tool, toolCall);
1708
+ }
1709
+
1710
+ /** Cap per-field string lengths when embedding received args in an error message. */
1711
+ const MAX_ERROR_ARG_STRING_LENGTH = 256;
1712
+
1713
+ function truncateArgsForError(value: unknown): unknown {
1714
+ if (typeof value === "string") {
1715
+ if (value.length <= MAX_ERROR_ARG_STRING_LENGTH) return value;
1716
+ return `${value.slice(0, MAX_ERROR_ARG_STRING_LENGTH)}… [truncated ${value.length - MAX_ERROR_ARG_STRING_LENGTH} chars]`;
1717
+ }
1718
+ if (Array.isArray(value)) return value.map(truncateArgsForError);
1719
+ if (value !== null && typeof value === "object") {
1720
+ const out: Record<string, unknown> = {};
1721
+ for (const [key, entry] of Object.entries(value)) out[key] = truncateArgsForError(entry);
1722
+ return out;
1723
+ }
1724
+ return value;
1725
+ }
1726
+
1727
+ /**
1728
+ * Validates tool call arguments against an ArkType or plain JSON Schema schema.
1729
+ * Applies conservative LLM-quirk normalization before declaring failure.
1730
+ *
1731
+ * @throws Error with a formatted message when validation cannot be reconciled.
1732
+ */
1733
+ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"] {
1734
+ const originalArgs = toolCall.arguments;
1735
+ if (originalArgs && typeof originalArgs === "object" && "__parseError" in originalArgs) {
1736
+ const parseError = originalArgs.__parseError;
1737
+ const rawJson = String(originalArgs.__rawJson ?? "");
1738
+ const maxLen = 512;
1739
+ const truncatedRawJson =
1740
+ rawJson.length <= maxLen
1741
+ ? rawJson
1742
+ : `${rawJson.slice(0, maxLen)}… [truncated ${rawJson.length - maxLen} chars]`;
1743
+ throw new AIError.ValidationError(
1744
+ `Validation failed for tool "${toolCall.name}": Tool call arguments are not valid JSON.\nParse Error: ${parseError}\nRaw JSON:\n${truncatedRawJson}`,
1745
+ );
1746
+ }
1747
+ const ctx = getValidationContext(tool);
1748
+ const { json } = ctx;
1749
+
1750
+ // Always normalize first — strip null/string "null" from optional fields,
1751
+ // strip optional empty strings only when their property schema rejects the
1752
+ // explicit value, and substitute defaults. Handles LLM outputting
1753
+ // placeholders for "no value" even when validation would otherwise pass.
1754
+ let normalizedArgs: unknown = originalArgs;
1755
+ let changed = false;
1756
+
1757
+ // Unwrap accidentally double-JSON-encoded object keys before any schema
1758
+ // pass. LLMs sometimes emit `{ "\"op\"": "done" }`, so the property name
1759
+ // arrives quote-wrapped; left alone it reads as an unrecognized key, gets
1760
+ // dropped by the coercion repair, and re-surfaces as a missing-required
1761
+ // error. Running first means every later pass sees the corrected names.
1762
+ const keyNormalization = normalizeDoubleEncodedKeys(normalizedArgs);
1763
+ if (keyNormalization.changed) {
1764
+ normalizedArgs = keyNormalization.value;
1765
+ changed = true;
1766
+ }
1767
+
1768
+ const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
1769
+ if (initialNormalization.changed) {
1770
+ normalizedArgs = initialNormalization.value;
1771
+ changed = true;
1772
+ }
1773
+
1774
+ const enumStringNormalization = normalizeEnumStringWhitespace(json, normalizedArgs);
1775
+ if (enumStringNormalization.changed) {
1776
+ normalizedArgs = enumStringNormalization.value;
1777
+ changed = true;
1778
+ }
1779
+
1780
+ // Strip trailing whitespace from string values on well-known
1781
+ // identifier-like property names (paths, URLs, titles). Some models tack
1782
+ // a newline onto a short-identifier arg from stream artifacts; downstream
1783
+ // tools then either fail to stat the target or annotate a "corrected
1784
+ // from" hint the model misreads as tool corruption.
1785
+ const identifierStringNormalization = normalizeIdentifierStringWhitespace(normalizedArgs);
1786
+ if (identifierStringNormalization.changed) {
1787
+ normalizedArgs = identifierStringNormalization.value;
1788
+ changed = true;
1789
+ }
1790
+
1791
+ // Then re-shape JSON-stringified arrays whose schema accepts both string
1792
+ // and array. Otherwise downstream tools receive the encoded string.
1793
+ const stringEncodedArrayNorm = normalizeStringEncodedArrayUnions(json, normalizedArgs);
1794
+ if (stringEncodedArrayNorm.changed) {
1795
+ normalizedArgs = stringEncodedArrayNorm.value;
1796
+ changed = true;
1797
+ }
1798
+
1799
+ const identifierStringNormalizationAfterArray = normalizeIdentifierStringWhitespace(normalizedArgs);
1800
+ if (identifierStringNormalizationAfterArray.changed) {
1801
+ normalizedArgs = identifierStringNormalizationAfterArray.value;
1802
+ changed = true;
1803
+ }
1804
+
1805
+ // Single-argument tools (e.g. `edit`): if the model put the lone required
1806
+ // string under a different key, adopt the first string field as that key.
1807
+ const singleStringNorm = normalizeSingleStringField(json, normalizedArgs);
1808
+ if (singleStringNorm.changed) {
1809
+ normalizedArgs = singleStringNorm.value;
1810
+ changed = true;
1811
+ }
1812
+
1813
+ let result = validateContext(ctx, normalizedArgs);
1814
+ if (result.success) return result.value as ToolCall["arguments"];
1815
+
1816
+ const coercionOutcome = runCoercionPasses(ctx, normalizedArgs, result);
1817
+ normalizedArgs = coercionOutcome.args;
1818
+ changed ||= coercionOutcome.changed;
1819
+ result = coercionOutcome.result;
1820
+ if (result.success) return result.value as ToolCall["arguments"];
1821
+
1822
+ // Last resort: some providers parse in-band tool-call syntax server-side,
1823
+ // and a mistyped/missing `</arg_value>` closer inlines the remaining pairs
1824
+ // into one string argument. Gated on validation failure so valid calls
1825
+ // with tag-like string content are never rewritten.
1826
+ const spillHeal = healInbandArgSpill(normalizedArgs);
1827
+ if (spillHeal.changed) {
1828
+ normalizedArgs = spillHeal.value;
1829
+ changed = true;
1830
+ result = validateContext(ctx, normalizedArgs);
1831
+ if (!result.success) {
1832
+ const healedOutcome = runCoercionPasses(ctx, normalizedArgs, result);
1833
+ normalizedArgs = healedOutcome.args;
1834
+ result = healedOutcome.result;
1835
+ }
1836
+ if (result.success) return result.value as ToolCall["arguments"];
1837
+ }
1838
+
1839
+ // Format validation errors nicely. The header phrase is asserted by
1840
+ // existing tests; the detailed body is informational.
1841
+ const errors = result.messages.join("\n") || "Unknown validation error";
1842
+
1843
+ // Truncate long per-field strings: the full payload (potentially hundreds
1844
+ // of KB for write/edit-class calls) would otherwise round-trip back to the
1845
+ // model inside the tool error.
1846
+ const receivedArgs = changed
1847
+ ? {
1848
+ original: truncateArgsForError(originalArgs),
1849
+ normalized: truncateArgsForError(normalizedArgs),
1850
+ }
1851
+ : truncateArgsForError(originalArgs);
1852
+
1853
+ const errorMessage = `Validation failed for tool "${
1854
+ toolCall.name
1855
+ }":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;
1856
+
1857
+ throw new AIError.ValidationError(errorMessage);
1858
+ }
1859
+
1860
+ /**
1861
+ * Runs up to {@link MAX_COERCION_PASSES} issue-driven coercion rounds,
1862
+ * re-applying the schema normalizations after each round because a coercion
1863
+ * may unwrap JSON-string containers and expose fields the pre-validation
1864
+ * passes could not reach.
1865
+ */
1866
+ function runCoercionPasses(
1867
+ ctx: ValidationContext,
1868
+ args: unknown,
1869
+ initial: ContextValidationResult,
1870
+ ): { args: unknown; result: ContextValidationResult; changed: boolean } {
1871
+ const { json } = ctx;
1872
+ let normalizedArgs = args;
1873
+ let result = initial;
1874
+ let changed = false;
1875
+ for (let pass = 0; pass < MAX_COERCION_PASSES; pass += 1) {
1876
+ if (result.success) break;
1877
+ const coercion = coerceArgsFromIssues(normalizedArgs, result.flatIssues);
1878
+ if (!coercion.changed) break;
1879
+
1880
+ normalizedArgs = coercion.value;
1881
+ changed = true;
1882
+
1883
+ // `coerceArgsFromIssues` may have just parsed a JSON-string container at
1884
+ // the root or a nested field, exposing double-encoded keys the initial
1885
+ // pass could not reach. Re-unwrap before the unrecognized-key repair on
1886
+ // the next validation pass would delete them.
1887
+ const keyNormalizationPass = normalizeDoubleEncodedKeys(normalizedArgs);
1888
+ if (keyNormalizationPass.changed) {
1889
+ normalizedArgs = keyNormalizationPass.value;
1890
+ }
1891
+
1892
+ const nullNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
1893
+ if (nullNormalization.changed) {
1894
+ normalizedArgs = nullNormalization.value;
1895
+ }
1896
+
1897
+ const enumStringNormalizationPass = normalizeEnumStringWhitespace(json, normalizedArgs);
1898
+ if (enumStringNormalizationPass.changed) {
1899
+ normalizedArgs = enumStringNormalizationPass.value;
1900
+ }
1901
+
1902
+ const identifierStringNormalizationPass = normalizeIdentifierStringWhitespace(normalizedArgs);
1903
+ if (identifierStringNormalizationPass.changed) {
1904
+ normalizedArgs = identifierStringNormalizationPass.value;
1905
+ }
1906
+
1907
+ // Re-run the union-string coercion because `coerceArgsFromIssues` may
1908
+ // have just unwrapped a JSON-stringified object at the root or inside a
1909
+ // nested field — exposing `string | string[]` descendants the initial
1910
+ // pre-validation pass could not reach.
1911
+ const stringEncodedArrayNormPass = normalizeStringEncodedArrayUnions(json, normalizedArgs);
1912
+ if (stringEncodedArrayNormPass.changed) {
1913
+ normalizedArgs = stringEncodedArrayNormPass.value;
1914
+ }
1915
+
1916
+ const identifierStringNormalizationAfterArrayPass = normalizeIdentifierStringWhitespace(normalizedArgs);
1917
+ if (identifierStringNormalizationAfterArrayPass.changed) {
1918
+ normalizedArgs = identifierStringNormalizationAfterArrayPass.value;
1919
+ }
1920
+
1921
+ // Re-run single-string remap: `coerceArgsFromIssues` may have just
1922
+ // unwrapped a JSON-stringified root object, exposing a mislabelled lone
1923
+ // string field the initial pre-pass could not see.
1924
+ const singleStringNormPass = normalizeSingleStringField(json, normalizedArgs);
1925
+ if (singleStringNormPass.changed) {
1926
+ normalizedArgs = singleStringNormPass.value;
1927
+ }
1928
+
1929
+ result = validateContext(ctx, normalizedArgs);
1930
+ }
1931
+ return { args: normalizedArgs, result, changed };
1932
+ }