@copilotkit/aimock 1.43.0 → 1.45.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 (396) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +14 -2
  5. package/dist/agui-handler.d.ts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +344 -43
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +345 -44
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +9 -5
  25. package/dist/chaos.cjs.map +1 -1
  26. package/dist/chaos.d.cts.map +1 -1
  27. package/dist/chaos.d.ts.map +1 -1
  28. package/dist/chaos.js +10 -7
  29. package/dist/chaos.js.map +1 -1
  30. package/dist/cli.cjs +238 -9
  31. package/dist/cli.cjs.map +1 -1
  32. package/dist/cli.js +242 -13
  33. package/dist/cli.js.map +1 -1
  34. package/dist/cohere.cjs +159 -72
  35. package/dist/cohere.cjs.map +1 -1
  36. package/dist/cohere.d.cts.map +1 -1
  37. package/dist/cohere.d.ts.map +1 -1
  38. package/dist/cohere.js +160 -73
  39. package/dist/cohere.js.map +1 -1
  40. package/dist/config-loader.cjs +63 -5
  41. package/dist/config-loader.cjs.map +1 -1
  42. package/dist/config-loader.d.cts +37 -4
  43. package/dist/config-loader.d.cts.map +1 -1
  44. package/dist/config-loader.d.ts +37 -4
  45. package/dist/config-loader.d.ts.map +1 -1
  46. package/dist/config-loader.js +63 -5
  47. package/dist/config-loader.js.map +1 -1
  48. package/dist/constants.cjs +8 -0
  49. package/dist/constants.cjs.map +1 -1
  50. package/dist/constants.d.cts +6 -2
  51. package/dist/constants.d.ts +6 -2
  52. package/dist/constants.js +8 -1
  53. package/dist/constants.js.map +1 -1
  54. package/dist/echo-text.cjs +65 -0
  55. package/dist/echo-text.cjs.map +1 -0
  56. package/dist/echo-text.d.cts +22 -0
  57. package/dist/echo-text.d.cts.map +1 -0
  58. package/dist/echo-text.d.ts +22 -0
  59. package/dist/echo-text.d.ts.map +1 -0
  60. package/dist/echo-text.js +65 -0
  61. package/dist/echo-text.js.map +1 -0
  62. package/dist/embeddings.cjs +18 -1
  63. package/dist/embeddings.cjs.map +1 -1
  64. package/dist/embeddings.d.cts +1 -1
  65. package/dist/embeddings.d.cts.map +1 -1
  66. package/dist/embeddings.d.ts +1 -1
  67. package/dist/embeddings.d.ts.map +1 -1
  68. package/dist/embeddings.js +18 -1
  69. package/dist/embeddings.js.map +1 -1
  70. package/dist/fakes-plugin.cjs +131 -0
  71. package/dist/fakes-plugin.cjs.map +1 -0
  72. package/dist/fakes-plugin.d.cts +23 -0
  73. package/dist/fakes-plugin.d.cts.map +1 -0
  74. package/dist/fakes-plugin.d.ts +23 -0
  75. package/dist/fakes-plugin.d.ts.map +1 -0
  76. package/dist/fakes-plugin.js +127 -0
  77. package/dist/fakes-plugin.js.map +1 -0
  78. package/dist/files.cjs +9 -11
  79. package/dist/files.cjs.map +1 -1
  80. package/dist/files.d.cts.map +1 -1
  81. package/dist/files.d.ts.map +1 -1
  82. package/dist/files.js +10 -13
  83. package/dist/files.js.map +1 -1
  84. package/dist/fine-tuning.cjs +1 -19
  85. package/dist/fine-tuning.cjs.map +1 -1
  86. package/dist/fine-tuning.d.cts.map +1 -1
  87. package/dist/fine-tuning.d.ts.map +1 -1
  88. package/dist/fine-tuning.js +2 -20
  89. package/dist/fine-tuning.js.map +1 -1
  90. package/dist/fixture-loader-services.cjs +116 -0
  91. package/dist/fixture-loader-services.cjs.map +1 -0
  92. package/dist/fixture-loader-services.d.cts +48 -0
  93. package/dist/fixture-loader-services.d.cts.map +1 -0
  94. package/dist/fixture-loader-services.d.ts +48 -0
  95. package/dist/fixture-loader-services.d.ts.map +1 -0
  96. package/dist/fixture-loader-services.js +114 -0
  97. package/dist/fixture-loader-services.js.map +1 -0
  98. package/dist/fixture-loader.cjs +552 -29
  99. package/dist/fixture-loader.cjs.map +1 -1
  100. package/dist/fixture-loader.d.cts +140 -2
  101. package/dist/fixture-loader.d.cts.map +1 -1
  102. package/dist/fixture-loader.d.ts +140 -2
  103. package/dist/fixture-loader.d.ts.map +1 -1
  104. package/dist/fixture-loader.js +542 -31
  105. package/dist/fixture-loader.js.map +1 -1
  106. package/dist/gemini-embeddings.cjs +42 -2
  107. package/dist/gemini-embeddings.cjs.map +1 -1
  108. package/dist/gemini-embeddings.js +43 -3
  109. package/dist/gemini-embeddings.js.map +1 -1
  110. package/dist/gemini-interactions.cjs +288 -52
  111. package/dist/gemini-interactions.cjs.map +1 -1
  112. package/dist/gemini-interactions.d.cts.map +1 -1
  113. package/dist/gemini-interactions.d.ts.map +1 -1
  114. package/dist/gemini-interactions.js +289 -53
  115. package/dist/gemini-interactions.js.map +1 -1
  116. package/dist/gemini.cjs +234 -38
  117. package/dist/gemini.cjs.map +1 -1
  118. package/dist/gemini.d.cts.map +1 -1
  119. package/dist/gemini.d.ts.map +1 -1
  120. package/dist/gemini.js +235 -39
  121. package/dist/gemini.js.map +1 -1
  122. package/dist/helpers.cjs +1157 -12
  123. package/dist/helpers.cjs.map +1 -1
  124. package/dist/helpers.d.cts +85 -4
  125. package/dist/helpers.d.cts.map +1 -1
  126. package/dist/helpers.d.ts +85 -4
  127. package/dist/helpers.d.ts.map +1 -1
  128. package/dist/helpers.js +1113 -14
  129. package/dist/helpers.js.map +1 -1
  130. package/dist/images.cjs +3 -2
  131. package/dist/images.cjs.map +1 -1
  132. package/dist/images.js +3 -2
  133. package/dist/images.js.map +1 -1
  134. package/dist/index.cjs +39 -1
  135. package/dist/index.d.cts +11 -4
  136. package/dist/index.d.ts +11 -4
  137. package/dist/index.js +11 -4
  138. package/dist/jest.cjs +43 -14
  139. package/dist/jest.cjs.map +1 -1
  140. package/dist/jest.d.cts +47 -5
  141. package/dist/jest.d.cts.map +1 -1
  142. package/dist/jest.d.ts +47 -5
  143. package/dist/jest.d.ts.map +1 -1
  144. package/dist/jest.js +45 -16
  145. package/dist/jest.js.map +1 -1
  146. package/dist/journal.cjs +155 -13
  147. package/dist/journal.cjs.map +1 -1
  148. package/dist/journal.d.cts +54 -4
  149. package/dist/journal.d.cts.map +1 -1
  150. package/dist/journal.d.ts +54 -4
  151. package/dist/journal.d.ts.map +1 -1
  152. package/dist/journal.js +155 -13
  153. package/dist/journal.js.map +1 -1
  154. package/dist/live-sanitize.cjs +4 -32
  155. package/dist/live-sanitize.cjs.map +1 -1
  156. package/dist/live-sanitize.js +4 -32
  157. package/dist/live-sanitize.js.map +1 -1
  158. package/dist/llmock.cjs +204 -39
  159. package/dist/llmock.cjs.map +1 -1
  160. package/dist/llmock.d.cts +51 -1
  161. package/dist/llmock.d.cts.map +1 -1
  162. package/dist/llmock.d.ts +51 -1
  163. package/dist/llmock.d.ts.map +1 -1
  164. package/dist/llmock.js +206 -41
  165. package/dist/llmock.js.map +1 -1
  166. package/dist/mcp-fakes-mount.cjs +217 -0
  167. package/dist/mcp-fakes-mount.cjs.map +1 -0
  168. package/dist/mcp-fakes-mount.js +211 -0
  169. package/dist/mcp-fakes-mount.js.map +1 -0
  170. package/dist/mcp-fakes-report.cjs +111 -0
  171. package/dist/mcp-fakes-report.cjs.map +1 -0
  172. package/dist/mcp-fakes-report.d.cts +40 -0
  173. package/dist/mcp-fakes-report.d.cts.map +1 -0
  174. package/dist/mcp-fakes-report.d.ts +40 -0
  175. package/dist/mcp-fakes-report.d.ts.map +1 -0
  176. package/dist/mcp-fakes-report.js +107 -0
  177. package/dist/mcp-fakes-report.js.map +1 -0
  178. package/dist/mcp-fakes.cjs +1942 -0
  179. package/dist/mcp-fakes.cjs.map +1 -0
  180. package/dist/mcp-fakes.d.cts +504 -0
  181. package/dist/mcp-fakes.d.cts.map +1 -0
  182. package/dist/mcp-fakes.d.ts +504 -0
  183. package/dist/mcp-fakes.d.ts.map +1 -0
  184. package/dist/mcp-fakes.js +1937 -0
  185. package/dist/mcp-fakes.js.map +1 -0
  186. package/dist/mcp-handler.cjs +534 -27
  187. package/dist/mcp-handler.cjs.map +1 -1
  188. package/dist/mcp-handler.d.cts +49 -0
  189. package/dist/mcp-handler.d.cts.map +1 -0
  190. package/dist/mcp-handler.d.ts +49 -0
  191. package/dist/mcp-handler.d.ts.map +1 -0
  192. package/dist/mcp-handler.js +530 -28
  193. package/dist/mcp-handler.js.map +1 -1
  194. package/dist/mcp-mock.cjs +326 -47
  195. package/dist/mcp-mock.cjs.map +1 -1
  196. package/dist/mcp-mock.d.cts +97 -1
  197. package/dist/mcp-mock.d.cts.map +1 -1
  198. package/dist/mcp-mock.d.ts +97 -1
  199. package/dist/mcp-mock.d.ts.map +1 -1
  200. package/dist/mcp-mock.js +327 -48
  201. package/dist/mcp-mock.js.map +1 -1
  202. package/dist/mcp-recorder.cjs +1224 -0
  203. package/dist/mcp-recorder.cjs.map +1 -0
  204. package/dist/mcp-recorder.d.cts +108 -0
  205. package/dist/mcp-recorder.d.cts.map +1 -0
  206. package/dist/mcp-recorder.d.ts +108 -0
  207. package/dist/mcp-recorder.d.ts.map +1 -0
  208. package/dist/mcp-recorder.js +1215 -0
  209. package/dist/mcp-recorder.js.map +1 -0
  210. package/dist/mcp-stub.cjs +5 -1
  211. package/dist/mcp-stub.d.cts +3 -1
  212. package/dist/mcp-stub.d.ts +3 -1
  213. package/dist/mcp-stub.js +3 -1
  214. package/dist/mcp-types.d.cts +6 -0
  215. package/dist/mcp-types.d.ts +6 -0
  216. package/dist/message-text.cjs +192 -0
  217. package/dist/message-text.cjs.map +1 -0
  218. package/dist/message-text.d.cts +31 -0
  219. package/dist/message-text.d.cts.map +1 -0
  220. package/dist/message-text.d.ts +31 -0
  221. package/dist/message-text.d.ts.map +1 -0
  222. package/dist/message-text.js +184 -0
  223. package/dist/message-text.js.map +1 -0
  224. package/dist/messages.cjs +400 -56
  225. package/dist/messages.cjs.map +1 -1
  226. package/dist/messages.d.cts.map +1 -1
  227. package/dist/messages.d.ts.map +1 -1
  228. package/dist/messages.js +401 -58
  229. package/dist/messages.js.map +1 -1
  230. package/dist/metrics.cjs +46 -66
  231. package/dist/metrics.cjs.map +1 -1
  232. package/dist/metrics.d.cts +14 -6
  233. package/dist/metrics.d.cts.map +1 -1
  234. package/dist/metrics.d.ts +14 -6
  235. package/dist/metrics.d.ts.map +1 -1
  236. package/dist/metrics.js +32 -40
  237. package/dist/metrics.js.map +1 -1
  238. package/dist/misbehavior.cjs +1523 -0
  239. package/dist/misbehavior.cjs.map +1 -0
  240. package/dist/misbehavior.d.cts +43 -0
  241. package/dist/misbehavior.d.cts.map +1 -0
  242. package/dist/misbehavior.d.ts +43 -0
  243. package/dist/misbehavior.d.ts.map +1 -0
  244. package/dist/misbehavior.js +1511 -0
  245. package/dist/misbehavior.js.map +1 -0
  246. package/dist/ollama.cjs +315 -113
  247. package/dist/ollama.cjs.map +1 -1
  248. package/dist/ollama.d.cts.map +1 -1
  249. package/dist/ollama.d.ts.map +1 -1
  250. package/dist/ollama.js +316 -114
  251. package/dist/ollama.js.map +1 -1
  252. package/dist/openapi.cjs +3405 -0
  253. package/dist/openapi.cjs.map +1 -0
  254. package/dist/openapi.d.cts +30 -0
  255. package/dist/openapi.d.cts.map +1 -0
  256. package/dist/openapi.d.ts +30 -0
  257. package/dist/openapi.d.ts.map +1 -0
  258. package/dist/openapi.js +3404 -0
  259. package/dist/openapi.js.map +1 -0
  260. package/dist/openrouter-video.cjs +4 -4
  261. package/dist/openrouter-video.cjs.map +1 -1
  262. package/dist/openrouter-video.d.cts.map +1 -1
  263. package/dist/openrouter-video.d.ts.map +1 -1
  264. package/dist/openrouter-video.js +4 -4
  265. package/dist/openrouter-video.js.map +1 -1
  266. package/dist/record-sanitize.cjs +231 -0
  267. package/dist/record-sanitize.cjs.map +1 -0
  268. package/dist/record-sanitize.js +224 -0
  269. package/dist/record-sanitize.js.map +1 -0
  270. package/dist/recorder.cjs +115 -6
  271. package/dist/recorder.cjs.map +1 -1
  272. package/dist/recorder.d.cts +2 -1
  273. package/dist/recorder.d.cts.map +1 -1
  274. package/dist/recorder.d.ts +2 -1
  275. package/dist/recorder.d.ts.map +1 -1
  276. package/dist/recorder.js +116 -8
  277. package/dist/recorder.js.map +1 -1
  278. package/dist/responses.cjs +701 -166
  279. package/dist/responses.cjs.map +1 -1
  280. package/dist/responses.d.cts +5 -13
  281. package/dist/responses.d.cts.map +1 -1
  282. package/dist/responses.d.ts +5 -13
  283. package/dist/responses.d.ts.map +1 -1
  284. package/dist/responses.js +699 -169
  285. package/dist/responses.js.map +1 -1
  286. package/dist/route-registry.cjs +951 -0
  287. package/dist/route-registry.cjs.map +1 -0
  288. package/dist/route-registry.d.cts +33 -0
  289. package/dist/route-registry.d.cts.map +1 -0
  290. package/dist/route-registry.d.ts +33 -0
  291. package/dist/route-registry.d.ts.map +1 -0
  292. package/dist/route-registry.js +866 -0
  293. package/dist/route-registry.js.map +1 -0
  294. package/dist/router.cjs +24 -2
  295. package/dist/router.cjs.map +1 -1
  296. package/dist/router.d.cts.map +1 -1
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +25 -4
  299. package/dist/router.js.map +1 -1
  300. package/dist/server.cjs +1159 -264
  301. package/dist/server.cjs.map +1 -1
  302. package/dist/server.d.cts +4 -1
  303. package/dist/server.d.cts.map +1 -1
  304. package/dist/server.d.ts +4 -1
  305. package/dist/server.d.ts.map +1 -1
  306. package/dist/server.js +1067 -172
  307. package/dist/server.js.map +1 -1
  308. package/dist/speech.cjs +4 -2
  309. package/dist/speech.cjs.map +1 -1
  310. package/dist/speech.js +4 -2
  311. package/dist/speech.js.map +1 -1
  312. package/dist/stream-collapse.cjs +236 -34
  313. package/dist/stream-collapse.cjs.map +1 -1
  314. package/dist/stream-collapse.d.cts +39 -4
  315. package/dist/stream-collapse.d.cts.map +1 -1
  316. package/dist/stream-collapse.d.ts +39 -4
  317. package/dist/stream-collapse.d.ts.map +1 -1
  318. package/dist/stream-collapse.js +235 -35
  319. package/dist/stream-collapse.js.map +1 -1
  320. package/dist/suite.cjs +1 -1
  321. package/dist/suite.js +1 -1
  322. package/dist/transcription.cjs +2 -2
  323. package/dist/transcription.cjs.map +1 -1
  324. package/dist/transcription.d.cts.map +1 -1
  325. package/dist/transcription.d.ts.map +1 -1
  326. package/dist/transcription.js +2 -2
  327. package/dist/transcription.js.map +1 -1
  328. package/dist/types.cjs +18 -0
  329. package/dist/types.cjs.map +1 -0
  330. package/dist/types.d.cts +516 -5
  331. package/dist/types.d.cts.map +1 -1
  332. package/dist/types.d.ts +516 -5
  333. package/dist/types.d.ts.map +1 -1
  334. package/dist/types.js +17 -0
  335. package/dist/types.js.map +1 -0
  336. package/dist/validate-cli.cjs +123 -33
  337. package/dist/validate-cli.cjs.map +1 -1
  338. package/dist/validate-cli.js +125 -35
  339. package/dist/validate-cli.js.map +1 -1
  340. package/dist/vector-stores.cjs +1180 -0
  341. package/dist/vector-stores.cjs.map +1 -0
  342. package/dist/vector-stores.d.cts +106 -0
  343. package/dist/vector-stores.d.cts.map +1 -0
  344. package/dist/vector-stores.d.ts +106 -0
  345. package/dist/vector-stores.d.ts.map +1 -0
  346. package/dist/vector-stores.js +1164 -0
  347. package/dist/vector-stores.js.map +1 -0
  348. package/dist/vector-types.d.cts.map +1 -1
  349. package/dist/version.cjs +45 -0
  350. package/dist/version.cjs.map +1 -0
  351. package/dist/version.js +44 -0
  352. package/dist/version.js.map +1 -0
  353. package/dist/video.cjs +19 -0
  354. package/dist/video.cjs.map +1 -1
  355. package/dist/video.d.cts.map +1 -1
  356. package/dist/video.d.ts.map +1 -1
  357. package/dist/video.js +19 -0
  358. package/dist/video.js.map +1 -1
  359. package/dist/vitest.cjs +77 -16
  360. package/dist/vitest.cjs.map +1 -1
  361. package/dist/vitest.d.cts +38 -5
  362. package/dist/vitest.d.cts.map +1 -1
  363. package/dist/vitest.d.ts +38 -5
  364. package/dist/vitest.d.ts.map +1 -1
  365. package/dist/vitest.js +78 -17
  366. package/dist/vitest.js.map +1 -1
  367. package/dist/watcher.cjs +6 -2
  368. package/dist/watcher.cjs.map +1 -1
  369. package/dist/watcher.js +6 -2
  370. package/dist/watcher.js.map +1 -1
  371. package/dist/ws-gemini-live.cjs +210 -101
  372. package/dist/ws-gemini-live.cjs.map +1 -1
  373. package/dist/ws-gemini-live.d.cts +9 -3
  374. package/dist/ws-gemini-live.d.cts.map +1 -1
  375. package/dist/ws-gemini-live.d.ts +9 -3
  376. package/dist/ws-gemini-live.d.ts.map +1 -1
  377. package/dist/ws-gemini-live.js +211 -102
  378. package/dist/ws-gemini-live.js.map +1 -1
  379. package/dist/ws-realtime.cjs +175 -62
  380. package/dist/ws-realtime.cjs.map +1 -1
  381. package/dist/ws-realtime.d.cts +8 -3
  382. package/dist/ws-realtime.d.cts.map +1 -1
  383. package/dist/ws-realtime.d.ts +8 -3
  384. package/dist/ws-realtime.d.ts.map +1 -1
  385. package/dist/ws-realtime.js +176 -63
  386. package/dist/ws-realtime.js.map +1 -1
  387. package/dist/ws-responses.cjs +121 -16
  388. package/dist/ws-responses.cjs.map +1 -1
  389. package/dist/ws-responses.d.cts +8 -12
  390. package/dist/ws-responses.d.cts.map +1 -1
  391. package/dist/ws-responses.d.ts +8 -12
  392. package/dist/ws-responses.d.ts.map +1 -1
  393. package/dist/ws-responses.js +123 -18
  394. package/dist/ws-responses.js.map +1 -1
  395. package/package.json +13 -2
  396. package/skills/write-fixtures/SKILL.md +179 -21
@@ -1 +1 @@
1
- {"version":3,"file":"fine-tuning.cjs","names":["applyChaosAsync","flattenHeaders","isJsonObject","generateId"],"sources":["../src/fine-tuning.ts"],"sourcesContent":["/**\n * OpenAI Fine-tuning jobs mock.\n *\n * Implements `POST /v1/fine_tuning/jobs`, `GET /v1/fine_tuning/jobs`,\n * `GET /v1/fine_tuning/jobs/{id}`, `POST .../cancel` and\n * `GET .../events` with a deterministic progression:\n *\n * validating_files → queued → running → succeeded\n *\n * Retrieve advances the job (first → queued, second → running,\n * third+ → succeeded with a fine-tuned model name). Cancel moves a\n * non-terminal job to `cancelled` and stamps `finished_at`; cancelling a job\n * that is already terminal (`succeeded`, `cancelled` or `failed`) is a 400.\n * Events are an append-only log: every status transition the job actually\n * went through appends exactly one event, so what a polling UI reads back is\n * the job's real history.\n *\n * In-memory per process, cleared by the full reset path, journaled as\n * `service: \"fine-tuning\"` and chaos-gated like the other job surfaces.\n *\n * Wire shapes verified 2026-09-15 against two sources, which agreed on every\n * required set below:\n * - openai/openai-openapi `openapi.yaml` v2.3.0 (`master`), schemas\n * `FineTuningJob`, `FineTuningJobEvent`, `ListPaginatedFineTuningJobsResponse`,\n * `ListFineTuningJobEventsResponse` and `CreateFineTuningJobRequest`.\n * - Vendored `openai` SDK 4.104.0: `resources/fine-tuning/jobs/jobs.d.ts`\n * (`FineTuningJob`, `FineTuningJob.Error`, `FineTuningJob.Hyperparameters`,\n * `FineTuningJobEvent`) and `pagination.d.ts` `CursorPageResponse`.\n *\n * What each source requires:\n * - `FineTuningJob` requires all fifteen of `id`, `created_at`, `error`,\n * `fine_tuned_model`, `finished_at`, `hyperparameters`, `model`, `object`,\n * `organization_id`, `result_files`, `seed`, `status`, `trained_tokens`,\n * `training_file`, `validation_file`. Five of them are nullable -\n * `error`, `fine_tuned_model`, `finished_at`, `trained_tokens` and\n * `validation_file` - and are emitted as `null` until the lifecycle fills\n * them in (`result_files` is required and non-nullable, so it starts as an\n * empty array rather than `null`). `integrations` and `method` are optional\n * in both sources and `metadata` is optional in the spec alone - SDK\n * 4.104.0's `FineTuningJob` and `JobCreateParams` do not declare it at all\n * (`grep -c metadata resources/fine-tuning/jobs/jobs.d.ts` is 0) - and all\n * three are emitted exactly when the create body carried them;\n * `estimated_finish` is optional and is never emitted.\n * - `FineTuningJobEvent` requires `id`, `object`, `created_at`, `level`,\n * `message`; `type` and `data` are optional (we always send `type`).\n * - Both list responses require `object`, `data`, `has_more`.\n * - `CreateFineTuningJobRequest` declares nine properties - `model`,\n * `training_file`, `hyperparameters`, `suffix`, `validation_file`,\n * `integrations`, `seed`, `method` and `metadata` - and every one of them\n * is read. That count is the spec's: SDK 4.104.0's `JobCreateParams`\n * declares the same set minus `metadata`. It types `validation_file` as a nullable string and each\n * `hyperparameters` member as `\"auto\"` or a number, with declared ranges we\n * enforce: `n_epochs` an integer 1-50, `batch_size` an integer 1-256,\n * `learning_rate_multiplier` a number strictly greater than 0\n * (`minimum: 0` plus `exclusiveMinimum: true`). `hyperparameters` itself is\n * `type: object` and is NOT marked `nullable`, so `null` is a 400 here.\n * `suffix` is a 1-64 character string that the spec's own example shows\n * landing in the fine-tuned model name\n * (`ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`), which is where this\n * mock puts it too; `seed` is an integer 0-2147483647; `metadata` is at\n * most 16 string pairs with 64-character keys and 512-character values;\n * and the per-method hyperparameter ranges come from\n * `FineTuneSupervisedHyperparameters`, `FineTuneDPOHyperparameters` (which\n * is where `beta`, 0 < beta <= 2, lives) and\n * `FineTuneReinforcementHyperparameters`.\n *\n * Not observed firsthand against api.openai.com. These details are ours, not\n * the vendor's: an event's id is `ftevent-` plus the job id's suffix (the job\n * id with its own `ftjob-` prefix stripped) plus the event's position in the\n * append-only log, which never renumbers, so an id always denotes the same\n * event across later reads and later transitions; every event is\n * `level: \"info\"` because this mock never emits a failure path; the 400 on\n * cancelling an already-terminal job is this mock's own contract - the spec\n * documents only a 200/`FineTuningJob` response for `cancelFineTuningJob` and\n * says nothing about repeat cancels; a `seed` the request did not carry is\n * derived from the job id (the spec says only \"if a seed is not specified, one\n * will be generated for you\"); `organization_id` is the fixed mock value\n * `org-aimock`; and the `result_files` entry and `trained_tokens` count\n * produced on success are deterministic stand-ins - `trained_tokens` may be 0,\n * which the spec permits (it declares `type: integer` with no `minimum`) even\n * on a succeeded job. Rejecting `validation_file: \"\"` is also ours; the spec\n * sets no `minLength` on either file id, but `training_file: \"\"` is already a\n * 400 and the two should not disagree. Four more rejections are ours, all of\n * the same shape - the spec marks none of these objects\n * `additionalProperties: false`, and this mock refuses to answer 200 having\n * ignored what the caller wrote: an unrecognized top-level create parameter, an\n * unrecognized member of `hyperparameters` or of a `method` variant (`beta`\n * belongs under `method.dpo.hyperparameters`, not in the deprecated top-level\n * object), an `integrations` list longer than the five `FineTuningJob` declares,\n * and a `limit` or `after` given more than once on a list route (the spec types\n * each as one scalar and gives no combining rule, so taking the first would\n * launder the others). Stamping `finished_at` on\n * cancel is an inference, not a vendor statement: neither source says what a\n * cancel does to the field. Both describe `finished_at` as \"null if the\n * fine-tuning job is still running\", and a cancelled job has stopped running,\n * so leaving it null would contradict that description - but the spec\n * documents only a 200/`FineTuningJob` for `cancelFineTuningJob` and never\n * shows the stamped body.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, generateId, isJsonObject } from \"./helpers.js\";\nimport { applyChaosAsync, type ChaosAsyncOutcome } from \"./chaos.js\";\nimport type { ChaosDefaults, JournalBody } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\nexport interface FineTuningJobEvent {\n id: string;\n object: \"fine_tuning.job.event\";\n created_at: number;\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n type: \"message\";\n}\n\nexport type FineTuningJobStatus =\n | \"validating_files\"\n | \"queued\"\n | \"running\"\n | \"succeeded\"\n | \"cancelled\"\n | \"failed\";\n\n/**\n * The statuses a job never leaves. Single source of truth: both `advance()`\n * (which refuses to move a terminal job) and the cancel handler (which rejects\n * a terminal job) read this set, so the two cannot drift apart again.\n */\nconst TERMINAL_STATUSES = new Set<FineTuningJobStatus>([\"succeeded\", \"cancelled\", \"failed\"]);\n\nfunction isTerminal(status: FineTuningJobStatus): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n\nexport interface FineTuningJobError {\n code: string;\n message: string;\n param: string | null;\n}\n\n/** Each member is `\"auto\"` or a number, per `CreateFineTuningJobRequest`. */\nexport interface FineTuningJobHyperparameters {\n n_epochs?: number | \"auto\";\n batch_size?: number | \"auto\";\n learning_rate_multiplier?: number | \"auto\";\n}\n\n/**\n * One `integrations` entry, per `FineTuningIntegration`: `wandb` is the only\n * type the spec declares, and `project` is the only member it requires.\n */\nexport interface FineTuningJobIntegration {\n type: \"wandb\";\n wandb: { project: string; name?: string | null; entity?: string | null; tags?: string[] };\n}\n\n/**\n * One `method` variant's configuration. `grader` is required by\n * `FineTuneReinforcementMethod` and only meaningful there; its own schema tree\n * is not modelled, so it is carried through as the JSON object the caller sent.\n */\nexport interface FineTuningJobMethodConfig {\n hyperparameters?: Record<string, number | string>;\n grader?: Record<string, unknown>;\n}\n\n/** `FineTuneMethod`: a required `type` plus that type's optional configuration. */\nexport interface FineTuningJobMethod {\n type: \"supervised\" | \"dpo\" | \"reinforcement\";\n supervised?: FineTuningJobMethodConfig;\n dpo?: FineTuningJobMethodConfig;\n reinforcement?: FineTuningJobMethodConfig;\n}\n\n/**\n * Every field the vendor marks required is non-optional here, nullable ones\n * included; the three the vendor marks optional are present only on a job\n * whose create body carried them.\n */\nexport interface FineTuningJob {\n id: string;\n object: \"fine_tuning.job\";\n model: string;\n training_file: string;\n validation_file: string | null;\n status: FineTuningJobStatus;\n created_at: number;\n finished_at: number | null;\n fine_tuned_model: string | null;\n hyperparameters: FineTuningJobHyperparameters;\n error: FineTuningJobError | null;\n organization_id: string;\n result_files: string[];\n seed: number;\n trained_tokens: number | null;\n integrations?: FineTuningJobIntegration[];\n method?: FineTuningJobMethod;\n metadata?: Record<string, string>;\n}\n\n/** Stable stand-in for the owning org; the vendor requires the field, not a value. */\nconst ORGANIZATION_ID = \"org-aimock\";\n\nconst jobs = new Map<string, FineTuningJob>();\nconst polls = new Map<string, number>();\n/**\n * The append-only event log per job id, kept beside the job rather than on it\n * so it never leaks into the `fine_tuning.job` payload. `appendEvent` is the\n * only writer of entries, and only by pushing, so an event's index — and\n * therefore its id — is fixed the moment it is recorded. The one other writer\n * is `clearFineTuningStore`, which empties the whole map: a reset drops every\n * log wholesale and never renumbers a surviving one.\n */\nconst eventLogs = new Map<string, FineTuningJobEvent[]>();\n/**\n * The last second this module stamped, which is what `stamp()` reads and\n * writes. Module-wide rather than per job: a per-job clamp says nothing about\n * two different jobs, and ordering ACROSS jobs is what the list endpoint's\n * newest-first page rests on.\n */\nlet lastStamp = 0;\n\n/**\n * The one clock the whole surface stamps from.\n *\n * The invariant: every timestamp this module ever exposes - every job's\n * `created_at`, every event's `created_at`, and every `finished_at` - is\n * non-decreasing in the order the module produced it, whichever way the wall\n * clock moves between two stamps, and across all jobs rather than within one.\n * The per-job reading is a consequence: a job's own stamps are a subsequence\n * of the module's, so they are non-decreasing too.\n *\n * Wall clock is the source, so a forward-running clock shows through\n * unchanged; a backwards step (NTP correction, a resumed VM, a test driving\n * `Date.now`) is absorbed by holding the previous value rather than emitting a\n * history that reads out of order. That makes this the only place a\n * fine-tuning timestamp may come from - a second clamp somewhere downstream is\n * exactly what used to be able to undo this one.\n */\nfunction stamp(): number {\n lastStamp = Math.max(Math.floor(Date.now() / 1000), lastStamp);\n return lastStamp;\n}\n\n/**\n * The `suffix` the create body asked for, per job id. Kept beside the job\n * because `suffix` is a member of `CreateFineTuningJobRequest` and NOT of\n * `fine_tuning.job`: storing it on the job would put a field on the wire that\n * the vendor never sends. It is read once, when the job succeeds and takes its\n * `fine_tuned_model` name.\n */\nconst suffixes = new Map<string, string>();\n\nexport function clearFineTuningStore(): void {\n jobs.clear();\n polls.clear();\n eventLogs.clear();\n lastStamp = 0;\n suffixes.clear();\n}\n\ntype Defaults = { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry };\n\n/**\n * Journal one fine-tuning request.\n *\n * `body` is the request body the handler actually parsed (create), and is null\n * in three cases: the bodyless GET/cancel routes, a create whose body never\n * parsed into a JSON object, and any request the chaos gate faulted — `chaosHit`\n * journals through `applyChaosAsync` with `body: null` because it rolls before\n * the body is read at all. Recording it otherwise is what makes \"inspect the\n * request you sent\" work against this surface: every create that got as far as\n * a parsed JSON object carries it, every rejection included, because those are\n * exactly the 400s whose offending payload a caller needs to read back.\n *\n * `path` is the raw request URL, query string INCLUDED. That is not cosmetic:\n * `entry.path` is the only place an untagged-by-header `?testId=` survives into\n * the journal, and `journalEntryTestId` (server.ts) parses it back out of\n * `entry.path` to serve `GET /__aimock/journal?testId=…`. Stripping the query\n * here — as `fal.ts` does with its `pathname` — silently drops every\n * query-tagged fine-tuning request out of its own test's slice. It also matches\n * the repo majority (images, responses, messages, moderation, embeddings,\n * rerank, cohere, gemini, search and vector all journal `req.url`). A caller\n * that wants route-level grouping should match on `service` or on a prefix,\n * not on string equality against a bare path.\n *\n * One route reaches a response without reaching this function: a\n * `POST /v1/fine_tuning/jobs` whose body read throws (over the size cap, or a\n * socket error) is caught in `server.ts` and answered 500 before\n * `handleFineTuningCreate` runs, so it is never journaled. That is deliberate\n * consistency, not an oversight — every sibling route's body-read catch in\n * `server.ts` (responses, messages, cohere, moderation, embeddings, rerank,\n * the video surfaces) writes the same unjournaled 500. Journaling it here alone\n * would make fine-tuning the one surface whose journal disagrees with the rest.\n *\n * `source: \"internal\"` marks the entry as served by aimock's own synthetic job\n * logic rather than a fixture or a proxy, which is the same value this file's\n * own chaos gate passes for a faulted request. It is a rare value elsewhere:\n * `grep -rn 'source: \"internal\"' src/*.ts` finds it on exactly two other\n * writers, `images.ts`'s removed-endpoint 404 and `openrouter-video.ts`'s\n * models listing synthesized after a failed proxy attempt — every other\n * service's synthesized journal writes `source: \"proxy\"`. Fine-tuning is the\n * one surface where every entry carries it, success and error alike, because\n * no fixture or proxy ever serves this store.\n */\nfunction journalFt(\n journal: Journal,\n method: string,\n path: string,\n headers: Record<string, string>,\n status: number,\n body: JournalBody | null = null,\n): void {\n journal.add({\n method,\n path,\n headers,\n body,\n service: \"fine-tuning\",\n response: { status, fixture: null, source: \"internal\" },\n });\n}\n\nfunction writeJson(\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction invalid(message: string): { error: { message: string; type: string } } {\n return { error: { message, type: \"invalid_request_error\" } };\n}\n\n/**\n * Roll the chaos dice for one fine-tuning request. Returns true when a fault\n * fired and the response is already written, so the caller returns early.\n *\n * CORS headers go on BEFORE the roll, because a fault writes the response\n * itself and never reaches `writeJson` (the only other place this file sets\n * them). Without this a browser client under chaos reads an opaque CORS\n * failure instead of the 429/500/malformed body the chaos config asked for —\n * exactly the fault it is trying to exercise, hidden. `setCorsHeaders` uses\n * `res.setHeader`, so the later `writeHead` on the non-fault path merges with\n * it rather than replacing it, and setting them twice is a no-op.\n * `src/images.ts` orders it the same way (CORS at the top of the handler,\n * chaos gate after).\n */\nasync function chaosHit(\n req: http.IncomingMessage,\n journal: Journal,\n defaults: Defaults,\n method: string,\n path: string,\n res: http.ServerResponse,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<ChaosAsyncOutcome> {\n setCorsHeaders(res);\n return await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"fine-tuning\",\n },\n \"internal\",\n defaults.registry,\n defaults.logger,\n );\n}\n\n/**\n * Record one thing that just happened to `job`. Call sites append only at the\n * moment of the transition they describe, so the log is the job's real\n * history rather than a reconstruction from its current status.\n *\n * `created_at` comes from `stamp()`, so a log that reads out of order is not\n * representable. Returns the second it stamped, which is what a terminal\n * transition assigns to `finished_at` so that nothing can follow the finish.\n */\nfunction appendEvent(job: FineTuningJob, message: string): number {\n const log = eventLogs.get(job.id) ?? [];\n const createdAt = stamp();\n log.push({\n id: `ftevent-${job.id.replace(/^ftjob-/, \"\")}-${log.length}`,\n object: \"fine_tuning.job.event\",\n created_at: createdAt,\n level: \"info\",\n message,\n type: \"message\",\n });\n eventLogs.set(job.id, log);\n return createdAt;\n}\n\nfunction advance(job: FineTuningJob): FineTuningJob {\n if (isTerminal(job.status)) {\n return job;\n }\n const n = (polls.get(job.id) ?? 0) + 1;\n polls.set(job.id, n);\n if (n === 1) {\n job.status = \"queued\";\n appendEvent(job, \"Training file validated, job queued\");\n } else if (n === 2) {\n job.status = \"running\";\n appendEvent(job, \"Training started\");\n } else {\n job.status = \"succeeded\";\n // `ft:<model>:<org>:<suffix>:<id>`, from the spec's own worked example for\n // `suffix`: \"a `suffix` of \"custom-model-name\" would produce a model name\n // like `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`\". A job created\n // without a `suffix` keeps the four-segment name this mock has always\n // emitted rather than inventing an empty segment the spec never shows.\n const suffix = suffixes.get(job.id);\n const tail = suffix === undefined ? \"\" : `${suffix}:`;\n job.fine_tuned_model = `ft:${job.model}:aimock:${tail}${job.id.slice(-6)}`;\n // `result_files` is required and NOT nullable, so it starts as an empty\n // array and gains its entry here; `trained_tokens` is the nullable one and\n // stays null until this point.\n job.result_files = [`file-${job.id.replace(/^ftjob-/, \"\")}`];\n job.trained_tokens = job.seed % 100000;\n // The finish IS the last event, so they share one stamp: `finished_at` can\n // be neither before an earlier event nor before the one announcing it.\n job.finished_at = appendEvent(job, `Training complete, model ${job.fine_tuned_model}`);\n }\n return job;\n}\n\nfunction eventsFor(job: FineTuningJob): FineTuningJobEvent[] {\n // A copy: callers order the page themselves (newest-first), and the\n // append-only log must never be reordered under them.\n return [...(eventLogs.get(job.id) ?? [])];\n}\n\n/** `limit` default for both fine-tuning list endpoints (openai-openapi v2.3.0). */\nconst DEFAULT_PAGE_LIMIT = 20;\n/** Upper bound we enforce on `limit`. Ours, not the vendor's — see `paginate`. */\nconst MAX_PAGE_LIMIT = 100;\n\nexport type Page<T> = { object: \"list\"; data: T[]; has_more: boolean };\nexport type PageResult<T> = { ok: true; page: Page<T> } | { ok: false; message: string };\n\n/**\n * The query parameters of a request target.\n *\n * The query is everything after the FIRST `?`, per RFC 3986 section 3.4\n * (\"The query component ... is terminated by a number sign (#) character or by\n * the end of the URI\") read together with RFC 9112 section 3.2: a request\n * target is an origin-form `absolute-path [ \"?\" query ]` and carries no\n * fragment component at all. Both halves of that matter here:\n *\n * - `split(\"?\")[1]` truncates at a SECOND `?`, which is a legal literal\n * inside a query, so `?a=1?b=2&limit=5` used to lose `limit` entirely.\n * Slicing from the first `?` keeps it.\n * - `new URL(target, base).searchParams` — what the router and the other\n * query-reading mocks use — truncates at a `#` instead, because `URL`\n * implements the fragment rule that a request target does not have. A\n * caller that sends `?after=abc#def` on the wire means the cursor\n * `abc#def`; reading it as `abc` is the same silent truncation one `?`\n * along, and it turns a bad cursor into a plausible-looking one. An origin\n * server sees no fragment, so neither does this.\n *\n * `new URLSearchParams(string)` cannot throw, so there is no parse failure to\n * swallow and no defaults-fallback to hide one: every byte of the target after\n * the first `?` is query data by definition. (A target malformed enough to\n * break `new URL` never reaches this module — `src/server.ts` builds a `URL`\n * to route on and fails there first.)\n */\nfunction queryParams(url: string | undefined): URLSearchParams {\n const target = url ?? \"/\";\n const start = target.indexOf(\"?\");\n return new URLSearchParams(start === -1 ? \"\" : target.slice(start + 1));\n}\n\n/**\n * The RAW query text a parameter was sent with - what the caller typed, before\n * percent- and `+`-decoding - or null when it was not sent at all.\n *\n * The unknown-`after` 400 below quotes the offending cursor, and the docs\n * promise it quotes \"what you sent\". `URLSearchParams` hands back the DECODED\n * value, so `?after=a%2Bb` was refused as `'a+b'` and `?after=a+b` as `'a b'` -\n * neither of which appears anywhere in the request the caller is staring at,\n * which is the one thing an error quoting a value is for.\n *\n * Pair splitting mirrors `URLSearchParams` itself, which splits on `&` alone\n * (`;` has not been a separator since the URL spec absorbed it), and the NAME\n * is matched through the platform's own decoder - one `URLSearchParams` per\n * pair - so an encoded spelling of the name still matches and nothing here\n * hand-rolls the percent codec. Only the VALUE is kept as raw text.\n */\nfunction rawParam(url: string | undefined, name: string): string | null {\n const target = url ?? \"/\";\n const start = target.indexOf(\"?\");\n if (start === -1) return null;\n for (const pair of target.slice(start + 1).split(\"&\")) {\n if (pair.length === 0) continue;\n const [decodedName] = new URLSearchParams(pair).keys();\n if (decodedName !== name) continue;\n const eq = pair.indexOf(\"=\");\n return eq === -1 ? \"\" : pair.slice(eq + 1);\n }\n return null;\n}\n\n/**\n * The single value of a query parameter, or the 400 a repeated one earns.\n *\n * `URLSearchParams.get` answers with the FIRST of a repeated parameter, which\n * launders the rest: `?limit=1&limit=abc` used to page at 1 and never look at\n * `abc`, so a caller building a URL twice over got a silent success where the\n * value it actually meant was invalid. The spec declares each of `limit` and\n * `after` as a single scalar (`listPaginatedFineTuningJobs`,\n * `listFineTuningEvents`) and gives no combining rule for a repeat, so a repeat\n * is a caller error here. That the 400 (rather than last-wins, or first-wins)\n * is ours, not the vendor's, is recorded in this module's header.\n */\nfunction singleParam(\n params: URLSearchParams,\n name: string,\n): { ok: true; value: string | null } | { ok: false; message: string } {\n const all = params.getAll(name);\n if (all.length > 1) {\n return {\n ok: false,\n message: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`,\n };\n }\n return { ok: true, value: all.length === 0 ? null : all[0] };\n}\n\n/**\n * A plain decimal integer, or null. Deliberately stricter than `Number()`,\n * which also accepts hex (`0x10` → 16), exponent notation (`1e2` → 100), a\n * leading sign (`+5`), a trailing `.0` and surrounding whitespace — none of\n * which is the \"integer\" the spec's `type: integer` or our own error message\n * describes, and all of which used to be honoured silently at some other\n * number than the one the caller wrote.\n *\n * Kept local rather than shared with the chaos config's own number parsing:\n * that one must accept fractional rates and so goes through `Number()`, which\n * is precisely the behaviour this function exists to refuse. Merging the two\n * would mean widening the lenient one, not renaming this one.\n */\nfunction parseDecimalInteger(raw: string): number | null {\n if (!/^\\d+$/.test(raw)) return null;\n const n = Number(raw);\n return Number.isSafeInteger(n) ? n : null;\n}\n\n/**\n * Cursor page over an array already ordered newest-first, mirroring the\n * `after`/`limit` params both fine-tuning list endpoints accept. `has_more`\n * reports whether anything was left behind, so it is never a constant.\n *\n * Sourced 2026-09-15 from openai/openai-openapi `openapi.yaml` v2.3.0\n * (`listPaginatedFineTuningJobs`, `listFineTuningEvents`) and the vendored\n * `openai` SDK 4.104.0 `pagination.d.ts`/`pagination.js` `CursorPage`:\n * - `after` is a free-form string id; `CursorPage.nextPageInfo()` feeds the\n * LAST id of the page back as `after`, and `hasNextPage()` stops on\n * `has_more: false`. Confirmed by the spec.\n * - `limit` is `type: integer, default: 20` on both endpoints. Confirmed by\n * the spec; the default is the vendor's.\n *\n * Three rules the spec does NOT state, chosen here and flagged as ours:\n * - Upper bound 100 on `limit`. The spec declares no `maximum`. We enforce\n * one so an oversized page fails loudly instead of being silently clamped,\n * and 100 is the number our own docs page already advertises.\n * - An `after` that matches no item is a 400 rather than a silent page 1.\n * Returning page 1 makes the SDK's `CursorPage` auto-paginator restart from\n * the top forever on a stale cursor; failing loudly is the whole point of a\n * mock. Not observed against api.openai.com.\n * - Newest-first ordering. Neither list endpoint accepts an `order` param and\n * the spec never states a direction; the `listFineTuningEvents` example\n * response lists the completion event ahead of the earlier\n * model-created event, which reads as reverse-chronological.\n */\nexport function paginate<T extends { id: string }>(\n items: T[],\n url: string | undefined,\n): PageResult<T> {\n const params = queryParams(url);\n const readLimit = singleParam(params, \"limit\");\n if (!readLimit.ok) return { ok: false, message: readLimit.message };\n const rawLimit = readLimit.value;\n let limit = DEFAULT_PAGE_LIMIT;\n if (rawLimit !== null) {\n const n = parseDecimalInteger(rawLimit);\n if (n === null || n < 1 || n > MAX_PAGE_LIMIT) {\n // Quoted from the RAW query text, like the `after` message below, so\n // `?limit=%2B5` is refused as `'%2B5'` rather than as the `'+5'` the\n // caller never wrote (see `rawParam`).\n return {\n ok: false,\n message: `Invalid parameter: 'limit' must be an integer between 1 and ${MAX_PAGE_LIMIT}, got '${rawParam(url, \"limit\") ?? rawLimit}'`,\n };\n }\n limit = n;\n }\n const readAfter = singleParam(params, \"after\");\n if (!readAfter.ok) return { ok: false, message: readAfter.message };\n const after = readAfter.value;\n let start = 0;\n if (after !== null) {\n const idx = items.findIndex((it) => it.id === after);\n if (idx === -1) {\n // Quoted so the message still names the offending value when the caller\n // sent `?after=`, which `URLSearchParams` reads as the empty string, and\n // quoted from the RAW query text so the value in the message is the one\n // the caller wrote rather than its decoding (see `rawParam`).\n return {\n ok: false,\n message: `Invalid parameter: 'after' is not a known cursor: '${rawParam(url, \"after\") ?? after}'`,\n };\n }\n start = idx + 1;\n }\n const rest = items.slice(start);\n return {\n ok: true,\n page: { object: \"list\", data: rest.slice(0, limit), has_more: rest.length > limit },\n };\n}\n\n/** Write a paginated list, or the 400 its `after`/`limit` params earned. */\nfunction writePage<T extends { id: string }>(\n res: http.ServerResponse,\n journal: Journal,\n req: http.IncomingMessage,\n method: string,\n path: string,\n items: T[],\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n const result = paginate(items, req.url);\n const status = result.ok ? 200 : 400;\n journalFt(journal, method, path, flattenHeaders(req.headers), status);\n writeJson(res, status, result.ok ? result.page : invalid(result.message), setCorsHeaders);\n}\n\n/**\n * Deterministic seed in the spec's `[0, 2147483647]` range, derived from the\n * job id so repeated reads of the same job report the same seed.\n */\nfunction seedFor(id: string): number {\n let h = 2166136261;\n for (let i = 0; i < id.length; i++) {\n h = Math.imul(h ^ id.charCodeAt(i), 16777619);\n }\n return (h >>> 0) % 2147483648;\n}\n\n/**\n * One hyperparameter's declared domain, in the shape `readHyper` reads it.\n *\n * The tag is explicit rather than inferred from which optional fields a rule\n * happens to carry: `\"integer\"` vs `\"number\"` IS the spec's `type:`, and a\n * table whose meaning depends on the absence of a key silently changes\n * behaviour the moment a bound is added or dropped.\n *\n * - `integer` / `number`: the spec spells each of these as a `oneOf` of the\n * string `\"auto\"` and a bounded numeric, so `\"auto\"` is legal for every\n * rule of either kind. `max: null` means the spec declares no upper bound.\n * - `enum`: a plain `type: string, enum: [...]` with NO `\"auto\"` member\n * (`reasoning_effort` is the only one), so `\"auto\"` is a 400 there.\n */\ntype HyperRule =\n | { kind: \"integer\"; min: number; max: number | null }\n | { kind: \"number\"; exclusiveMin: number; max: number | null }\n | { kind: \"enum\"; values: readonly string[] };\n\ntype HyperTable = Readonly<Record<string, HyperRule>>;\n\n/**\n * The three members `CreateFineTuningJobRequest.hyperparameters` and\n * `FineTuneSupervisedHyperparameters` both declare, quoted from\n * openai/openai-openapi `openapi.yaml` v2.3.0 (`master`):\n * - `n_epochs`: `type: integer, minimum: 1, maximum: 50`\n * - `batch_size`: `type: integer, minimum: 1, maximum: 256`\n * - `learning_rate_multiplier`: `type: number, minimum: 0,\n * exclusiveMinimum: true` — strictly greater than zero, hence no `max`.\n *\n * The vendored SDK's `JobCreateParams.Hyperparameters` types each member as\n * `\"auto\" | number`, which is as much as TypeScript can say; the bounds only\n * exist in the spec, so they are enforced here from it.\n */\nconst SUPERVISED_HYPER_RULES = {\n n_epochs: { kind: \"integer\", min: 1, max: 50 },\n batch_size: { kind: \"integer\", min: 1, max: 256 },\n learning_rate_multiplier: { kind: \"number\", exclusiveMin: 0, max: null },\n} as const satisfies HyperTable;\n\n/**\n * The deprecated top-level `hyperparameters` object. The spec declares exactly\n * the three supervised members on it — `beta` and the reinforcement knobs live\n * only under `method`, and the description says this whole object \"is now\n * deprecated in favor of `method`\".\n */\nconst LEGACY_HYPER_RULES = SUPERVISED_HYPER_RULES;\n\n/**\n * `FineTuneDPOHyperparameters`: the supervised three plus `beta`, declared\n * `type: number, minimum: 0, exclusiveMinimum: true, maximum: 2`.\n */\nconst DPO_HYPER_RULES = {\n ...SUPERVISED_HYPER_RULES,\n beta: { kind: \"number\", exclusiveMin: 0, max: 2 },\n} as const satisfies HyperTable;\n\n/**\n * `FineTuneReinforcementHyperparameters`: the supervised three plus\n * `reasoning_effort` (`enum: [default, low, medium, high]`, no `\"auto\"`),\n * `compute_multiplier` (`minimum: 0.00001, exclusiveMinimum: true,\n * maximum: 10`), and `eval_interval`/`eval_samples` (`type: integer,\n * minimum: 1`, no declared maximum).\n */\nconst REINFORCEMENT_HYPER_RULES = {\n ...SUPERVISED_HYPER_RULES,\n reasoning_effort: { kind: \"enum\", values: [\"default\", \"low\", \"medium\", \"high\"] },\n compute_multiplier: { kind: \"number\", exclusiveMin: 0.00001, max: 10 },\n eval_interval: { kind: \"integer\", min: 1, max: null },\n eval_samples: { kind: \"integer\", min: 1, max: null },\n} as const satisfies HyperTable;\n\n/** How a rule reads in an error message, so the caller learns the domain. */\nfunction describeRule(rule: HyperRule): string {\n if (rule.kind === \"enum\") {\n return `one of ${rule.values.map((v) => `\"${v}\"`).join(\", \")}`;\n }\n const upper = rule.max === null ? \"\" : ` and at most ${rule.max}`;\n const lower =\n rule.kind === \"integer\"\n ? `an integer at least ${rule.min}`\n : `a number greater than ${rule.exclusiveMin}`;\n return `${lower}${upper}, or the string \"auto\"`;\n}\n\n/**\n * Reads one hyperparameter against its rule. Anything outside the rule is a\n * caller error, never a silent drop — a mock that accepted `n_epochs: 0` would\n * let a bug reach the real API instead of failing where it is cheap to see.\n *\n * `path` names the object the member came from (`hyperparameters`,\n * `method.dpo.hyperparameters`, …) so the message points at the thing the\n * caller actually wrote.\n */\nfunction readHyper(\n table: HyperTable,\n path: string,\n key: string,\n value: unknown,\n): { ok: true; value: number | string } | { ok: false; message: string } {\n // Own property only: `table[key]` walks `Object.prototype`, so `toString`\n // and friends would resolve to an inherited function instead of `undefined`.\n const rule = Object.hasOwn(table, key) ? table[key] : undefined;\n // `readHyperObject` rejects an unknown key first, so this only fires for a\n // direct caller; it is the same 400 either way.\n if (rule === undefined) return { ok: false, message: unknownMemberMessage(table, path, key) };\n const bad = {\n ok: false as const,\n message: `Invalid parameter: '${path}.${key}' must be ${describeRule(rule)}`,\n };\n if (rule.kind === \"enum\") {\n return typeof value === \"string\" && rule.values.includes(value) ? { ok: true, value } : bad;\n }\n if (value === \"auto\") return { ok: true, value: \"auto\" };\n if (typeof value !== \"number\" || !Number.isFinite(value)) return bad;\n if (rule.max !== null && value > rule.max) return bad;\n if (rule.kind === \"integer\") {\n return value >= rule.min && Number.isInteger(value) ? { ok: true, value } : bad;\n }\n return value > rule.exclusiveMin ? { ok: true, value } : bad;\n}\n\n/** The 400 an unrecognised member of a rule-table-backed object earns. */\nfunction unknownMemberMessage(table: HyperTable, path: string, key: string): string {\n return (\n `Invalid parameter: '${path}.${key}' is not a recognized member of '${path}'; ` +\n `it accepts ${Object.keys(table).join(\", \")}`\n );\n}\n\n/**\n * Reads a whole hyperparameters object against its rule table.\n *\n * An unrecognised member is a 400 rather than a silent drop. The spec does not\n * mark these objects `additionalProperties: false`, so this rule is ours, not\n * the vendor's — and it is the point of the surface: `{n_epochs: 3, beta: 0.5}`\n * used to answer 200 echoing only `n_epochs`, so a caller who put a real DPO\n * hyperparameter (`FineTuneDPOHyperparameters.beta`) in the deprecated\n * top-level object instead of under `method.dpo.hyperparameters` was told it\n * had been accepted. Naming the members the object does take is what turns\n * that into a fixable message.\n */\nfunction readHyperObject(\n table: HyperTable,\n path: string,\n raw: Record<string, unknown>,\n): { ok: true; value: Record<string, number | string> } | { ok: false; message: string } {\n const out: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n // `key in table` would walk `Object.prototype`, letting `toString`,\n // `constructor`, `valueOf` and `__proto__` through this gate.\n if (!Object.hasOwn(table, key)) {\n return { ok: false, message: unknownMemberMessage(table, path, key) };\n }\n const read = readHyper(table, path, key, value);\n if (!read.ok) return read;\n out[key] = read.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Every property `CreateFineTuningJobRequest` declares (openai-openapi v2.3.0).\n *\n * Each one is read below; nothing on this list is accepted and dropped, and\n * anything NOT on it is a 400. The spec does not mark the request object\n * `additionalProperties: false`, so rejecting an unknown key is this mock's\n * rule, not the vendor's: a create body is written once and reused for months,\n * and a typo that answers 200 having ignored the field is exactly the bug a\n * mock exists to catch.\n */\nconst CREATE_KEYS = [\n \"model\",\n \"training_file\",\n \"hyperparameters\",\n \"suffix\",\n \"validation_file\",\n \"integrations\",\n \"seed\",\n \"method\",\n \"metadata\",\n] as const;\n\n/** The members of the deprecated top-level `hyperparameters`, as the job echoes them. */\nconst LEGACY_HYPER_KEYS = [\n \"n_epochs\",\n \"batch_size\",\n \"learning_rate_multiplier\",\n] as const satisfies readonly (keyof typeof LEGACY_HYPER_RULES)[];\n\n/** `Metadata` in the spec: at most 16 pairs, keys <= 64 chars, values <= 512. */\nconst METADATA_MAX_PAIRS = 16;\nconst METADATA_MAX_KEY = 64;\nconst METADATA_MAX_VALUE = 512;\n\n/** `CreateFineTuningJobRequest.suffix`: `minLength: 1, maxLength: 64`. */\nconst SUFFIX_MAX_LENGTH = 64;\n\n/** `CreateFineTuningJobRequest.seed`: `minimum: 0, maximum: 2147483647`. */\nconst SEED_MAX = 2147483647;\n\n/** `FineTuningJob.integrations`: `maxItems: 5`. */\nconst MAX_INTEGRATIONS = 5;\n\nexport type Read<T> = { ok: true; value: T } | { ok: false; message: string };\n\n/**\n * Length in Unicode CODE POINTS, which is the unit the spec's limits are in.\n *\n * Every `maxLength` quoted in this module is a pydantic `max_length` on the\n * vendor's side, and pydantic counts code points; `String.prototype.length`\n * counts UTF-16 code units, which is the same number only while the string\n * stays inside the BMP. One astral character - an emoji, a rarer CJK\n * ideograph, a musical symbol - counts two there, so a `suffix` of exactly 64\n * characters holding one used to earn a 400 the real API would not send, and a\n * caller that trusted the mock would have shortened a name that was legal.\n */\nfunction codePointLength(value: string): number {\n return [...value].length;\n}\n\n/**\n * `suffix`, per its spec description: \"A string of up to 64 characters that\n * will be added to your fine-tuned model name. For example, a `suffix` of\n * \"custom-model-name\" would produce a model name like\n * `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`.\" It is declared\n * `type: string, minLength: 1, maxLength: 64, nullable: true, default: null`,\n * so `null` means \"no suffix\" and `\"\"` is a 400.\n */\nfunction readSuffix(value: unknown): Read<string | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (\n typeof value !== \"string\" ||\n value.length === 0 ||\n codePointLength(value) > SUFFIX_MAX_LENGTH\n ) {\n return {\n ok: false,\n message: `Invalid parameter: 'suffix' must be a string of 1 to ${SUFFIX_MAX_LENGTH} characters or null`,\n };\n }\n return { ok: true, value };\n}\n\n/**\n * `seed`, per its spec description: \"The seed controls the reproducibility of\n * the job ... If a seed is not specified, one will be generated for you\", and\n * declared `type: integer, nullable: true, minimum: 0, maximum: 2147483647`.\n * `null` and absence both mean \"generate one\", which is what `seedFor` does.\n */\nfunction readSeed(value: unknown): Read<number | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0 || value > SEED_MAX) {\n return {\n ok: false,\n message: `Invalid parameter: 'seed' must be an integer between 0 and ${SEED_MAX} or null`,\n };\n }\n return { ok: true, value };\n}\n\n/**\n * `metadata`, per the shared `Metadata` schema: an object of string values, at\n * most 16 pairs, keys at most 64 characters and values at most 512, or `null`.\n * `FineTuningJob` declares `metadata` too, so what is accepted is echoed back.\n */\nexport function readMetadata(value: unknown): Read<Record<string, string> | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!isJsonObject(value)) {\n return { ok: false, message: \"Invalid parameter: 'metadata' must be a JSON object or null\" };\n }\n const entries = Object.entries(value);\n if (entries.length > METADATA_MAX_PAIRS) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata' accepts at most ${METADATA_MAX_PAIRS} key-value pairs, got ${entries.length}`,\n };\n }\n // `metadata` keys are the caller's own namespace, so `__proto__` is an\n // ordinary key here and is echoed back like any other. Writing it onto an\n // object literal would instead hit `Object.prototype`'s `__proto__` setter,\n // which ignores a string value silently - a 200 that dropped the pair, and a\n // pair count that no longer matches what reads back.\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const [key, entry] of entries) {\n const keyLength = codePointLength(key);\n if (keyLength > METADATA_MAX_KEY) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata' keys are at most ${METADATA_MAX_KEY} characters, got ${keyLength}`,\n };\n }\n if (typeof entry !== \"string\" || codePointLength(entry) > METADATA_MAX_VALUE) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata.${key}' must be a string of at most ${METADATA_MAX_VALUE} characters`,\n };\n }\n out[key] = entry;\n }\n return { ok: true, value: out };\n}\n\n/**\n * One `integrations` entry. The spec requires both `type` and `wandb`, types\n * `type` as the single-member enum `wandb`, and requires `project` inside\n * `wandb`; `name` and `entity` are nullable strings and `tags` an array of\n * strings. `FineTuningJob.integrations` caps the list at `maxItems: 5` — the\n * request schema declares no cap, so enforcing the response's here is ours.\n */\nfunction readIntegration(index: number, raw: unknown): Read<FineTuningJobIntegration> {\n const at = `integrations[${index}]`;\n if (!isJsonObject(raw)) {\n return { ok: false, message: `Invalid parameter: '${at}' must be a JSON object` };\n }\n for (const key of Object.keys(raw)) {\n if (key !== \"type\" && key !== \"wandb\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.${key}' is not a recognized member of '${at}'; it accepts type, wandb`,\n };\n }\n }\n if (raw[\"type\"] !== \"wandb\") {\n return { ok: false, message: `Invalid parameter: '${at}.type' must be the string \"wandb\"` };\n }\n const wandb = raw[\"wandb\"];\n if (!isJsonObject(wandb)) {\n return { ok: false, message: `Invalid parameter: '${at}.wandb' must be a JSON object` };\n }\n for (const key of Object.keys(wandb)) {\n if (key !== \"project\" && key !== \"name\" && key !== \"entity\" && key !== \"tags\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.${key}' is not a recognized member of '${at}.wandb'; it accepts project, name, entity, tags`,\n };\n }\n }\n const project = wandb[\"project\"];\n if (typeof project !== \"string\" || project.length === 0) {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.project' must be a non-empty string`,\n };\n }\n const value: FineTuningJobIntegration = { type: \"wandb\", wandb: { project } };\n for (const key of [\"name\", \"entity\"] as const) {\n const member = wandb[key];\n if (member === undefined) continue;\n if (member !== null && typeof member !== \"string\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.${key}' must be a string or null`,\n };\n }\n value.wandb[key] = member;\n }\n const tags = wandb[\"tags\"];\n if (tags !== undefined) {\n if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== \"string\")) {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.tags' must be an array of strings`,\n };\n }\n value.wandb.tags = tags.filter((tag): tag is string => typeof tag === \"string\");\n }\n return { ok: true, value };\n}\n\n/** `integrations`: a nullable array of the entries above. */\nfunction readIntegrations(value: unknown): Read<FineTuningJobIntegration[] | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!Array.isArray(value)) {\n return { ok: false, message: \"Invalid parameter: 'integrations' must be an array or null\" };\n }\n if (value.length > MAX_INTEGRATIONS) {\n return {\n ok: false,\n message: `Invalid parameter: 'integrations' accepts at most ${MAX_INTEGRATIONS} entries, got ${value.length}`,\n };\n }\n const out: FineTuningJobIntegration[] = [];\n for (const [index, entry] of value.entries()) {\n const read = readIntegration(index, entry);\n if (!read.ok) return read;\n out.push(read.value);\n }\n return { ok: true, value: out };\n}\n\n/** The rule table each `method` variant's `hyperparameters` is read against. */\nconst METHOD_HYPER_RULES = {\n supervised: SUPERVISED_HYPER_RULES,\n dpo: DPO_HYPER_RULES,\n reinforcement: REINFORCEMENT_HYPER_RULES,\n} as const;\n\nconst METHOD_TYPES = [\"supervised\", \"dpo\", \"reinforcement\"] as const;\n\n/**\n * `method`, per `FineTuneMethod`: `type` is required and is one of\n * `supervised`, `dpo`, `reinforcement`, and each of those names an optional\n * sub-object carrying that method's `hyperparameters`. `FineTuneReinforcementMethod`\n * additionally requires a `grader`, so `type: \"reinforcement\"` requires one\n * whether or not the `reinforcement` sub-object is present at all — the\n * requirement is keyed on the chosen `type`, not on the sub-object happening\n * to be there.\n *\n * Only the chosen type's sub-object is accepted. `FineTuneMethod` leaves all\n * three optional and says nothing about a configuration for a type that was\n * not chosen, and neither does SDK 4.104.0's `JobCreateParams.Method`; every\n * example in the spec pairs a `type` with that type's own configuration and\n * no other (`createFineTuningJob`, the \"Reinforcement\" example). With the real\n * API's behaviour uncitable, a stray configuration is rejected rather than\n * accepted-and-ignored: this mock never takes a member it then drops.\n *\n * The grader itself (`GraderStringCheck` | `GraderTextSimilarity` |\n * `GraderPython` | `GraderScoreModel` | `GraderMulti`) is a schema tree of its\n * own that this mock does not model: it is required to be a JSON object and is\n * then echoed back verbatim on the job. That is a deliberate pass-through, not\n * a drop — what the caller sent is what the caller reads back.\n *\n * `FineTuningJob` declares `method`, so an accepted method is echoed.\n */\nfunction readMethod(value: unknown): Read<FineTuningJobMethod | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!isJsonObject(value)) {\n return { ok: false, message: \"Invalid parameter: 'method' must be a JSON object or null\" };\n }\n for (const key of Object.keys(value)) {\n if (key !== \"type\" && !METHOD_TYPES.some((t) => t === key)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${key}' is not a recognized member of 'method'; it accepts type, ${METHOD_TYPES.join(\", \")}`,\n };\n }\n }\n const type = METHOD_TYPES.find((t) => t === value[\"type\"]);\n if (type === undefined) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.type' must be one of ${METHOD_TYPES.map((t) => `\"${t}\"`).join(\", \")}`,\n };\n }\n const method: FineTuningJobMethod = { type };\n for (const variant of METHOD_TYPES) {\n if (variant !== type && value[variant] !== undefined) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${variant}' configures the \"${variant}\" method, but 'method.type' is \"${type}\"; a method carries only its own configuration`,\n };\n }\n }\n const supplied = value[type];\n // A reinforcement method requires a `grader`; an absent `reinforcement`\n // object is therefore an empty one, read here so the `grader` check below\n // rejects it instead of the method slipping through unconfigured.\n const raw = supplied === undefined && type === \"reinforcement\" ? {} : supplied;\n if (raw !== undefined) {\n if (!isJsonObject(raw)) {\n return { ok: false, message: `Invalid parameter: 'method.${type}' must be a JSON object` };\n }\n const allowed = type === \"reinforcement\" ? [\"grader\", \"hyperparameters\"] : [\"hyperparameters\"];\n for (const key of Object.keys(raw)) {\n if (!allowed.includes(key)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${type}.${key}' is not a recognized member of 'method.${type}'; it accepts ${allowed.join(\", \")}`,\n };\n }\n }\n const config: FineTuningJobMethodConfig = {};\n if (type === \"reinforcement\") {\n const grader = raw[\"grader\"];\n if (!isJsonObject(grader)) {\n return {\n ok: false,\n message:\n \"Invalid parameter: 'method.reinforcement.grader' is required and must be a JSON object\",\n };\n }\n config.grader = grader;\n }\n const hyper = raw[\"hyperparameters\"];\n if (hyper !== undefined) {\n if (!isJsonObject(hyper)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${type}.hyperparameters' must be a JSON object`,\n };\n }\n const read = readHyperObject(\n METHOD_HYPER_RULES[type],\n `method.${type}.hyperparameters`,\n hyper,\n );\n if (!read.ok) return read;\n config.hyperparameters = read.value;\n }\n method[type] = config;\n }\n return { ok: true, value: method };\n}\n\nexport async function handleFineTuningCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/fine_tuning/jobs\";\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n let body: unknown;\n try {\n body = JSON.parse(raw) as unknown;\n } catch (err) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(`Malformed JSON: ${err instanceof Error ? err.message : \"unknown\"}`),\n setCorsHeaders,\n );\n return;\n }\n if (!isJsonObject(body)) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(\"Request body must be a JSON object\"), setCorsHeaders);\n return;\n }\n // Recorded verbatim rather than reshaped into a synthetic chat request that\n // would misreport what the caller actually sent. `JournalBody` covers the\n // non-chat member, so this needs no cast — and must not have one: the body\n // is still unvalidated here (the checks below are what reject it), so any\n // claim about its shape at this point would be a claim about arbitrary JSON.\n const journaledBody: JournalBody = body;\n /**\n * Every rejection below journals the parsed body before answering: these are\n * exactly the 400s whose offending payload a caller needs to read back.\n */\n const reject = (message: string): void => {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400, journaledBody);\n writeJson(res, 400, invalid(message), setCorsHeaders);\n };\n for (const key of Object.keys(body)) {\n if (!CREATE_KEYS.some((known) => known === key)) {\n reject(\n `Invalid parameter: '${key}' is not a recognized request parameter; this endpoint accepts ${CREATE_KEYS.join(\", \")}`,\n );\n return;\n }\n }\n const trainingFile = body[\"training_file\"];\n const model = body[\"model\"];\n if (typeof trainingFile !== \"string\" || trainingFile.length === 0) {\n reject(\"Invalid parameter: 'training_file' must be a non-empty string\");\n return;\n }\n if (typeof model !== \"string\" || model.length === 0) {\n reject(\"Invalid parameter: 'model' must be a non-empty string\");\n return;\n }\n const validationFile = body[\"validation_file\"];\n // `null` is legal - the spec marks `validation_file` `nullable: true` - but\n // `\"\"` is not a file id. The spec sets no `minLength` on either file id; the\n // empty-string rejection is this mock's, and it matches `training_file`\n // above rather than disagreeing with it.\n if (\n validationFile !== undefined &&\n validationFile !== null &&\n (typeof validationFile !== \"string\" || validationFile.length === 0)\n ) {\n reject(\"Invalid parameter: 'validation_file' must be a non-empty string or null\");\n return;\n }\n const hyper = body[\"hyperparameters\"];\n // `null` is rejected with everything else that is not an object: the spec\n // types `hyperparameters` as `type: object` and, unlike its siblings\n // `suffix`, `validation_file` and `integrations`, never marks it\n // `nullable: true`.\n if (hyper !== undefined && !isJsonObject(hyper)) {\n reject(\"Invalid parameter: 'hyperparameters' must be a JSON object\");\n return;\n }\n const hyperparameters: FineTuningJobHyperparameters = {};\n if (isJsonObject(hyper)) {\n const read = readHyperObject(LEGACY_HYPER_RULES, \"hyperparameters\", hyper);\n if (!read.ok) {\n reject(read.message);\n return;\n }\n for (const key of LEGACY_HYPER_KEYS) {\n const value = read.value[key];\n // Every rule in this table is an `integer`/`number` kind, so a value\n // that survived it is a number or the string \"auto\". The guard proves\n // that to the type; it is not a filter, and cannot drop anything.\n if (typeof value === \"number\" || value === \"auto\") hyperparameters[key] = value;\n }\n }\n const suffix = readSuffix(body[\"suffix\"]);\n if (!suffix.ok) {\n reject(suffix.message);\n return;\n }\n const seed = readSeed(body[\"seed\"]);\n if (!seed.ok) {\n reject(seed.message);\n return;\n }\n const integrations = readIntegrations(body[\"integrations\"]);\n if (!integrations.ok) {\n reject(integrations.message);\n return;\n }\n const jobMethod = readMethod(body[\"method\"]);\n if (!jobMethod.ok) {\n reject(jobMethod.message);\n return;\n }\n const metadata = readMetadata(body[\"metadata\"]);\n if (!metadata.ok) {\n reject(metadata.message);\n return;\n }\n const id = generateId(\"ftjob\");\n const job: FineTuningJob = {\n id,\n object: \"fine_tuning.job\",\n model,\n training_file: trainingFile,\n validation_file: typeof validationFile === \"string\" ? validationFile : null,\n status: \"validating_files\",\n created_at: stamp(),\n finished_at: null,\n fine_tuned_model: null,\n hyperparameters,\n error: null,\n organization_id: ORGANIZATION_ID,\n result_files: [],\n seed: seed.value ?? seedFor(id),\n trained_tokens: null,\n };\n // The three optional job properties are set only when the caller sent them:\n // the spec lists none of them in `FineTuningJob.required`, so emitting\n // `\"method\": null` on every job would be a field the vendor never sends.\n if (integrations.value !== null) job.integrations = integrations.value;\n if (jobMethod.value !== null) job.method = jobMethod.value;\n if (metadata.value !== null) job.metadata = metadata.value;\n // `suffix` is NOT a property of `fine_tuning.job`, so it is kept beside the\n // job rather than on it, the way the event log is; it is observable only\n // through the `fine_tuned_model` name the job takes when it succeeds.\n if (suffix.value !== null) suffixes.set(job.id, suffix.value);\n jobs.set(job.id, job);\n polls.set(job.id, 0);\n appendEvent(job, \"Job created, validating training file\");\n journalFt(journal, method, path, flattenHeaders(req.headers), 200, journaledBody);\n writeJson(res, 200, job, setCorsHeaders);\n}\n\nexport async function handleFineTuningList(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/fine_tuning/jobs\";\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n // Newest-first. `stamp()` is monotonic module-wide, so insertion order is\n // unconditionally non-decreasing in `created_at` - a job created later never\n // carries an earlier second, whichever way the wall clock moved - and\n // reversing it is therefore both newest-first and the stable total order a\n // cursor walk needs. Sorting on `created_at` would not be: it has one-second\n // resolution, so ties are the normal case and a sort cannot order them.\n const all = [...jobs.values()].reverse();\n writePage(res, journal, req, method, path, all, setCorsHeaders);\n}\n\nexport async function handleFineTuningRetrieve(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}`;\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n journalFt(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, advance(job), setCorsHeaders);\n}\n\nexport async function handleFineTuningCancel(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}/cancel`;\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n if (isTerminal(job.status)) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(`Job ${id} is already terminal (${job.status})`), setCorsHeaders);\n return;\n }\n job.status = \"cancelled\";\n // A cancelled job is finished, and the wire type declares `finished_at` null\n // only \"if the fine-tuning job is still running\" - so stamp it the same way\n // `advance()` stamps it on `succeeded`: from the cancellation event itself,\n // which keeps the finish at or after every timestamp that preceded it.\n job.finished_at = appendEvent(job, \"Job cancelled by user\");\n journalFt(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, job, setCorsHeaders);\n}\n\nexport async function handleFineTuningEvents(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}/events`;\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n writePage(res, journal, req, method, path, eventsFor(job).reverse(), setCorsHeaders);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fine-tuning.cjs","names":["applyChaosAsync","flattenHeaders","parseStrictIntegerText","isJsonObject","generateId"],"sources":["../src/fine-tuning.ts"],"sourcesContent":["/**\n * OpenAI Fine-tuning jobs mock.\n *\n * Implements `POST /v1/fine_tuning/jobs`, `GET /v1/fine_tuning/jobs`,\n * `GET /v1/fine_tuning/jobs/{id}`, `POST .../cancel` and\n * `GET .../events` with a deterministic progression:\n *\n * validating_files → queued → running → succeeded\n *\n * Retrieve advances the job (first → queued, second → running,\n * third+ → succeeded with a fine-tuned model name). Cancel moves a\n * non-terminal job to `cancelled` and stamps `finished_at`; cancelling a job\n * that is already terminal (`succeeded`, `cancelled` or `failed`) is a 400.\n * Events are an append-only log: every status transition the job actually\n * went through appends exactly one event, so what a polling UI reads back is\n * the job's real history.\n *\n * In-memory per process, cleared by the full reset path, journaled as\n * `service: \"fine-tuning\"` and chaos-gated like the other job surfaces.\n *\n * Wire shapes verified 2026-09-15 against two sources, which agreed on every\n * required set below:\n * - openai/openai-openapi `openapi.yaml` v2.3.0 (`master`), schemas\n * `FineTuningJob`, `FineTuningJobEvent`, `ListPaginatedFineTuningJobsResponse`,\n * `ListFineTuningJobEventsResponse` and `CreateFineTuningJobRequest`.\n * - Vendored `openai` SDK 4.104.0: `resources/fine-tuning/jobs/jobs.d.ts`\n * (`FineTuningJob`, `FineTuningJob.Error`, `FineTuningJob.Hyperparameters`,\n * `FineTuningJobEvent`) and `pagination.d.ts` `CursorPageResponse`.\n *\n * What each source requires:\n * - `FineTuningJob` requires all fifteen of `id`, `created_at`, `error`,\n * `fine_tuned_model`, `finished_at`, `hyperparameters`, `model`, `object`,\n * `organization_id`, `result_files`, `seed`, `status`, `trained_tokens`,\n * `training_file`, `validation_file`. Five of them are nullable -\n * `error`, `fine_tuned_model`, `finished_at`, `trained_tokens` and\n * `validation_file` - and are emitted as `null` until the lifecycle fills\n * them in (`result_files` is required and non-nullable, so it starts as an\n * empty array rather than `null`). `integrations` and `method` are optional\n * in both sources and `metadata` is optional in the spec alone - SDK\n * 4.104.0's `FineTuningJob` and `JobCreateParams` do not declare it at all\n * (`grep -c metadata resources/fine-tuning/jobs/jobs.d.ts` is 0) - and all\n * three are emitted exactly when the create body carried them;\n * `estimated_finish` is optional and is never emitted.\n * - `FineTuningJobEvent` requires `id`, `object`, `created_at`, `level`,\n * `message`; `type` and `data` are optional (we always send `type`).\n * - Both list responses require `object`, `data`, `has_more`.\n * - `CreateFineTuningJobRequest` declares nine properties - `model`,\n * `training_file`, `hyperparameters`, `suffix`, `validation_file`,\n * `integrations`, `seed`, `method` and `metadata` - and every one of them\n * is read. That count is the spec's: SDK 4.104.0's `JobCreateParams`\n * declares the same set minus `metadata`. It types `validation_file` as a nullable string and each\n * `hyperparameters` member as `\"auto\"` or a number, with declared ranges we\n * enforce: `n_epochs` an integer 1-50, `batch_size` an integer 1-256,\n * `learning_rate_multiplier` a number strictly greater than 0\n * (`minimum: 0` plus `exclusiveMinimum: true`). `hyperparameters` itself is\n * `type: object` and is NOT marked `nullable`, so `null` is a 400 here.\n * `suffix` is a 1-64 character string that the spec's own example shows\n * landing in the fine-tuned model name\n * (`ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`), which is where this\n * mock puts it too; `seed` is an integer 0-2147483647; `metadata` is at\n * most 16 string pairs with 64-character keys and 512-character values;\n * and the per-method hyperparameter ranges come from\n * `FineTuneSupervisedHyperparameters`, `FineTuneDPOHyperparameters` (which\n * is where `beta`, 0 < beta <= 2, lives) and\n * `FineTuneReinforcementHyperparameters`.\n *\n * Not observed firsthand against api.openai.com. These details are ours, not\n * the vendor's: an event's id is `ftevent-` plus the job id's suffix (the job\n * id with its own `ftjob-` prefix stripped) plus the event's position in the\n * append-only log, which never renumbers, so an id always denotes the same\n * event across later reads and later transitions; every event is\n * `level: \"info\"` because this mock never emits a failure path; the 400 on\n * cancelling an already-terminal job is this mock's own contract - the spec\n * documents only a 200/`FineTuningJob` response for `cancelFineTuningJob` and\n * says nothing about repeat cancels; a `seed` the request did not carry is\n * derived from the job id (the spec says only \"if a seed is not specified, one\n * will be generated for you\"); `organization_id` is the fixed mock value\n * `org-aimock`; and the `result_files` entry and `trained_tokens` count\n * produced on success are deterministic stand-ins - `trained_tokens` may be 0,\n * which the spec permits (it declares `type: integer` with no `minimum`) even\n * on a succeeded job. Rejecting `validation_file: \"\"` is also ours; the spec\n * sets no `minLength` on either file id, but `training_file: \"\"` is already a\n * 400 and the two should not disagree. Four more rejections are ours, all of\n * the same shape - the spec marks none of these objects\n * `additionalProperties: false`, and this mock refuses to answer 200 having\n * ignored what the caller wrote: an unrecognized top-level create parameter, an\n * unrecognized member of `hyperparameters` or of a `method` variant (`beta`\n * belongs under `method.dpo.hyperparameters`, not in the deprecated top-level\n * object), an `integrations` list longer than the five `FineTuningJob` declares,\n * and a `limit` or `after` given more than once on a list route (the spec types\n * each as one scalar and gives no combining rule, so taking the first would\n * launder the others). Stamping `finished_at` on\n * cancel is an inference, not a vendor statement: neither source says what a\n * cancel does to the field. Both describe `finished_at` as \"null if the\n * fine-tuning job is still running\", and a cancelled job has stopped running,\n * so leaving it null would contradict that description - but the spec\n * documents only a 200/`FineTuningJob` for `cancelFineTuningJob` and never\n * shows the stamped body.\n */\n\nimport type * as http from \"node:http\";\nimport { flattenHeaders, generateId, isJsonObject, parseStrictIntegerText } from \"./helpers.js\";\nimport { applyChaosAsync, type ChaosAsyncOutcome } from \"./chaos.js\";\nimport type { ChaosDefaults, JournalBody } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\n\nexport interface FineTuningJobEvent {\n id: string;\n object: \"fine_tuning.job.event\";\n created_at: number;\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n type: \"message\";\n}\n\nexport type FineTuningJobStatus =\n | \"validating_files\"\n | \"queued\"\n | \"running\"\n | \"succeeded\"\n | \"cancelled\"\n | \"failed\";\n\n/**\n * The statuses a job never leaves. Single source of truth: both `advance()`\n * (which refuses to move a terminal job) and the cancel handler (which rejects\n * a terminal job) read this set, so the two cannot drift apart again.\n */\nconst TERMINAL_STATUSES = new Set<FineTuningJobStatus>([\"succeeded\", \"cancelled\", \"failed\"]);\n\nfunction isTerminal(status: FineTuningJobStatus): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n\nexport interface FineTuningJobError {\n code: string;\n message: string;\n param: string | null;\n}\n\n/** Each member is `\"auto\"` or a number, per `CreateFineTuningJobRequest`. */\nexport interface FineTuningJobHyperparameters {\n n_epochs?: number | \"auto\";\n batch_size?: number | \"auto\";\n learning_rate_multiplier?: number | \"auto\";\n}\n\n/**\n * One `integrations` entry, per `FineTuningIntegration`: `wandb` is the only\n * type the spec declares, and `project` is the only member it requires.\n */\nexport interface FineTuningJobIntegration {\n type: \"wandb\";\n wandb: { project: string; name?: string | null; entity?: string | null; tags?: string[] };\n}\n\n/**\n * One `method` variant's configuration. `grader` is required by\n * `FineTuneReinforcementMethod` and only meaningful there; its own schema tree\n * is not modelled, so it is carried through as the JSON object the caller sent.\n */\nexport interface FineTuningJobMethodConfig {\n hyperparameters?: Record<string, number | string>;\n grader?: Record<string, unknown>;\n}\n\n/** `FineTuneMethod`: a required `type` plus that type's optional configuration. */\nexport interface FineTuningJobMethod {\n type: \"supervised\" | \"dpo\" | \"reinforcement\";\n supervised?: FineTuningJobMethodConfig;\n dpo?: FineTuningJobMethodConfig;\n reinforcement?: FineTuningJobMethodConfig;\n}\n\n/**\n * Every field the vendor marks required is non-optional here, nullable ones\n * included; the three the vendor marks optional are present only on a job\n * whose create body carried them.\n */\nexport interface FineTuningJob {\n id: string;\n object: \"fine_tuning.job\";\n model: string;\n training_file: string;\n validation_file: string | null;\n status: FineTuningJobStatus;\n created_at: number;\n finished_at: number | null;\n fine_tuned_model: string | null;\n hyperparameters: FineTuningJobHyperparameters;\n error: FineTuningJobError | null;\n organization_id: string;\n result_files: string[];\n seed: number;\n trained_tokens: number | null;\n integrations?: FineTuningJobIntegration[];\n method?: FineTuningJobMethod;\n metadata?: Record<string, string>;\n}\n\n/** Stable stand-in for the owning org; the vendor requires the field, not a value. */\nconst ORGANIZATION_ID = \"org-aimock\";\n\nconst jobs = new Map<string, FineTuningJob>();\nconst polls = new Map<string, number>();\n/**\n * The append-only event log per job id, kept beside the job rather than on it\n * so it never leaks into the `fine_tuning.job` payload. `appendEvent` is the\n * only writer of entries, and only by pushing, so an event's index — and\n * therefore its id — is fixed the moment it is recorded. The one other writer\n * is `clearFineTuningStore`, which empties the whole map: a reset drops every\n * log wholesale and never renumbers a surviving one.\n */\nconst eventLogs = new Map<string, FineTuningJobEvent[]>();\n/**\n * The last second this module stamped, which is what `stamp()` reads and\n * writes. Module-wide rather than per job: a per-job clamp says nothing about\n * two different jobs, and ordering ACROSS jobs is what the list endpoint's\n * newest-first page rests on.\n */\nlet lastStamp = 0;\n\n/**\n * The one clock the whole surface stamps from.\n *\n * The invariant: every timestamp this module ever exposes - every job's\n * `created_at`, every event's `created_at`, and every `finished_at` - is\n * non-decreasing in the order the module produced it, whichever way the wall\n * clock moves between two stamps, and across all jobs rather than within one.\n * The per-job reading is a consequence: a job's own stamps are a subsequence\n * of the module's, so they are non-decreasing too.\n *\n * Wall clock is the source, so a forward-running clock shows through\n * unchanged; a backwards step (NTP correction, a resumed VM, a test driving\n * `Date.now`) is absorbed by holding the previous value rather than emitting a\n * history that reads out of order. That makes this the only place a\n * fine-tuning timestamp may come from - a second clamp somewhere downstream is\n * exactly what used to be able to undo this one.\n */\nfunction stamp(): number {\n lastStamp = Math.max(Math.floor(Date.now() / 1000), lastStamp);\n return lastStamp;\n}\n\n/**\n * The `suffix` the create body asked for, per job id. Kept beside the job\n * because `suffix` is a member of `CreateFineTuningJobRequest` and NOT of\n * `fine_tuning.job`: storing it on the job would put a field on the wire that\n * the vendor never sends. It is read once, when the job succeeds and takes its\n * `fine_tuned_model` name.\n */\nconst suffixes = new Map<string, string>();\n\nexport function clearFineTuningStore(): void {\n jobs.clear();\n polls.clear();\n eventLogs.clear();\n lastStamp = 0;\n suffixes.clear();\n}\n\ntype Defaults = { logger: Logger; chaos?: ChaosDefaults; registry?: MetricsRegistry };\n\n/**\n * Journal one fine-tuning request.\n *\n * `body` is the request body the handler actually parsed (create), and is null\n * in three cases: the bodyless GET/cancel routes, a create whose body never\n * parsed into a JSON object, and any request the chaos gate faulted — `chaosHit`\n * journals through `applyChaosAsync` with `body: null` because it rolls before\n * the body is read at all. Recording it otherwise is what makes \"inspect the\n * request you sent\" work against this surface: every create that got as far as\n * a parsed JSON object carries it, every rejection included, because those are\n * exactly the 400s whose offending payload a caller needs to read back.\n *\n * `path` is the raw request URL, query string INCLUDED. That is not cosmetic:\n * `entry.path` is the only place an untagged-by-header `?testId=` survives into\n * the journal, and `journalEntryTestId` (server.ts) parses it back out of\n * `entry.path` to serve `GET /__aimock/journal?testId=…`. Stripping the query\n * here — as `fal.ts` does with its `pathname` — silently drops every\n * query-tagged fine-tuning request out of its own test's slice. It also matches\n * the repo majority (images, responses, messages, moderation, embeddings,\n * rerank, cohere, gemini, search and vector all journal `req.url`). A caller\n * that wants route-level grouping should match on `service` or on a prefix,\n * not on string equality against a bare path.\n *\n * One route reaches a response without reaching this function: a\n * `POST /v1/fine_tuning/jobs` whose body read throws (over the size cap, or a\n * socket error) is caught in `server.ts` and answered 500 before\n * `handleFineTuningCreate` runs, so it is never journaled. That is deliberate\n * consistency, not an oversight — every sibling route's body-read catch in\n * `server.ts` (responses, messages, cohere, moderation, embeddings, rerank,\n * the video surfaces) writes the same unjournaled 500. Journaling it here alone\n * would make fine-tuning the one surface whose journal disagrees with the rest.\n *\n * `source: \"internal\"` marks the entry as served by aimock's own synthetic job\n * logic rather than a fixture or a proxy, which is the same value this file's\n * own chaos gate passes for a faulted request. It is a rare value elsewhere:\n * `grep -rn 'source: \"internal\"' src/*.ts` finds it on exactly two other\n * writers, `images.ts`'s removed-endpoint 404 and `openrouter-video.ts`'s\n * models listing synthesized after a failed proxy attempt — every other\n * service's synthesized journal writes `source: \"proxy\"`. Fine-tuning is the\n * one surface where every entry carries it, success and error alike, because\n * no fixture or proxy ever serves this store.\n */\nfunction journalFt(\n journal: Journal,\n method: string,\n path: string,\n headers: Record<string, string>,\n status: number,\n body: JournalBody | null = null,\n): void {\n journal.add({\n method,\n path,\n headers,\n body,\n service: \"fine-tuning\",\n response: { status, fixture: null, source: \"internal\" },\n });\n}\n\nfunction writeJson(\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction invalid(message: string): { error: { message: string; type: string } } {\n return { error: { message, type: \"invalid_request_error\" } };\n}\n\n/**\n * Roll the chaos dice for one fine-tuning request. Returns true when a fault\n * fired and the response is already written, so the caller returns early.\n *\n * CORS headers go on BEFORE the roll, because a fault writes the response\n * itself and never reaches `writeJson` (the only other place this file sets\n * them). Without this a browser client under chaos reads an opaque CORS\n * failure instead of the 429/500/malformed body the chaos config asked for —\n * exactly the fault it is trying to exercise, hidden. `setCorsHeaders` uses\n * `res.setHeader`, so the later `writeHead` on the non-fault path merges with\n * it rather than replacing it, and setting them twice is a no-op.\n * `src/images.ts` orders it the same way (CORS at the top of the handler,\n * chaos gate after).\n */\nasync function chaosHit(\n req: http.IncomingMessage,\n journal: Journal,\n defaults: Defaults,\n method: string,\n path: string,\n res: http.ServerResponse,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<ChaosAsyncOutcome> {\n setCorsHeaders(res);\n return await applyChaosAsync(\n res,\n null,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"fine-tuning\",\n },\n \"internal\",\n defaults.registry,\n defaults.logger,\n );\n}\n\n/**\n * Record one thing that just happened to `job`. Call sites append only at the\n * moment of the transition they describe, so the log is the job's real\n * history rather than a reconstruction from its current status.\n *\n * `created_at` comes from `stamp()`, so a log that reads out of order is not\n * representable. Returns the second it stamped, which is what a terminal\n * transition assigns to `finished_at` so that nothing can follow the finish.\n */\nfunction appendEvent(job: FineTuningJob, message: string): number {\n const log = eventLogs.get(job.id) ?? [];\n const createdAt = stamp();\n log.push({\n id: `ftevent-${job.id.replace(/^ftjob-/, \"\")}-${log.length}`,\n object: \"fine_tuning.job.event\",\n created_at: createdAt,\n level: \"info\",\n message,\n type: \"message\",\n });\n eventLogs.set(job.id, log);\n return createdAt;\n}\n\nfunction advance(job: FineTuningJob): FineTuningJob {\n if (isTerminal(job.status)) {\n return job;\n }\n const n = (polls.get(job.id) ?? 0) + 1;\n polls.set(job.id, n);\n if (n === 1) {\n job.status = \"queued\";\n appendEvent(job, \"Training file validated, job queued\");\n } else if (n === 2) {\n job.status = \"running\";\n appendEvent(job, \"Training started\");\n } else {\n job.status = \"succeeded\";\n // `ft:<model>:<org>:<suffix>:<id>`, from the spec's own worked example for\n // `suffix`: \"a `suffix` of \"custom-model-name\" would produce a model name\n // like `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`\". A job created\n // without a `suffix` keeps the four-segment name this mock has always\n // emitted rather than inventing an empty segment the spec never shows.\n const suffix = suffixes.get(job.id);\n const tail = suffix === undefined ? \"\" : `${suffix}:`;\n job.fine_tuned_model = `ft:${job.model}:aimock:${tail}${job.id.slice(-6)}`;\n // `result_files` is required and NOT nullable, so it starts as an empty\n // array and gains its entry here; `trained_tokens` is the nullable one and\n // stays null until this point.\n job.result_files = [`file-${job.id.replace(/^ftjob-/, \"\")}`];\n job.trained_tokens = job.seed % 100000;\n // The finish IS the last event, so they share one stamp: `finished_at` can\n // be neither before an earlier event nor before the one announcing it.\n job.finished_at = appendEvent(job, `Training complete, model ${job.fine_tuned_model}`);\n }\n return job;\n}\n\nfunction eventsFor(job: FineTuningJob): FineTuningJobEvent[] {\n // A copy: callers order the page themselves (newest-first), and the\n // append-only log must never be reordered under them.\n return [...(eventLogs.get(job.id) ?? [])];\n}\n\n/** `limit` default for both fine-tuning list endpoints (openai-openapi v2.3.0). */\nconst DEFAULT_PAGE_LIMIT = 20;\n/** Upper bound we enforce on `limit`. Ours, not the vendor's — see `paginate`. */\nconst MAX_PAGE_LIMIT = 100;\n\nexport type Page<T> = { object: \"list\"; data: T[]; has_more: boolean };\nexport type PageResult<T> = { ok: true; page: Page<T> } | { ok: false; message: string };\n\n/**\n * The query parameters of a request target.\n *\n * The query is everything after the FIRST `?`, per RFC 3986 section 3.4\n * (\"The query component ... is terminated by a number sign (#) character or by\n * the end of the URI\") read together with RFC 9112 section 3.2: a request\n * target is an origin-form `absolute-path [ \"?\" query ]` and carries no\n * fragment component at all. Both halves of that matter here:\n *\n * - `split(\"?\")[1]` truncates at a SECOND `?`, which is a legal literal\n * inside a query, so `?a=1?b=2&limit=5` used to lose `limit` entirely.\n * Slicing from the first `?` keeps it.\n * - `new URL(target, base).searchParams` — what the router and the other\n * query-reading mocks use — truncates at a `#` instead, because `URL`\n * implements the fragment rule that a request target does not have. A\n * caller that sends `?after=abc#def` on the wire means the cursor\n * `abc#def`; reading it as `abc` is the same silent truncation one `?`\n * along, and it turns a bad cursor into a plausible-looking one. An origin\n * server sees no fragment, so neither does this.\n *\n * `new URLSearchParams(string)` cannot throw, so there is no parse failure to\n * swallow and no defaults-fallback to hide one: every byte of the target after\n * the first `?` is query data by definition. (A target malformed enough to\n * break `new URL` never reaches this module — `src/server.ts` builds a `URL`\n * to route on and fails there first.)\n */\nfunction queryParams(url: string | undefined): URLSearchParams {\n const target = url ?? \"/\";\n const start = target.indexOf(\"?\");\n return new URLSearchParams(start === -1 ? \"\" : target.slice(start + 1));\n}\n\n/**\n * The RAW query text a parameter was sent with - what the caller typed, before\n * percent- and `+`-decoding - or null when it was not sent at all.\n *\n * The unknown-`after` 400 below quotes the offending cursor, and the docs\n * promise it quotes \"what you sent\". `URLSearchParams` hands back the DECODED\n * value, so `?after=a%2Bb` was refused as `'a+b'` and `?after=a+b` as `'a b'` -\n * neither of which appears anywhere in the request the caller is staring at,\n * which is the one thing an error quoting a value is for.\n *\n * Pair splitting mirrors `URLSearchParams` itself, which splits on `&` alone\n * (`;` has not been a separator since the URL spec absorbed it), and the NAME\n * is matched through the platform's own decoder - one `URLSearchParams` per\n * pair - so an encoded spelling of the name still matches and nothing here\n * hand-rolls the percent codec. Only the VALUE is kept as raw text.\n */\nfunction rawParam(url: string | undefined, name: string): string | null {\n const target = url ?? \"/\";\n const start = target.indexOf(\"?\");\n if (start === -1) return null;\n for (const pair of target.slice(start + 1).split(\"&\")) {\n if (pair.length === 0) continue;\n const [decodedName] = new URLSearchParams(pair).keys();\n if (decodedName !== name) continue;\n const eq = pair.indexOf(\"=\");\n return eq === -1 ? \"\" : pair.slice(eq + 1);\n }\n return null;\n}\n\n/**\n * The single value of a query parameter, or the 400 a repeated one earns.\n *\n * `URLSearchParams.get` answers with the FIRST of a repeated parameter, which\n * launders the rest: `?limit=1&limit=abc` used to page at 1 and never look at\n * `abc`, so a caller building a URL twice over got a silent success where the\n * value it actually meant was invalid. The spec declares each of `limit` and\n * `after` as a single scalar (`listPaginatedFineTuningJobs`,\n * `listFineTuningEvents`) and gives no combining rule for a repeat, so a repeat\n * is a caller error here. That the 400 (rather than last-wins, or first-wins)\n * is ours, not the vendor's, is recorded in this module's header.\n */\nfunction singleParam(\n params: URLSearchParams,\n name: string,\n): { ok: true; value: string | null } | { ok: false; message: string } {\n const all = params.getAll(name);\n if (all.length > 1) {\n return {\n ok: false,\n message: `Invalid parameter: '${name}' was given ${all.length} times; it takes a single value`,\n };\n }\n return { ok: true, value: all.length === 0 ? null : all[0] };\n}\n\n/**\n * Cursor page over an array already ordered newest-first, mirroring the\n * `after`/`limit` params both fine-tuning list endpoints accept. `has_more`\n * reports whether anything was left behind, so it is never a constant.\n *\n * Sourced 2026-09-15 from openai/openai-openapi `openapi.yaml` v2.3.0\n * (`listPaginatedFineTuningJobs`, `listFineTuningEvents`) and the vendored\n * `openai` SDK 4.104.0 `pagination.d.ts`/`pagination.js` `CursorPage`:\n * - `after` is a free-form string id; `CursorPage.nextPageInfo()` feeds the\n * LAST id of the page back as `after`, and `hasNextPage()` stops on\n * `has_more: false`. Confirmed by the spec.\n * - `limit` is `type: integer, default: 20` on both endpoints. Confirmed by\n * the spec; the default is the vendor's.\n *\n * Three rules the spec does NOT state, chosen here and flagged as ours:\n * - Upper bound 100 on `limit`. The spec declares no `maximum`. We enforce\n * one so an oversized page fails loudly instead of being silently clamped,\n * and 100 is the number our own docs page already advertises.\n * - An `after` that matches no item is a 400 rather than a silent page 1.\n * Returning page 1 makes the SDK's `CursorPage` auto-paginator restart from\n * the top forever on a stale cursor; failing loudly is the whole point of a\n * mock. Not observed against api.openai.com.\n * - Newest-first ordering. Neither list endpoint accepts an `order` param and\n * the spec never states a direction; the `listFineTuningEvents` example\n * response lists the completion event ahead of the earlier\n * model-created event, which reads as reverse-chronological.\n */\nexport function paginate<T extends { id: string }>(\n items: T[],\n url: string | undefined,\n): PageResult<T> {\n const params = queryParams(url);\n const readLimit = singleParam(params, \"limit\");\n if (!readLimit.ok) return { ok: false, message: readLimit.message };\n const rawLimit = readLimit.value;\n let limit = DEFAULT_PAGE_LIMIT;\n if (rawLimit !== null) {\n const n = parseStrictIntegerText(rawLimit);\n if (n === null || n < 1 || n > MAX_PAGE_LIMIT) {\n // Quoted from the RAW query text, like the `after` message below, so\n // `?limit=%2B5` is refused as `'%2B5'` rather than as the `'+5'` the\n // caller never wrote (see `rawParam`).\n return {\n ok: false,\n message: `Invalid parameter: 'limit' must be an integer between 1 and ${MAX_PAGE_LIMIT}, got '${rawParam(url, \"limit\") ?? rawLimit}'`,\n };\n }\n limit = n;\n }\n const readAfter = singleParam(params, \"after\");\n if (!readAfter.ok) return { ok: false, message: readAfter.message };\n const after = readAfter.value;\n let start = 0;\n if (after !== null) {\n const idx = items.findIndex((it) => it.id === after);\n if (idx === -1) {\n // Quoted so the message still names the offending value when the caller\n // sent `?after=`, which `URLSearchParams` reads as the empty string, and\n // quoted from the RAW query text so the value in the message is the one\n // the caller wrote rather than its decoding (see `rawParam`).\n return {\n ok: false,\n message: `Invalid parameter: 'after' is not a known cursor: '${rawParam(url, \"after\") ?? after}'`,\n };\n }\n start = idx + 1;\n }\n const rest = items.slice(start);\n return {\n ok: true,\n page: { object: \"list\", data: rest.slice(0, limit), has_more: rest.length > limit },\n };\n}\n\n/** Write a paginated list, or the 400 its `after`/`limit` params earned. */\nfunction writePage<T extends { id: string }>(\n res: http.ServerResponse,\n journal: Journal,\n req: http.IncomingMessage,\n method: string,\n path: string,\n items: T[],\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n const result = paginate(items, req.url);\n const status = result.ok ? 200 : 400;\n journalFt(journal, method, path, flattenHeaders(req.headers), status);\n writeJson(res, status, result.ok ? result.page : invalid(result.message), setCorsHeaders);\n}\n\n/**\n * Deterministic seed in the spec's `[0, 2147483647]` range, derived from the\n * job id so repeated reads of the same job report the same seed.\n */\nfunction seedFor(id: string): number {\n let h = 2166136261;\n for (let i = 0; i < id.length; i++) {\n h = Math.imul(h ^ id.charCodeAt(i), 16777619);\n }\n return (h >>> 0) % 2147483648;\n}\n\n/**\n * One hyperparameter's declared domain, in the shape `readHyper` reads it.\n *\n * The tag is explicit rather than inferred from which optional fields a rule\n * happens to carry: `\"integer\"` vs `\"number\"` IS the spec's `type:`, and a\n * table whose meaning depends on the absence of a key silently changes\n * behaviour the moment a bound is added or dropped.\n *\n * - `integer` / `number`: the spec spells each of these as a `oneOf` of the\n * string `\"auto\"` and a bounded numeric, so `\"auto\"` is legal for every\n * rule of either kind. `max: null` means the spec declares no upper bound.\n * - `enum`: a plain `type: string, enum: [...]` with NO `\"auto\"` member\n * (`reasoning_effort` is the only one), so `\"auto\"` is a 400 there.\n */\ntype HyperRule =\n | { kind: \"integer\"; min: number; max: number | null }\n | { kind: \"number\"; exclusiveMin: number; max: number | null }\n | { kind: \"enum\"; values: readonly string[] };\n\ntype HyperTable = Readonly<Record<string, HyperRule>>;\n\n/**\n * The three members `CreateFineTuningJobRequest.hyperparameters` and\n * `FineTuneSupervisedHyperparameters` both declare, quoted from\n * openai/openai-openapi `openapi.yaml` v2.3.0 (`master`):\n * - `n_epochs`: `type: integer, minimum: 1, maximum: 50`\n * - `batch_size`: `type: integer, minimum: 1, maximum: 256`\n * - `learning_rate_multiplier`: `type: number, minimum: 0,\n * exclusiveMinimum: true` — strictly greater than zero, hence no `max`.\n *\n * The vendored SDK's `JobCreateParams.Hyperparameters` types each member as\n * `\"auto\" | number`, which is as much as TypeScript can say; the bounds only\n * exist in the spec, so they are enforced here from it.\n */\nconst SUPERVISED_HYPER_RULES = {\n n_epochs: { kind: \"integer\", min: 1, max: 50 },\n batch_size: { kind: \"integer\", min: 1, max: 256 },\n learning_rate_multiplier: { kind: \"number\", exclusiveMin: 0, max: null },\n} as const satisfies HyperTable;\n\n/**\n * The deprecated top-level `hyperparameters` object. The spec declares exactly\n * the three supervised members on it — `beta` and the reinforcement knobs live\n * only under `method`, and the description says this whole object \"is now\n * deprecated in favor of `method`\".\n */\nconst LEGACY_HYPER_RULES = SUPERVISED_HYPER_RULES;\n\n/**\n * `FineTuneDPOHyperparameters`: the supervised three plus `beta`, declared\n * `type: number, minimum: 0, exclusiveMinimum: true, maximum: 2`.\n */\nconst DPO_HYPER_RULES = {\n ...SUPERVISED_HYPER_RULES,\n beta: { kind: \"number\", exclusiveMin: 0, max: 2 },\n} as const satisfies HyperTable;\n\n/**\n * `FineTuneReinforcementHyperparameters`: the supervised three plus\n * `reasoning_effort` (`enum: [default, low, medium, high]`, no `\"auto\"`),\n * `compute_multiplier` (`minimum: 0.00001, exclusiveMinimum: true,\n * maximum: 10`), and `eval_interval`/`eval_samples` (`type: integer,\n * minimum: 1`, no declared maximum).\n */\nconst REINFORCEMENT_HYPER_RULES = {\n ...SUPERVISED_HYPER_RULES,\n reasoning_effort: { kind: \"enum\", values: [\"default\", \"low\", \"medium\", \"high\"] },\n compute_multiplier: { kind: \"number\", exclusiveMin: 0.00001, max: 10 },\n eval_interval: { kind: \"integer\", min: 1, max: null },\n eval_samples: { kind: \"integer\", min: 1, max: null },\n} as const satisfies HyperTable;\n\n/** How a rule reads in an error message, so the caller learns the domain. */\nfunction describeRule(rule: HyperRule): string {\n if (rule.kind === \"enum\") {\n return `one of ${rule.values.map((v) => `\"${v}\"`).join(\", \")}`;\n }\n const upper = rule.max === null ? \"\" : ` and at most ${rule.max}`;\n const lower =\n rule.kind === \"integer\"\n ? `an integer at least ${rule.min}`\n : `a number greater than ${rule.exclusiveMin}`;\n return `${lower}${upper}, or the string \"auto\"`;\n}\n\n/**\n * Reads one hyperparameter against its rule. Anything outside the rule is a\n * caller error, never a silent drop — a mock that accepted `n_epochs: 0` would\n * let a bug reach the real API instead of failing where it is cheap to see.\n *\n * `path` names the object the member came from (`hyperparameters`,\n * `method.dpo.hyperparameters`, …) so the message points at the thing the\n * caller actually wrote.\n */\nfunction readHyper(\n table: HyperTable,\n path: string,\n key: string,\n value: unknown,\n): { ok: true; value: number | string } | { ok: false; message: string } {\n // Own property only: `table[key]` walks `Object.prototype`, so `toString`\n // and friends would resolve to an inherited function instead of `undefined`.\n const rule = Object.hasOwn(table, key) ? table[key] : undefined;\n // `readHyperObject` rejects an unknown key first, so this only fires for a\n // direct caller; it is the same 400 either way.\n if (rule === undefined) return { ok: false, message: unknownMemberMessage(table, path, key) };\n const bad = {\n ok: false as const,\n message: `Invalid parameter: '${path}.${key}' must be ${describeRule(rule)}`,\n };\n if (rule.kind === \"enum\") {\n return typeof value === \"string\" && rule.values.includes(value) ? { ok: true, value } : bad;\n }\n if (value === \"auto\") return { ok: true, value: \"auto\" };\n if (typeof value !== \"number\" || !Number.isFinite(value)) return bad;\n if (rule.max !== null && value > rule.max) return bad;\n if (rule.kind === \"integer\") {\n return value >= rule.min && Number.isInteger(value) ? { ok: true, value } : bad;\n }\n return value > rule.exclusiveMin ? { ok: true, value } : bad;\n}\n\n/** The 400 an unrecognised member of a rule-table-backed object earns. */\nfunction unknownMemberMessage(table: HyperTable, path: string, key: string): string {\n return (\n `Invalid parameter: '${path}.${key}' is not a recognized member of '${path}'; ` +\n `it accepts ${Object.keys(table).join(\", \")}`\n );\n}\n\n/**\n * Reads a whole hyperparameters object against its rule table.\n *\n * An unrecognised member is a 400 rather than a silent drop. The spec does not\n * mark these objects `additionalProperties: false`, so this rule is ours, not\n * the vendor's — and it is the point of the surface: `{n_epochs: 3, beta: 0.5}`\n * used to answer 200 echoing only `n_epochs`, so a caller who put a real DPO\n * hyperparameter (`FineTuneDPOHyperparameters.beta`) in the deprecated\n * top-level object instead of under `method.dpo.hyperparameters` was told it\n * had been accepted. Naming the members the object does take is what turns\n * that into a fixable message.\n */\nfunction readHyperObject(\n table: HyperTable,\n path: string,\n raw: Record<string, unknown>,\n): { ok: true; value: Record<string, number | string> } | { ok: false; message: string } {\n const out: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (value === undefined) continue;\n // `key in table` would walk `Object.prototype`, letting `toString`,\n // `constructor`, `valueOf` and `__proto__` through this gate.\n if (!Object.hasOwn(table, key)) {\n return { ok: false, message: unknownMemberMessage(table, path, key) };\n }\n const read = readHyper(table, path, key, value);\n if (!read.ok) return read;\n out[key] = read.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Every property `CreateFineTuningJobRequest` declares (openai-openapi v2.3.0).\n *\n * Each one is read below; nothing on this list is accepted and dropped, and\n * anything NOT on it is a 400. The spec does not mark the request object\n * `additionalProperties: false`, so rejecting an unknown key is this mock's\n * rule, not the vendor's: a create body is written once and reused for months,\n * and a typo that answers 200 having ignored the field is exactly the bug a\n * mock exists to catch.\n */\nconst CREATE_KEYS = [\n \"model\",\n \"training_file\",\n \"hyperparameters\",\n \"suffix\",\n \"validation_file\",\n \"integrations\",\n \"seed\",\n \"method\",\n \"metadata\",\n] as const;\n\n/** The members of the deprecated top-level `hyperparameters`, as the job echoes them. */\nconst LEGACY_HYPER_KEYS = [\n \"n_epochs\",\n \"batch_size\",\n \"learning_rate_multiplier\",\n] as const satisfies readonly (keyof typeof LEGACY_HYPER_RULES)[];\n\n/** `Metadata` in the spec: at most 16 pairs, keys <= 64 chars, values <= 512. */\nconst METADATA_MAX_PAIRS = 16;\nconst METADATA_MAX_KEY = 64;\nconst METADATA_MAX_VALUE = 512;\n\n/** `CreateFineTuningJobRequest.suffix`: `minLength: 1, maxLength: 64`. */\nconst SUFFIX_MAX_LENGTH = 64;\n\n/** `CreateFineTuningJobRequest.seed`: `minimum: 0, maximum: 2147483647`. */\nconst SEED_MAX = 2147483647;\n\n/** `FineTuningJob.integrations`: `maxItems: 5`. */\nconst MAX_INTEGRATIONS = 5;\n\nexport type Read<T> = { ok: true; value: T } | { ok: false; message: string };\n\n/**\n * Length in Unicode CODE POINTS, which is the unit the spec's limits are in.\n *\n * Every `maxLength` quoted in this module is a pydantic `max_length` on the\n * vendor's side, and pydantic counts code points; `String.prototype.length`\n * counts UTF-16 code units, which is the same number only while the string\n * stays inside the BMP. One astral character - an emoji, a rarer CJK\n * ideograph, a musical symbol - counts two there, so a `suffix` of exactly 64\n * characters holding one used to earn a 400 the real API would not send, and a\n * caller that trusted the mock would have shortened a name that was legal.\n */\nfunction codePointLength(value: string): number {\n return [...value].length;\n}\n\n/**\n * `suffix`, per its spec description: \"A string of up to 64 characters that\n * will be added to your fine-tuned model name. For example, a `suffix` of\n * \"custom-model-name\" would produce a model name like\n * `ft:gpt-4o-mini:openai:custom-model-name:7p4lURel`.\" It is declared\n * `type: string, minLength: 1, maxLength: 64, nullable: true, default: null`,\n * so `null` means \"no suffix\" and `\"\"` is a 400.\n */\nfunction readSuffix(value: unknown): Read<string | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (\n typeof value !== \"string\" ||\n value.length === 0 ||\n codePointLength(value) > SUFFIX_MAX_LENGTH\n ) {\n return {\n ok: false,\n message: `Invalid parameter: 'suffix' must be a string of 1 to ${SUFFIX_MAX_LENGTH} characters or null`,\n };\n }\n return { ok: true, value };\n}\n\n/**\n * `seed`, per its spec description: \"The seed controls the reproducibility of\n * the job ... If a seed is not specified, one will be generated for you\", and\n * declared `type: integer, nullable: true, minimum: 0, maximum: 2147483647`.\n * `null` and absence both mean \"generate one\", which is what `seedFor` does.\n */\nfunction readSeed(value: unknown): Read<number | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (typeof value !== \"number\" || !Number.isInteger(value) || value < 0 || value > SEED_MAX) {\n return {\n ok: false,\n message: `Invalid parameter: 'seed' must be an integer between 0 and ${SEED_MAX} or null`,\n };\n }\n return { ok: true, value };\n}\n\n/**\n * `metadata`, per the shared `Metadata` schema: an object of string values, at\n * most 16 pairs, keys at most 64 characters and values at most 512, or `null`.\n * `FineTuningJob` declares `metadata` too, so what is accepted is echoed back.\n */\nexport function readMetadata(value: unknown): Read<Record<string, string> | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!isJsonObject(value)) {\n return { ok: false, message: \"Invalid parameter: 'metadata' must be a JSON object or null\" };\n }\n const entries = Object.entries(value);\n if (entries.length > METADATA_MAX_PAIRS) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata' accepts at most ${METADATA_MAX_PAIRS} key-value pairs, got ${entries.length}`,\n };\n }\n // `metadata` keys are the caller's own namespace, so `__proto__` is an\n // ordinary key here and is echoed back like any other. Writing it onto an\n // object literal would instead hit `Object.prototype`'s `__proto__` setter,\n // which ignores a string value silently - a 200 that dropped the pair, and a\n // pair count that no longer matches what reads back.\n const out: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const [key, entry] of entries) {\n const keyLength = codePointLength(key);\n if (keyLength > METADATA_MAX_KEY) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata' keys are at most ${METADATA_MAX_KEY} characters, got ${keyLength}`,\n };\n }\n if (typeof entry !== \"string\" || codePointLength(entry) > METADATA_MAX_VALUE) {\n return {\n ok: false,\n message: `Invalid parameter: 'metadata.${key}' must be a string of at most ${METADATA_MAX_VALUE} characters`,\n };\n }\n out[key] = entry;\n }\n return { ok: true, value: out };\n}\n\n/**\n * One `integrations` entry. The spec requires both `type` and `wandb`, types\n * `type` as the single-member enum `wandb`, and requires `project` inside\n * `wandb`; `name` and `entity` are nullable strings and `tags` an array of\n * strings. `FineTuningJob.integrations` caps the list at `maxItems: 5` — the\n * request schema declares no cap, so enforcing the response's here is ours.\n */\nfunction readIntegration(index: number, raw: unknown): Read<FineTuningJobIntegration> {\n const at = `integrations[${index}]`;\n if (!isJsonObject(raw)) {\n return { ok: false, message: `Invalid parameter: '${at}' must be a JSON object` };\n }\n for (const key of Object.keys(raw)) {\n if (key !== \"type\" && key !== \"wandb\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.${key}' is not a recognized member of '${at}'; it accepts type, wandb`,\n };\n }\n }\n if (raw[\"type\"] !== \"wandb\") {\n return { ok: false, message: `Invalid parameter: '${at}.type' must be the string \"wandb\"` };\n }\n const wandb = raw[\"wandb\"];\n if (!isJsonObject(wandb)) {\n return { ok: false, message: `Invalid parameter: '${at}.wandb' must be a JSON object` };\n }\n for (const key of Object.keys(wandb)) {\n if (key !== \"project\" && key !== \"name\" && key !== \"entity\" && key !== \"tags\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.${key}' is not a recognized member of '${at}.wandb'; it accepts project, name, entity, tags`,\n };\n }\n }\n const project = wandb[\"project\"];\n if (typeof project !== \"string\" || project.length === 0) {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.project' must be a non-empty string`,\n };\n }\n const value: FineTuningJobIntegration = { type: \"wandb\", wandb: { project } };\n for (const key of [\"name\", \"entity\"] as const) {\n const member = wandb[key];\n if (member === undefined) continue;\n if (member !== null && typeof member !== \"string\") {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.${key}' must be a string or null`,\n };\n }\n value.wandb[key] = member;\n }\n const tags = wandb[\"tags\"];\n if (tags !== undefined) {\n if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== \"string\")) {\n return {\n ok: false,\n message: `Invalid parameter: '${at}.wandb.tags' must be an array of strings`,\n };\n }\n value.wandb.tags = tags.filter((tag): tag is string => typeof tag === \"string\");\n }\n return { ok: true, value };\n}\n\n/** `integrations`: a nullable array of the entries above. */\nfunction readIntegrations(value: unknown): Read<FineTuningJobIntegration[] | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!Array.isArray(value)) {\n return { ok: false, message: \"Invalid parameter: 'integrations' must be an array or null\" };\n }\n if (value.length > MAX_INTEGRATIONS) {\n return {\n ok: false,\n message: `Invalid parameter: 'integrations' accepts at most ${MAX_INTEGRATIONS} entries, got ${value.length}`,\n };\n }\n const out: FineTuningJobIntegration[] = [];\n for (const [index, entry] of value.entries()) {\n const read = readIntegration(index, entry);\n if (!read.ok) return read;\n out.push(read.value);\n }\n return { ok: true, value: out };\n}\n\n/** The rule table each `method` variant's `hyperparameters` is read against. */\nconst METHOD_HYPER_RULES = {\n supervised: SUPERVISED_HYPER_RULES,\n dpo: DPO_HYPER_RULES,\n reinforcement: REINFORCEMENT_HYPER_RULES,\n} as const;\n\nconst METHOD_TYPES = [\"supervised\", \"dpo\", \"reinforcement\"] as const;\n\n/**\n * `method`, per `FineTuneMethod`: `type` is required and is one of\n * `supervised`, `dpo`, `reinforcement`, and each of those names an optional\n * sub-object carrying that method's `hyperparameters`. `FineTuneReinforcementMethod`\n * additionally requires a `grader`, so `type: \"reinforcement\"` requires one\n * whether or not the `reinforcement` sub-object is present at all — the\n * requirement is keyed on the chosen `type`, not on the sub-object happening\n * to be there.\n *\n * Only the chosen type's sub-object is accepted. `FineTuneMethod` leaves all\n * three optional and says nothing about a configuration for a type that was\n * not chosen, and neither does SDK 4.104.0's `JobCreateParams.Method`; every\n * example in the spec pairs a `type` with that type's own configuration and\n * no other (`createFineTuningJob`, the \"Reinforcement\" example). With the real\n * API's behaviour uncitable, a stray configuration is rejected rather than\n * accepted-and-ignored: this mock never takes a member it then drops.\n *\n * The grader itself (`GraderStringCheck` | `GraderTextSimilarity` |\n * `GraderPython` | `GraderScoreModel` | `GraderMulti`) is a schema tree of its\n * own that this mock does not model: it is required to be a JSON object and is\n * then echoed back verbatim on the job. That is a deliberate pass-through, not\n * a drop — what the caller sent is what the caller reads back.\n *\n * `FineTuningJob` declares `method`, so an accepted method is echoed.\n */\nfunction readMethod(value: unknown): Read<FineTuningJobMethod | null> {\n if (value === undefined || value === null) return { ok: true, value: null };\n if (!isJsonObject(value)) {\n return { ok: false, message: \"Invalid parameter: 'method' must be a JSON object or null\" };\n }\n for (const key of Object.keys(value)) {\n if (key !== \"type\" && !METHOD_TYPES.some((t) => t === key)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${key}' is not a recognized member of 'method'; it accepts type, ${METHOD_TYPES.join(\", \")}`,\n };\n }\n }\n const type = METHOD_TYPES.find((t) => t === value[\"type\"]);\n if (type === undefined) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.type' must be one of ${METHOD_TYPES.map((t) => `\"${t}\"`).join(\", \")}`,\n };\n }\n const method: FineTuningJobMethod = { type };\n for (const variant of METHOD_TYPES) {\n if (variant !== type && value[variant] !== undefined) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${variant}' configures the \"${variant}\" method, but 'method.type' is \"${type}\"; a method carries only its own configuration`,\n };\n }\n }\n const supplied = value[type];\n // A reinforcement method requires a `grader`; an absent `reinforcement`\n // object is therefore an empty one, read here so the `grader` check below\n // rejects it instead of the method slipping through unconfigured.\n const raw = supplied === undefined && type === \"reinforcement\" ? {} : supplied;\n if (raw !== undefined) {\n if (!isJsonObject(raw)) {\n return { ok: false, message: `Invalid parameter: 'method.${type}' must be a JSON object` };\n }\n const allowed = type === \"reinforcement\" ? [\"grader\", \"hyperparameters\"] : [\"hyperparameters\"];\n for (const key of Object.keys(raw)) {\n if (!allowed.includes(key)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${type}.${key}' is not a recognized member of 'method.${type}'; it accepts ${allowed.join(\", \")}`,\n };\n }\n }\n const config: FineTuningJobMethodConfig = {};\n if (type === \"reinforcement\") {\n const grader = raw[\"grader\"];\n if (!isJsonObject(grader)) {\n return {\n ok: false,\n message:\n \"Invalid parameter: 'method.reinforcement.grader' is required and must be a JSON object\",\n };\n }\n config.grader = grader;\n }\n const hyper = raw[\"hyperparameters\"];\n if (hyper !== undefined) {\n if (!isJsonObject(hyper)) {\n return {\n ok: false,\n message: `Invalid parameter: 'method.${type}.hyperparameters' must be a JSON object`,\n };\n }\n const read = readHyperObject(\n METHOD_HYPER_RULES[type],\n `method.${type}.hyperparameters`,\n hyper,\n );\n if (!read.ok) return read;\n config.hyperparameters = read.value;\n }\n method[type] = config;\n }\n return { ok: true, value: method };\n}\n\nexport async function handleFineTuningCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/fine_tuning/jobs\";\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n let body: unknown;\n try {\n body = JSON.parse(raw) as unknown;\n } catch (err) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(\n res,\n 400,\n invalid(`Malformed JSON: ${err instanceof Error ? err.message : \"unknown\"}`),\n setCorsHeaders,\n );\n return;\n }\n if (!isJsonObject(body)) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(\"Request body must be a JSON object\"), setCorsHeaders);\n return;\n }\n // Recorded verbatim rather than reshaped into a synthetic chat request that\n // would misreport what the caller actually sent. `JournalBody` covers the\n // non-chat member, so this needs no cast — and must not have one: the body\n // is still unvalidated here (the checks below are what reject it), so any\n // claim about its shape at this point would be a claim about arbitrary JSON.\n const journaledBody: JournalBody = body;\n /**\n * Every rejection below journals the parsed body before answering: these are\n * exactly the 400s whose offending payload a caller needs to read back.\n */\n const reject = (message: string): void => {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400, journaledBody);\n writeJson(res, 400, invalid(message), setCorsHeaders);\n };\n for (const key of Object.keys(body)) {\n if (!CREATE_KEYS.some((known) => known === key)) {\n reject(\n `Invalid parameter: '${key}' is not a recognized request parameter; this endpoint accepts ${CREATE_KEYS.join(\", \")}`,\n );\n return;\n }\n }\n const trainingFile = body[\"training_file\"];\n const model = body[\"model\"];\n if (typeof trainingFile !== \"string\" || trainingFile.length === 0) {\n reject(\"Invalid parameter: 'training_file' must be a non-empty string\");\n return;\n }\n if (typeof model !== \"string\" || model.length === 0) {\n reject(\"Invalid parameter: 'model' must be a non-empty string\");\n return;\n }\n const validationFile = body[\"validation_file\"];\n // `null` is legal - the spec marks `validation_file` `nullable: true` - but\n // `\"\"` is not a file id. The spec sets no `minLength` on either file id; the\n // empty-string rejection is this mock's, and it matches `training_file`\n // above rather than disagreeing with it.\n if (\n validationFile !== undefined &&\n validationFile !== null &&\n (typeof validationFile !== \"string\" || validationFile.length === 0)\n ) {\n reject(\"Invalid parameter: 'validation_file' must be a non-empty string or null\");\n return;\n }\n const hyper = body[\"hyperparameters\"];\n // `null` is rejected with everything else that is not an object: the spec\n // types `hyperparameters` as `type: object` and, unlike its siblings\n // `suffix`, `validation_file` and `integrations`, never marks it\n // `nullable: true`.\n if (hyper !== undefined && !isJsonObject(hyper)) {\n reject(\"Invalid parameter: 'hyperparameters' must be a JSON object\");\n return;\n }\n const hyperparameters: FineTuningJobHyperparameters = {};\n if (isJsonObject(hyper)) {\n const read = readHyperObject(LEGACY_HYPER_RULES, \"hyperparameters\", hyper);\n if (!read.ok) {\n reject(read.message);\n return;\n }\n for (const key of LEGACY_HYPER_KEYS) {\n const value = read.value[key];\n // Every rule in this table is an `integer`/`number` kind, so a value\n // that survived it is a number or the string \"auto\". The guard proves\n // that to the type; it is not a filter, and cannot drop anything.\n if (typeof value === \"number\" || value === \"auto\") hyperparameters[key] = value;\n }\n }\n const suffix = readSuffix(body[\"suffix\"]);\n if (!suffix.ok) {\n reject(suffix.message);\n return;\n }\n const seed = readSeed(body[\"seed\"]);\n if (!seed.ok) {\n reject(seed.message);\n return;\n }\n const integrations = readIntegrations(body[\"integrations\"]);\n if (!integrations.ok) {\n reject(integrations.message);\n return;\n }\n const jobMethod = readMethod(body[\"method\"]);\n if (!jobMethod.ok) {\n reject(jobMethod.message);\n return;\n }\n const metadata = readMetadata(body[\"metadata\"]);\n if (!metadata.ok) {\n reject(metadata.message);\n return;\n }\n const id = generateId(\"ftjob\");\n const job: FineTuningJob = {\n id,\n object: \"fine_tuning.job\",\n model,\n training_file: trainingFile,\n validation_file: typeof validationFile === \"string\" ? validationFile : null,\n status: \"validating_files\",\n created_at: stamp(),\n finished_at: null,\n fine_tuned_model: null,\n hyperparameters,\n error: null,\n organization_id: ORGANIZATION_ID,\n result_files: [],\n seed: seed.value ?? seedFor(id),\n trained_tokens: null,\n };\n // The three optional job properties are set only when the caller sent them:\n // the spec lists none of them in `FineTuningJob.required`, so emitting\n // `\"method\": null` on every job would be a field the vendor never sends.\n if (integrations.value !== null) job.integrations = integrations.value;\n if (jobMethod.value !== null) job.method = jobMethod.value;\n if (metadata.value !== null) job.metadata = metadata.value;\n // `suffix` is NOT a property of `fine_tuning.job`, so it is kept beside the\n // job rather than on it, the way the event log is; it is observable only\n // through the `fine_tuned_model` name the job takes when it succeeds.\n if (suffix.value !== null) suffixes.set(job.id, suffix.value);\n jobs.set(job.id, job);\n polls.set(job.id, 0);\n appendEvent(job, \"Job created, validating training file\");\n journalFt(journal, method, path, flattenHeaders(req.headers), 200, journaledBody);\n writeJson(res, 200, job, setCorsHeaders);\n}\n\nexport async function handleFineTuningList(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? \"/v1/fine_tuning/jobs\";\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n // Newest-first. `stamp()` is monotonic module-wide, so insertion order is\n // unconditionally non-decreasing in `created_at` - a job created later never\n // carries an earlier second, whichever way the wall clock moved - and\n // reversing it is therefore both newest-first and the stable total order a\n // cursor walk needs. Sorting on `created_at` would not be: it has one-second\n // resolution, so ties are the normal case and a sort cannot order them.\n const all = [...jobs.values()].reverse();\n writePage(res, journal, req, method, path, all, setCorsHeaders);\n}\n\nexport async function handleFineTuningRetrieve(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}`;\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n journalFt(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, advance(job), setCorsHeaders);\n}\n\nexport async function handleFineTuningCancel(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}/cancel`;\n const method = req.method ?? \"POST\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n if (isTerminal(job.status)) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 400);\n writeJson(res, 400, invalid(`Job ${id} is already terminal (${job.status})`), setCorsHeaders);\n return;\n }\n job.status = \"cancelled\";\n // A cancelled job is finished, and the wire type declares `finished_at` null\n // only \"if the fine-tuning job is still running\" - so stamp it the same way\n // `advance()` stamps it on `succeeded`: from the cancellation event itself,\n // which keeps the finish at or after every timestamp that preceded it.\n job.finished_at = appendEvent(job, \"Job cancelled by user\");\n journalFt(journal, method, path, flattenHeaders(req.headers), 200);\n writeJson(res, 200, job, setCorsHeaders);\n}\n\nexport async function handleFineTuningEvents(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string,\n journal: Journal,\n defaults: Defaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const path = req.url ?? `/v1/fine_tuning/jobs/${id}/events`;\n const method = req.method ?? \"GET\";\n if (await chaosHit(req, journal, defaults, method, path, res, setCorsHeaders)) return;\n const job = jobs.get(id);\n if (!job) {\n journalFt(journal, method, path, flattenHeaders(req.headers), 404);\n writeJson(res, 404, invalid(`No such fine-tuning job: ${id}`), setCorsHeaders);\n return;\n }\n writePage(res, journal, req, method, path, eventsFor(job).reverse(), setCorsHeaders);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fine-tuning.d.cts","names":[],"sources":["../src/fine-tuning.ts"],"sourcesContent":[],"mappings":";;;;;;;;UA4GiB,kBAAA;;;;;;;;KASL,mBAAA;UAmBK,kBAAA;;;;;;UAOA,4BAAA;;;;;;;;;UAUA,wBAAA;;;;;;;;;;;;;;UAUA,yBAAA;oBACG;WACT;;;UAIM,mBAAA;;eAEF;QACP;kBACU;;;;;;;UAQD,aAAA;;;;;;UAMP;;;;mBAIS;SACV;;;;;iBAKQ;WACN;aACE;;iBAwDG,oBAAA,CAAA;KAQX,QAAA;UAAqB;UAAgB;aAA0B;;iBA44B9C,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,sCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAuJmB,oBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,yBACD,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,wBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAyBmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B"}
1
+ {"version":3,"file":"fine-tuning.d.cts","names":[],"sources":["../src/fine-tuning.ts"],"sourcesContent":[],"mappings":";;;;;;;;UA4GiB,kBAAA;;;;;;;;KASL,mBAAA;UAmBK,kBAAA;;;;;;UAOA,4BAAA;;;;;;;;;UAUA,wBAAA;;;;;;;;;;;;;;UAUA,yBAAA;oBACG;WACT;;;UAIM,mBAAA;;eAEF;QACP;kBACU;;;;;;;UAQD,aAAA;;;;;;UAMP;;;;mBAIS;SACV;;;;;iBAKQ;WACN;aACE;;iBAwDG,oBAAA,CAAA;KAQX,QAAA;UAAqB;UAAgB;aAA0B;;iBAy3B9C,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,sCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAuJmB,oBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,yBACD,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,wBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAyBmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"fine-tuning.d.ts","names":[],"sources":["../src/fine-tuning.ts"],"sourcesContent":[],"mappings":";;;;;;;;UA4GiB,kBAAA;;;;;;;;KASL,mBAAA;UAmBK,kBAAA;;;;;;UAOA,4BAAA;;;;;;;;;UAUA,wBAAA;;;;;;;;;;;;;;UAUA,yBAAA;oBACG;WACT;;;UAIM,mBAAA;;eAEF;QACP;kBACU;;;;;;;UAQD,aAAA;;;;;;UAMP;;;;mBAIS;SACV;;;;;iBAKQ;WACN;aACE;;iBAwDG,oBAAA,CAAA;KAQX,QAAA;UAAqB;UAAgB;aAA0B;;iBA44B9C,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,sCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAuJmB,oBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,yBACD,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,wBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAyBmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B"}
1
+ {"version":3,"file":"fine-tuning.d.ts","names":[],"sources":["../src/fine-tuning.ts"],"sourcesContent":[],"mappings":";;;;;;;;UA4GiB,kBAAA;;;;;;;;KASL,mBAAA;UAmBK,kBAAA;;;;;;UAOA,4BAAA;;;;;;;;;UAUA,wBAAA;;;;;;;;;;;;;;UAUA,yBAAA;oBACG;WACT;;;UAIM,mBAAA;;eAEF;QACP;kBACU;;;;;;;UAQD,aAAA;;;;;;UAMP;;;;mBAIS;SACV;;;;;iBAKQ;WACN;aACE;;iBAwDG,oBAAA,CAAA;KAQX,QAAA;UAAqB;UAAgB;aAA0B;;iBAy3B9C,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,sCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAuJmB,oBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,yBACD,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,wBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAcmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B;iBAyBmB,sBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,qCAED,mBACC,gCACY,MAAA,CAAK,0BAC1B"}
@@ -1,4 +1,4 @@
1
- import { flattenHeaders, generateId, isJsonObject } from "./helpers.js";
1
+ import { flattenHeaders, generateId, isJsonObject, parseStrictIntegerText } from "./helpers.js";
2
2
  import { applyChaosAsync } from "./chaos.js";
3
3
 
4
4
  //#region src/fine-tuning.ts
@@ -297,24 +297,6 @@ function singleParam(params, name) {
297
297
  };
298
298
  }
299
299
  /**
300
- * A plain decimal integer, or null. Deliberately stricter than `Number()`,
301
- * which also accepts hex (`0x10` → 16), exponent notation (`1e2` → 100), a
302
- * leading sign (`+5`), a trailing `.0` and surrounding whitespace — none of
303
- * which is the "integer" the spec's `type: integer` or our own error message
304
- * describes, and all of which used to be honoured silently at some other
305
- * number than the one the caller wrote.
306
- *
307
- * Kept local rather than shared with the chaos config's own number parsing:
308
- * that one must accept fractional rates and so goes through `Number()`, which
309
- * is precisely the behaviour this function exists to refuse. Merging the two
310
- * would mean widening the lenient one, not renaming this one.
311
- */
312
- function parseDecimalInteger(raw) {
313
- if (!/^\d+$/.test(raw)) return null;
314
- const n = Number(raw);
315
- return Number.isSafeInteger(n) ? n : null;
316
- }
317
- /**
318
300
  * Cursor page over an array already ordered newest-first, mirroring the
319
301
  * `after`/`limit` params both fine-tuning list endpoints accept. `has_more`
320
302
  * reports whether anything was left behind, so it is never a constant.
@@ -351,7 +333,7 @@ function paginate(items, url) {
351
333
  const rawLimit = readLimit.value;
352
334
  let limit = DEFAULT_PAGE_LIMIT;
353
335
  if (rawLimit !== null) {
354
- const n = parseDecimalInteger(rawLimit);
336
+ const n = parseStrictIntegerText(rawLimit);
355
337
  if (n === null || n < 1 || n > MAX_PAGE_LIMIT) return {
356
338
  ok: false,
357
339
  message: `Invalid parameter: 'limit' must be an integer between 1 and ${MAX_PAGE_LIMIT}, got '${rawParam(url, "limit") ?? rawLimit}'`