@wenathlan/devthink 2.0.12 → 2.0.14

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 (444) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/engine.ts ADDED
@@ -0,0 +1,1524 @@
1
+ /**
2
+ * engine — universal gateway engine
3
+ * one file one responsibility — the generic request pipeline for all versions
4
+ * 0 to 100 correlated logics grouped in this one file
5
+ *
6
+ * consumes a gatewayconfig produces all 7 route handlers
7
+ * supports any llm any baseurl any api key via configuration
8
+ * v1 zai sdk v2 babel v3 nvidia v4 opencode kilo v5 openrouter are all
9
+ * just config objects feeding this single engine
10
+ *
11
+ * pipeline per request:
12
+ * 1 paused check → 503 fallback
13
+ * 2 model resolution — requested or default or meta rotation pick
14
+ * 3 auth resolution — 12 methods db env inline ephemeral
15
+ * 4 body building — sampling params per policy thinking param per config
16
+ * 5 execution — retry backoff rotation timeout circuit breaker
17
+ * 6 streaming — byte passthrough sse parsing heartbeat discard masking
18
+ * 7 persistence — savemsg with version specific fields
19
+ * 8 response building — chat completion anthropic message response formats
20
+ */
21
+
22
+ import { buildauthheaders, extractrequestid, extractsessionid, resolvekeys } from "./server.js";
23
+ import { getsessionmessages, savemsg } from "./database";
24
+ import { makechunk, makefinalchunk, makestreamresponse } from "./server.js";
25
+ import type {
26
+ gatewayconfig,
27
+ modeldef,
28
+ resolvedkey,
29
+ sessionstate,
30
+ thinkinglevel,
31
+ upstreamdef,
32
+ versionhandlers,
33
+ } from "./types";
34
+ import {
35
+ autofrequencypenalty,
36
+ auton,
37
+ autopresencepenalty,
38
+ autoseed,
39
+ autotemp,
40
+ autothinking,
41
+ autotopp,
42
+ clamp,
43
+ corsheaders,
44
+ defaultmaxtokens,
45
+ esttokens,
46
+ genid,
47
+ handlecors,
48
+ jsonheaders,
49
+ randombase36,
50
+ safejsonparse,
51
+ safestringify,
52
+ securerandom,
53
+ truncatemessages,
54
+ } from "./utils";
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // defaults — universal fallbacks when config omits values
58
+ // ---------------------------------------------------------------------------
59
+
60
+ /** default thinking budgets — 7 level canonical map */
61
+ const defaultbudgets: Record<thinkinglevel, number> = {
62
+ none: 0,
63
+ minimal: 1400,
64
+ low: 5500,
65
+ medium: 17000,
66
+ high: 68000,
67
+ xhigh: 68000,
68
+ max: 68000,
69
+ };
70
+
71
+ /** default retryable statuses — 429 rate limit plus transient 5xx */
72
+ const defaultretrystatuses = [429, 502, 503, 504];
73
+
74
+ /** default rotate statuses — nvidia style gateway errors */
75
+ const defaultrotatestatuses = [529, 404, 410];
76
+
77
+ /** default fallback statuses — v4 cross-provider swap set */
78
+ const defaultfallbackstatuses = [404, 429, 500, 502, 503, 529];
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // session state — in-memory rotation tracking per version
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /** in-memory session store — one map per version instance */
85
+ function createsessionstore() {
86
+ const sessions = new Map<string, sessionstate>();
87
+ return {
88
+ get(sessionid: string): sessionstate {
89
+ let s = sessions.get(sessionid);
90
+ if (!s) {
91
+ s = { messagecount: 0, rotationindex: 0, model: "" };
92
+ sessions.set(sessionid, s);
93
+ }
94
+ return s;
95
+ },
96
+ bump(sessionid: string): sessionstate {
97
+ const s = this.get(sessionid);
98
+ s.messagecount += 1;
99
+ return s;
100
+ },
101
+ };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // zai sdk transport — singleton for keyless sdk auth
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /** zai sdk lazy singleton — only imported when transport demands it */
109
+ let zaipromise: Promise<unknown> | null = null;
110
+ async function getzai() {
111
+ if (!zaipromise) {
112
+ const mod = await import("z-ai-web-dev-sdk");
113
+ const ZAI =
114
+ (mod as { default?: { create: () => Promise<unknown> } }).default ??
115
+ (mod as unknown as { create: () => Promise<unknown> });
116
+ zaipromise = ZAI.create();
117
+ }
118
+ return zaipromise;
119
+ }
120
+
121
+ /** call the zai sdk chat completions with messages and options */
122
+ async function zaisdkcall(
123
+ messages: unknown[],
124
+ opts: { thinking?: unknown; max_tokens?: number; temperature?: number; stream?: boolean },
125
+ ): Promise<{ content: string; reasoning?: string | undefined }> {
126
+ const zai = (await getzai()) as {
127
+ chat: {
128
+ completions: {
129
+ create: (p: Record<string, unknown>) => Promise<{
130
+ choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>;
131
+ }>;
132
+ };
133
+ };
134
+ };
135
+ const res = await zai.chat.completions.create({
136
+ messages: messages as never,
137
+ ...(opts.thinking !== undefined ? { thinking: opts.thinking } : {}),
138
+ ...(opts.max_tokens !== undefined ? { max_tokens: opts.max_tokens } : {}),
139
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
140
+ stream: opts.stream ?? false,
141
+ });
142
+ const choice = res.choices?.[0]?.message;
143
+ return {
144
+ content: choice?.content ?? "",
145
+ reasoning: choice?.reasoning_content,
146
+ };
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // model resolution
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /** find a model by id in the catalog — returns undefined when unknown */
154
+ function findmodel(cfg: gatewayconfig, id: string): modeldef | undefined {
155
+ return cfg.models.find((m) => m.id === id);
156
+ }
157
+
158
+ /** resolve the model for a request — meta rotation individual or default */
159
+ function resolvemodel(
160
+ cfg: gatewayconfig,
161
+ requested: string | undefined,
162
+ session: sessionstate,
163
+ globalcounter: { n: number },
164
+ ): {
165
+ model: modeldef | undefined;
166
+ modelid: string;
167
+ display: string;
168
+ ismeta: boolean;
169
+ rotationindex: number;
170
+ } {
171
+ const metaid = cfg.metamodel.id;
172
+ const rotationmodels = cfg.rotation?.models ?? [];
173
+ const ismetarequest = requested === metaid || requested === undefined || requested === "";
174
+
175
+ // meta model with rotation — pick from the rotation pool
176
+ if (ismetarequest && rotationmodels.length > 0 && cfg.rotation?.mode !== "none") {
177
+ const everyn = cfg.rotation?.everynmessages ?? 6;
178
+ let idx: number;
179
+ if (cfg.rotation?.mode === "perrequest") {
180
+ idx = globalcounter.n % rotationmodels.length;
181
+ globalcounter.n += 1;
182
+ } else {
183
+ // persession — rotate every n messages
184
+ idx = Math.floor(session.messagecount / everyn) % rotationmodels.length;
185
+ if (cfg.rotation?.staggernewsessions && session.messagecount === 0) {
186
+ idx = (idx + globalcounter.n) % rotationmodels.length;
187
+ globalcounter.n += 1;
188
+ }
189
+ }
190
+ const modelid = rotationmodels[idx] ?? rotationmodels[0];
191
+ const model = findmodel(cfg, modelid);
192
+ session.model = modelid;
193
+ session.rotationindex = idx;
194
+ return {
195
+ model,
196
+ modelid,
197
+ display: cfg.metamodel.alwaysdisplay ? metaid : modelid,
198
+ ismeta: true,
199
+ rotationindex: idx,
200
+ };
201
+ }
202
+
203
+ // individual model request
204
+ if (requested && !ismetarequest) {
205
+ const model = findmodel(cfg, requested);
206
+ if (model) {
207
+ return {
208
+ model,
209
+ modelid: requested,
210
+ display: cfg.metamodel.alwaysdisplay ? metaid : requested,
211
+ ismeta: false,
212
+ rotationindex: -1,
213
+ };
214
+ }
215
+ }
216
+
217
+ // default model fallback
218
+ const defaultid = cfg.defaultmodel ?? rotationmodels[0] ?? cfg.models[0]?.id ?? metaid;
219
+ const model = findmodel(cfg, defaultid);
220
+ return {
221
+ model,
222
+ modelid: defaultid,
223
+ display: cfg.metamodel.alwaysdisplay ? metaid : defaultid,
224
+ ismeta: defaultid === metaid,
225
+ rotationindex: -1,
226
+ };
227
+ }
228
+
229
+ /** compute the fusion context — sum of model contexts or explicit override */
230
+ function fusioncontext(cfg: gatewayconfig, modelsonly?: modeldef[]): number {
231
+ if (cfg.metamodel.contextoverride !== undefined) return cfg.metamodel.contextoverride;
232
+ const pool = modelsonly ?? cfg.models;
233
+ return pool.reduce((sum, m) => sum + m.context, 0);
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // auth resolution — delegates to authentication.ts with config
238
+ // ---------------------------------------------------------------------------
239
+
240
+ /** resolve a key for the request — returns null when keyless */
241
+ async function resolveauth(cfg: gatewayconfig, _req?: Request): Promise<resolvedkey | null> {
242
+ const auth = cfg.auth;
243
+ if (auth.mode === "anonymous" || auth.mode === "none" || auth.mode === "keylesssdk") {
244
+ return null;
245
+ }
246
+ const keys = await resolvekeys(auth);
247
+ if (keys.length === 0) {
248
+ if (auth.required) {
249
+ return null;
250
+ }
251
+ return null;
252
+ }
253
+ // round robin across keys
254
+ const idx = resolveauth.cursor % keys.length;
255
+ resolveauth.cursor = (resolveauth.cursor + 1) % Math.max(keys.length, 1);
256
+ return keys[idx];
257
+ }
258
+ resolveauth.cursor = 0;
259
+
260
+ /** build upstream headers from auth config plus resolved key */
261
+ function buildheaders(cfg: gatewayconfig, key: resolvedkey | null, stream: boolean): Record<string, string> {
262
+ const auth = cfg.auth;
263
+ const headers: Record<string, string> = {
264
+ "content-type": "application/json",
265
+ ...(cfg.headers?.useragent ? { "user-agent": cfg.headers.useragent } : {}),
266
+ ...(auth.extraheaders ?? {}),
267
+ };
268
+ if (stream) headers["accept"] = "text/event-stream";
269
+ if (key) {
270
+ Object.assign(headers, buildauthheaders(auth, key.key));
271
+ }
272
+ return headers;
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // body building
277
+ // ---------------------------------------------------------------------------
278
+
279
+ /** build the upstream request body per config policy
280
+ * model is optional: callers without a resolved model fall back to
281
+ * cfg.defaultmodel then the meta id for the model id mapping */
282
+ function buildbody(
283
+ cfg: gatewayconfig,
284
+ body: Record<string, unknown>,
285
+ model?: modeldef,
286
+ overridemodel?: string,
287
+ ): Record<string, unknown> {
288
+ const out: Record<string, unknown> = { ...body };
289
+ const modelid = overridemodel ?? model?.id ?? cfg.defaultmodel ?? cfg.metamodel.id;
290
+
291
+ // model id — map to upstream id when configured
292
+ const upstreamid = model?.upstream ?? modelid;
293
+ out["model"] = upstreamid;
294
+
295
+ // remove gateway-only fields
296
+ delete out["apiKey"];
297
+ delete out["api_key"];
298
+ delete out["token"];
299
+
300
+ // thinking param — chattemplatekwargs or thinking object
301
+ const thinkinglevel = autothinking(body["thinking"] ?? body["reasoning_effort"]);
302
+ const budgets = cfg.thinking?.budgets ?? defaultbudgets;
303
+ const budget = budgets[thinkinglevel] ?? 0;
304
+
305
+ if (cfg.thinking?.param === "chattemplatekwargs" || model?.chattemplatekwargs) {
306
+ // nvidia style — merge model kwargs
307
+ const kwargs = model?.chattemplatekwargs ?? { enable_thinking: thinkinglevel !== "none" };
308
+ out["chat_template_kwargs"] = { ...kwargs };
309
+ delete out["thinking"];
310
+ delete out["reasoning_effort"];
311
+ } else {
312
+ // zai openai style — thinking object
313
+ if (body["thinking"] !== undefined || thinkinglevel !== "none") {
314
+ out["thinking"] = thinkinglevel === "none" ? { type: "disabled" } : { type: "enabled", budget };
315
+ }
316
+ delete out["reasoning_effort"];
317
+ }
318
+
319
+ // sampling params — omit-unspecified policy strips defaults
320
+ const policy = cfg.bodybuild?.optionalparamspolicy ?? "always";
321
+ const userfields: Record<string, unknown> = {};
322
+ const paramnames = [
323
+ "temperature",
324
+ "top_p",
325
+ "n",
326
+ "frequency_penalty",
327
+ "presence_penalty",
328
+ "seed",
329
+ "stop",
330
+ "max_tokens",
331
+ ];
332
+ for (const p of paramnames) {
333
+ if (body[p] !== undefined) userfields[p] = body[p];
334
+ }
335
+
336
+ if (policy === "omit-unspecified") {
337
+ // nvidia pattern — only include what the user set
338
+ if (userfields["temperature"] !== undefined) out["temperature"] = autotemp(userfields["temperature"]);
339
+ if (userfields["top_p"] !== undefined) out["top_p"] = autotopp(userfields["top_p"]);
340
+ if (userfields["n"] !== undefined) out["n"] = auton(userfields["n"]);
341
+ if (userfields["frequency_penalty"] !== undefined)
342
+ out["frequency_penalty"] = autofrequencypenalty(userfields["frequency_penalty"]);
343
+ if (userfields["presence_penalty"] !== undefined)
344
+ out["presence_penalty"] = autopresencepenalty(userfields["presence_penalty"]);
345
+ if (userfields["seed"] !== undefined) out["seed"] = autoseed(userfields["seed"]);
346
+ if (userfields["stop"] !== undefined) out["stop"] = userfields["stop"];
347
+ } else {
348
+ out["temperature"] = autotemp(userfields["temperature"]);
349
+ out["top_p"] = autotopp(userfields["top_p"]);
350
+ out["n"] = auton(userfields["n"]);
351
+ out["frequency_penalty"] = autofrequencypenalty(userfields["frequency_penalty"]);
352
+ out["presence_penalty"] = autopresencepenalty(userfields["presence_penalty"]);
353
+ if (userfields["seed"] !== undefined) out["seed"] = autoseed(userfields["seed"]);
354
+ if (userfields["stop"] !== undefined) out["stop"] = userfields["stop"];
355
+ }
356
+
357
+ // max tokens — clamp per config
358
+ const clampmax = cfg.bodybuild?.clampmaxtokens ?? true;
359
+ const clampceiling = cfg.defaults?.maxtokensclamp ?? 98304;
360
+ const modelmax = model?.maxoutput ?? clampceiling;
361
+ if (clampmax) {
362
+ out["max_tokens"] = clamp(Number(body["max_tokens"] ?? defaultmaxtokens), 1, Math.min(modelmax, clampceiling));
363
+ } else {
364
+ out["max_tokens"] = Number(body["max_tokens"] ?? defaultmaxtokens);
365
+ }
366
+
367
+ return out;
368
+ }
369
+
370
+ // ---------------------------------------------------------------------------
371
+ // history reconstruction — shared by v3 v4 pattern
372
+ // ---------------------------------------------------------------------------
373
+
374
+ /** reconstruct message history from db when context restoresessionhistory */
375
+ async function reconstructhistory(cfg: gatewayconfig, sessionid: string, incoming: unknown[]): Promise<unknown[]> {
376
+ if (!cfg.context?.restoresessionhistory || !sessionid) return incoming;
377
+ try {
378
+ const limit = cfg.context?.historylimit ?? 1000;
379
+ const past = (await getsessionmessages(sessionid, limit)) as Array<Record<string, unknown>>;
380
+ const history: unknown[] = [];
381
+ for (const row of past) {
382
+ if (row.role === "user" || row.role === "assistant") {
383
+ const role = row.role as string;
384
+ const content = String(row.content ?? row.completion ?? "");
385
+ if (content) history.push({ role, content });
386
+ }
387
+ }
388
+ // dedup prefix — incoming messages that repeat history
389
+ const incomingjson = incoming.map((m) => safestringify(m));
390
+ const _deduped: unknown[] = [];
391
+ let skip = 0;
392
+ for (let i = 0; i < Math.min(history.length, incomingjson.length); i++) {
393
+ const h = safestringify({
394
+ role: (history[i] as { role: string }).role,
395
+ content: (history[i] as { content: string }).content,
396
+ });
397
+ if (h === incomingjson[i]) skip = i + 1;
398
+ else break;
399
+ }
400
+ const merged = [...history.slice(skip), ...incoming];
401
+ return merged;
402
+ } catch {
403
+ return incoming;
404
+ }
405
+ }
406
+
407
+ /** truncate messages to fit the per-call context */
408
+ function fitcontext(cfg: gatewayconfig, messages: unknown[], model: modeldef | undefined): unknown[] {
409
+ if (!cfg.context) return messages;
410
+ const percall = model?.context ?? cfg.context?.percallfallback ?? 1048576;
411
+ const margin = cfg.context?.truncatemargin ?? 4096;
412
+ return truncatemessages(messages, percall - margin);
413
+ }
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // execution — fetch with retry backoff rotation timeout
417
+ // ---------------------------------------------------------------------------
418
+
419
+ /** sleep helper */
420
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
421
+
422
+ /** compute backoff with jitter */
423
+ function backoffms(cfg: gatewayconfig, attempt: number): number {
424
+ const retry = cfg.retry ?? {};
425
+ const base = retry.backoffbasems ?? 400;
426
+ const cap = retry.backoffcapms ?? 8000;
427
+ const jitter = retry.jitter ?? 0.2;
428
+ const raw = Math.min(cap, base * 2 ** attempt);
429
+ const j = raw * (1 - jitter + securerandom() * jitter * 2);
430
+ return Math.round(j);
431
+ }
432
+
433
+ /** classify a status against the retry rotate fallback sets
434
+ * the rotate set only comes from an explicit rotation config: the old
435
+ * implicit default (any maxretries turned on rotate-on-529-404-410) stole
436
+ * 404 and 410 from the fallback classification a user configured
437
+ * explicitly — rotation is declared, never inferred from retry settings */
438
+ function classifystatus(cfg: gatewayconfig, status: number): "retry" | "rotate" | "fallback" | "fail" {
439
+ const retryset = new Set(cfg.retry?.statuses ?? defaultretrystatuses);
440
+ const rotateset = new Set(cfg.rotation?.rotateonstatus ?? []);
441
+ const fallbackset = new Set(
442
+ cfg.retry?.fallbackstatuses ?? (cfg.retry?.fallback === "crossprovider" ? defaultfallbackstatuses : []),
443
+ );
444
+ const nonretryable = new Set(cfg.retry?.nonretryable ?? [400]);
445
+ if (nonretryable.has(status)) return "fail";
446
+ if (retryset.has(status)) return "retry";
447
+ if (rotateset.has(status)) return "rotate";
448
+ if (fallbackset.has(status)) return "fallback";
449
+ if (status >= 500) return "retry";
450
+ return "fail";
451
+ }
452
+
453
+ /** execute an upstream call with the full retry rotation timeout pipeline */
454
+ async function executefetch(
455
+ cfg: gatewayconfig,
456
+ upstream: upstreamdef,
457
+ url: string,
458
+ headers: Record<string, string>,
459
+ body: Record<string, unknown>,
460
+ stream: boolean,
461
+ session: sessionstate,
462
+ ): Promise<Response> {
463
+ const retry = cfg.retry ?? {};
464
+ const maxretries = retry.maxretries ?? 0;
465
+ const maxrotations = cfg.rotation?.maxmodelrotations ?? 1;
466
+ const timeoutms = cfg.timeout?.modelswitchintervalms ?? cfg.timeout?.requestms ?? 300000;
467
+ const streamtimeout = cfg.timeout?.streamms ?? 2147483647;
468
+ const rotationmodels = cfg.rotation?.models ?? [];
469
+
470
+ let lastresponse: Response | null = null;
471
+ let lasterror: unknown = null;
472
+
473
+ for (let rotation = 0; rotation < maxrotations; rotation++) {
474
+ // rotate model on subsequent attempts
475
+ const attempturl = url;
476
+ let attemptbody = body;
477
+ if (rotation > 0 && rotationmodels.length > 0) {
478
+ const idx = (session.rotationindex + rotation) % rotationmodels.length;
479
+ const nextmodel = rotationmodels[idx];
480
+ const model = findmodel(cfg, nextmodel);
481
+ if (model) {
482
+ attemptbody = { ...body, model: model.upstream ?? nextmodel };
483
+ session.model = nextmodel;
484
+ session.rotationindex = idx;
485
+ }
486
+ }
487
+
488
+ for (let attempt = 0; attempt <= maxretries; attempt++) {
489
+ // per-attempt timeout via abort controller on headers
490
+ const controller = new AbortController();
491
+ const timer = setTimeout(() => controller.abort(), stream ? streamtimeout : timeoutms);
492
+ try {
493
+ const key = await resolveauth(cfg);
494
+ const finalheaders = { ...headers, ...(key ? buildauthheaders(cfg.auth, key.key) : {}) };
495
+ const res = await fetch(attempturl, {
496
+ method: "POST",
497
+ headers: finalheaders,
498
+ body: safestringify(attemptbody),
499
+ signal: controller.signal,
500
+ });
501
+ clearTimeout(timer);
502
+
503
+ if (res.ok) return res;
504
+
505
+ const action = classifystatus(cfg, res.status);
506
+ if (action === "retry" && attempt < maxretries) {
507
+ await sleep(backoffms(cfg, attempt));
508
+ continue;
509
+ }
510
+ if (action === "rotate" || action === "fallback") {
511
+ lastresponse = res;
512
+ break; // break retry loop — try next rotation
513
+ }
514
+ return res; // fail — pass through the error
515
+ } catch (err) {
516
+ clearTimeout(timer);
517
+ lasterror = err;
518
+ const aborted = err instanceof Error && err.name === "AbortError";
519
+ if (aborted && cfg.rotation?.rotateontimeout) {
520
+ break; // timeout — rotate model
521
+ }
522
+ if (attempt < maxretries) {
523
+ await sleep(backoffms(cfg, attempt));
524
+ continue;
525
+ }
526
+ if (rotation + 1 < maxrotations) break;
527
+ }
528
+ }
529
+ // cross-provider fallback when configured
530
+ if (cfg.retry?.fallback === "crossprovider" && upstream.fallback) {
531
+ const fb = cfg.upstreams.find((u) => u.name === upstream.fallback);
532
+ if (fb) {
533
+ try {
534
+ const key = await resolveauth(cfg);
535
+ const finalheaders = { ...headers, ...(key ? buildauthheaders(cfg.auth, key.key) : {}) };
536
+ const res = await fetch(fb.baseurl + (fb.endpoints?.chat ?? "/chat/completions"), {
537
+ method: "POST",
538
+ headers: finalheaders,
539
+ body: safestringify(attemptbody),
540
+ signal: AbortSignal.timeout(cfg.timeout?.requestms ?? 300000),
541
+ });
542
+ if (res.ok) return res;
543
+ lastresponse = res;
544
+ } catch (err) {
545
+ lasterror = err;
546
+ }
547
+ }
548
+ }
549
+ }
550
+
551
+ if (lastresponse) return lastresponse;
552
+ return new Response(
553
+ safestringify({
554
+ error: lasterror instanceof Error ? lasterror.message : "upstream request failed",
555
+ object: "error",
556
+ }),
557
+ { status: 502, headers: jsonheaders() },
558
+ );
559
+ }
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // response builders
563
+ // ---------------------------------------------------------------------------
564
+
565
+ /** build a standard chat completion response */
566
+ function chatcompletion(
567
+ cfg: gatewayconfig,
568
+ id: string,
569
+ model: string,
570
+ content: string,
571
+ reasoning: string | undefined,
572
+ usage: Record<string, unknown>,
573
+ ): Record<string, unknown> {
574
+ return {
575
+ id,
576
+ object: "chat.completion",
577
+ created: Math.floor(Date.now() / 1000),
578
+ model,
579
+ choices: [
580
+ {
581
+ index: 0,
582
+ message: {
583
+ role: "assistant",
584
+ content,
585
+ ...(reasoning ? { reasoning_content: reasoning } : {}),
586
+ },
587
+ finish_reason: "stop",
588
+ },
589
+ ],
590
+ usage,
591
+ provider: cfg.providername,
592
+ };
593
+ }
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // info keys models payload generators
597
+ // ---------------------------------------------------------------------------
598
+
599
+ /** generate the version info payload from config */
600
+ function makeinfopayload(cfg: gatewayconfig): Record<string, unknown> {
601
+ const modelcount = cfg.models.length;
602
+ return {
603
+ version: cfg.id,
604
+ name: cfg.name ?? cfg.id,
605
+ provider: cfg.providername,
606
+ paused: cfg.paused ?? false,
607
+ upstream: cfg.upstreams[0]?.baseurl ?? "",
608
+ upstreams: cfg.upstreams.map((u) => ({ name: u.name, baseurl: u.baseurl })),
609
+ auth: {
610
+ mode: cfg.auth.mode,
611
+ required: cfg.auth.required ?? false,
612
+ ...(cfg.auth.envvar ? { envvar: cfg.auth.envvar } : {}),
613
+ ...(cfg.auth.signupurl ? { signup: cfg.auth.signupurl } : {}),
614
+ },
615
+ models: modelcount,
616
+ ...(cfg.rotation
617
+ ? {
618
+ rotation: {
619
+ mode: cfg.rotation.mode,
620
+ models: cfg.rotation.models.length,
621
+ everynmessages: cfg.rotation.everynmessages ?? 6,
622
+ },
623
+ }
624
+ : {}),
625
+ ...(cfg.retry?.maxretries
626
+ ? {
627
+ retry: {
628
+ maxretries: cfg.retry.maxretries,
629
+ backoffbasems: cfg.retry.backoffbasems ?? 400,
630
+ backoffcapms: cfg.retry.backoffcapms ?? 8000,
631
+ statuses: cfg.retry.statuses ?? defaultretrystatuses,
632
+ },
633
+ }
634
+ : {}),
635
+ ...(cfg.timeout?.modelswitchintervalms ? { modeltimeoutms: cfg.timeout.modelswitchintervalms } : {}),
636
+ fusioncontext: fusioncontext(cfg),
637
+ metamodel: {
638
+ id: cfg.metamodel.id,
639
+ context: cfg.metamodel.contextoverride ?? fusioncontext(cfg),
640
+ maxoutput: cfg.metamodel.maxoutput ?? 32768,
641
+ ...(cfg.metamodel.pattern ? { pattern: cfg.metamodel.pattern } : {}),
642
+ },
643
+ routes: ["chat/completions", "completions", "messages", "responses", "embeddings", "keys", "models"],
644
+ ...(cfg.note ? { note: cfg.note } : {}),
645
+ };
646
+ }
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // engine — createversion produces all 7 handlers
650
+ // ---------------------------------------------------------------------------
651
+
652
+ /**
653
+ * createversion — the universal engine entry
654
+ * consumes one gatewayconfig produces all 7 route handlers
655
+ * every behavior is config driven — nothing hardcoded
656
+ */
657
+ export function createversion(cfg: gatewayconfig): versionhandlers {
658
+ const sessions = createsessionstore();
659
+ const globalcounter = { n: 0 };
660
+ const metaid = cfg.metamodel.id;
661
+
662
+ // ---------------------------------------------------------------------
663
+ // invalidbody — 400 for malformed request bodies (openai semantics:
664
+ // malformed json or a missing empty messages array answers 400 before
665
+ // any upstream call — the old flow forwarded the empty request and
666
+ // surfaced a confusing upstream error far from the real cause)
667
+ // ---------------------------------------------------------------------
668
+ function invalidbody(message: string): Response {
669
+ return new Response(safestringify({ error: message, code: "invalid_request", object: "error" }), {
670
+ status: 400,
671
+ headers: jsonheaders(),
672
+ });
673
+ }
674
+
675
+ /** parse and validate a request body — null when the body is not a json
676
+ * object (numbers strings arrays and bare null all reject: gateway post
677
+ * routes consume objects) */
678
+ async function parsebody(req: Request): Promise<Record<string, unknown> | null> {
679
+ const raw = await req.text();
680
+ const parsed = safejsonparse(raw);
681
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
682
+ return parsed as Record<string, unknown>;
683
+ }
684
+
685
+ // ---------------------------------------------------------------------
686
+ // handleinfo — get routes return the version descriptor
687
+ // ---------------------------------------------------------------------
688
+ async function handleinfo(req: Request): Promise<Response> {
689
+ const cors = handlecors(req);
690
+ if (cors) return cors;
691
+ return new Response(safestringify(makeinfopayload(cfg)), { headers: jsonheaders() });
692
+ }
693
+
694
+ // ---------------------------------------------------------------------
695
+ // handlekeys — key status endpoint
696
+ // ---------------------------------------------------------------------
697
+ async function handlekeys(req: Request): Promise<Response> {
698
+ const cors = handlecors(req);
699
+ if (cors) return cors;
700
+ const auth = cfg.auth;
701
+ const keycount =
702
+ auth.keysources?.includes("env") && auth.envvar
703
+ ? (process.env[auth.envvar] ?? "").split(auth.envseparator ?? ",").filter(Boolean).length
704
+ : 0;
705
+ return new Response(
706
+ safestringify({
707
+ authmode: auth.mode,
708
+ required: auth.required ?? false,
709
+ ...(auth.envvar ? { envvar: auth.envvar, envkeycount: keycount } : {}),
710
+ ...(auth.signupurl ? { signup: auth.signupurl } : {}),
711
+ ...(auth.keyurl ? { keyurl: auth.keyurl, keyexpiryminutes: auth.keyexpiryminutes ?? 60 } : {}),
712
+ provider: cfg.providername,
713
+ upstream: cfg.upstreams[0]?.baseurl ?? "",
714
+ models: cfg.models.map((m) => m.id),
715
+ version: cfg.id,
716
+ }),
717
+ { headers: jsonheaders() },
718
+ );
719
+ }
720
+
721
+ // ---------------------------------------------------------------------
722
+ // handlemodels — post models returns the catalog
723
+ // ---------------------------------------------------------------------
724
+ async function handlemodels(req: Request): Promise<Response> {
725
+ const cors = handlecors(req);
726
+ if (cors) return cors;
727
+ const models = cfg.models.map((m) => ({
728
+ id: m.id,
729
+ object: "model",
730
+ created: cfg.metamodel.listedcreated ?? 1700000000,
731
+ owned_by: cfg.providername,
732
+ permission: [],
733
+ root: m.id,
734
+ parent: null,
735
+ max_context_length: m.context,
736
+ max_output_tokens: m.maxoutput,
737
+ supports_thinking: m.reasoning ?? true,
738
+ supports_streaming: true,
739
+ supports_tools: true,
740
+ supports_vision: m.vision ?? false,
741
+ ...(m.free !== undefined ? { free: m.free } : {}),
742
+ ...(m.rank ? { rank: m.rank } : {}),
743
+ }));
744
+ // meta model first in the list
745
+ const metaentry = {
746
+ id: metaid,
747
+ object: "model",
748
+ created: cfg.metamodel.listedcreated ?? 1700000000,
749
+ owned_by: metaid,
750
+ permission: [],
751
+ root: metaid,
752
+ parent: null,
753
+ max_context_length: cfg.metamodel.contextoverride ?? fusioncontext(cfg),
754
+ max_output_tokens: cfg.metamodel.maxoutput ?? 32768,
755
+ supports_thinking: true,
756
+ supports_streaming: true,
757
+ supports_tools: true,
758
+ supports_vision: false,
759
+ description: cfg.metamodel.pattern ?? `${metaid} meta model over ${cfg.providername}`,
760
+ meta: {
761
+ provider: cfg.providername,
762
+ ...(cfg.rotation?.models.length ? { rotationbackends: cfg.rotation.models } : {}),
763
+ },
764
+ };
765
+ return new Response(safestringify({ object: "list", data: [metaentry, ...models] }), {
766
+ headers: jsonheaders(),
767
+ });
768
+ }
769
+
770
+ // ---------------------------------------------------------------------
771
+ // paused check — shared by all post routes
772
+ // ---------------------------------------------------------------------
773
+ function pausedresponse(): Response | null {
774
+ if (!cfg.paused) return null;
775
+ const routes = cfg.routes ?? {};
776
+ return new Response(
777
+ safestringify({
778
+ error: routes.pausedmessage ?? `${cfg.id} is paused`,
779
+ fallback: routes.fallbackroute,
780
+ status: routes.pausedstatus ?? 503,
781
+ object: "error",
782
+ }),
783
+ { status: routes.pausedstatus ?? 503, headers: jsonheaders() },
784
+ );
785
+ }
786
+
787
+ // ---------------------------------------------------------------------
788
+ // handlechatcompletions — the main chat route
789
+ // ---------------------------------------------------------------------
790
+ async function handlechatcompletions(req: Request): Promise<Response> {
791
+ const cors = handlecors(req);
792
+ if (cors) return cors;
793
+ const paused = pausedresponse();
794
+ if (paused) return paused;
795
+
796
+ const startedat = Date.now();
797
+ try {
798
+ const body = await parsebody(req);
799
+ if (body === null) return invalidbody("request body is not valid json");
800
+ const incoming = Array.isArray(body["messages"]) ? body["messages"] : [];
801
+ if (incoming.length === 0) return invalidbody("messages must be a non-empty array");
802
+ const stream = body["stream"] === true;
803
+ const sessionid = extractsessionid(req, body) || `s_${Date.now()}_${randombase36(6)}`;
804
+ const requestid = extractrequestid(req) || genid("req");
805
+ const session = sessions.bump(sessionid);
806
+
807
+ // resolve model
808
+ const resolved = resolvemodel(cfg, body["model"] as string | undefined, session, globalcounter);
809
+ const displaymodel = resolved.display || metaid;
810
+
811
+ // reconstruct history when configured
812
+ const history = await reconstructhistory(cfg, sessionid, incoming);
813
+ const fitted = fitcontext(cfg, history, resolved.model);
814
+
815
+ // ------------------------------------------------------------------
816
+ // zai sdk transport — 2-calls thinking pattern
817
+ // ------------------------------------------------------------------
818
+ if (cfg.transport?.type === "zai-sdk") {
819
+ return await executezaisdk(cfg, body, fitted, resolved, displaymodel, stream, {
820
+ sessionid,
821
+ requestid,
822
+ startedat,
823
+ });
824
+ }
825
+
826
+ // ------------------------------------------------------------------
827
+ // fetch transport — standard pipeline
828
+ // ------------------------------------------------------------------
829
+ const upstream = cfg.upstreams[0];
830
+ if (!upstream) {
831
+ return new Response(safestringify({ error: "no upstream configured", object: "error" }), {
832
+ status: 500,
833
+ headers: jsonheaders(),
834
+ });
835
+ }
836
+
837
+ // auth check
838
+ const auth = cfg.auth;
839
+ if (auth.required && auth.mode !== "keylesssdk" && auth.mode !== "anonymous" && auth.mode !== "none") {
840
+ const keys = await resolvekeys(auth);
841
+ if (keys.length === 0) {
842
+ return new Response(
843
+ safestringify({
844
+ error: auth.envvar
845
+ ? `${cfg.providername} api key required — set ${auth.envvar} env var${auth.signupurl ? ` or signup at ${auth.signupurl}` : ""}`
846
+ : "api key required",
847
+ code: "key_required",
848
+ ...(auth.signupurl ? { signup: auth.signupurl } : {}),
849
+ }),
850
+ { status: 401, headers: jsonheaders() },
851
+ );
852
+ }
853
+ }
854
+
855
+ // build body
856
+ const upstreambody = buildbody(cfg, { ...body, messages: fitted, model: resolved.modelid }, resolved.model);
857
+
858
+ // execute
859
+ const url = upstream.baseurl + (upstream.endpoints?.chat ?? "/chat/completions");
860
+ const headers = buildheaders(cfg, null, stream);
861
+ const upstreamresponse = await executefetch(cfg, upstream, url, headers, upstreambody, stream, session);
862
+
863
+ if (!upstreamresponse.ok && !stream) {
864
+ const errtext = await upstreamresponse.text().catch(() => "");
865
+ await persist(cfg, {
866
+ sessionid,
867
+ requestid,
868
+ route: "chat/completions",
869
+ model: displaymodel,
870
+ modelvariant: resolved.modelid,
871
+ status: "error",
872
+ httpstatus: upstreamresponse.status,
873
+ error: errtext.slice(0, 500),
874
+ stream,
875
+ startedat,
876
+ rotationindex: resolved.rotationindex,
877
+ messagenumber: session.messagecount,
878
+ });
879
+ return new Response(errtext || safestringify({ error: "upstream error", status: upstreamresponse.status }), {
880
+ status: upstreamresponse.status,
881
+ headers: jsonheaders(),
882
+ });
883
+ }
884
+
885
+ if (stream) {
886
+ // stream — byte passthrough with masking and keepalive
887
+ await persist(cfg, {
888
+ sessionid,
889
+ requestid,
890
+ route: "chat/completions",
891
+ model: displaymodel,
892
+ modelvariant: resolved.modelid,
893
+ status: "completed",
894
+ stream,
895
+ streammode: "bytepassthrough",
896
+ startedat,
897
+ rotationindex: resolved.rotationindex,
898
+ messagenumber: session.messagecount,
899
+ contextshared: resolved.ismeta,
900
+ });
901
+ return makestreamresponse(upstreamresponse, {
902
+ mask: cfg.metamodel.maskupstreammodel ? metaid : null,
903
+ onfinal: (tracked) => {
904
+ void persist(cfg, {
905
+ sessionid,
906
+ requestid,
907
+ route: "chat/completions",
908
+ model: displaymodel,
909
+ modelvariant: resolved.modelid,
910
+ status: "completed",
911
+ stream,
912
+ startedat,
913
+ content: tracked.content?.slice(0, 2000),
914
+ reasoningcontent: tracked.reasoning?.slice(0, 2000),
915
+ totaltokens: tracked.totaltokens,
916
+ rotationindex: resolved.rotationindex,
917
+ messagenumber: session.messagecount,
918
+ });
919
+ },
920
+ });
921
+ }
922
+
923
+ // non stream — parse and rebuild with display model
924
+ const data = (await safejsonparse(await upstreamresponse.text())) as Record<string, unknown>;
925
+ const choices = (data?.["choices"] as Array<Record<string, unknown>> | undefined) ?? [];
926
+ const choice = choices[0]?.["message"] as Record<string, unknown> | undefined;
927
+ const content = String(choice?.["content"] ?? "");
928
+ const reasoning = String(choice?.["reasoning_content"] ?? choice?.["reasoning"] ?? "");
929
+ const usage = (data?.["usage"] as Record<string, unknown>) ?? {
930
+ prompt_tokens: esttokens(safestringify(fitted)),
931
+ completion_tokens: Math.ceil(content.length / 4),
932
+ total_tokens: esttokens(safestringify(fitted)) + Math.ceil(content.length / 4),
933
+ };
934
+ const responseid = String(data?.["id"] ?? genid());
935
+ const payload = chatcompletion(cfg, responseid, displaymodel, content, reasoning || undefined, usage);
936
+
937
+ await persist(cfg, {
938
+ sessionid,
939
+ requestid,
940
+ route: "chat/completions",
941
+ model: displaymodel,
942
+ modelvariant: resolved.modelid,
943
+ status: "completed",
944
+ stream,
945
+ startedat,
946
+ content: String(content).slice(0, 2000),
947
+ reasoningcontent: String(reasoning).slice(0, 2000),
948
+ prompttokens: Number(usage["prompt_tokens"] ?? 0),
949
+ completiontokens: Number(usage["completion_tokens"] ?? 0),
950
+ totaltokens: Number(usage["total_tokens"] ?? 0),
951
+ rotationindex: resolved.rotationindex,
952
+ messagenumber: session.messagecount,
953
+ contextshared: resolved.ismeta,
954
+ });
955
+ return new Response(safestringify(payload), { headers: jsonheaders() });
956
+ } catch (err) {
957
+ return new Response(
958
+ safestringify({
959
+ error: err instanceof Error ? err.message : "internal error",
960
+ object: "error",
961
+ }),
962
+ {
963
+ status: 500,
964
+ headers: jsonheaders(),
965
+ },
966
+ );
967
+ }
968
+ }
969
+
970
+ // ---------------------------------------------------------------------
971
+ // zai sdk execution — 2-calls thinking pattern with fresh response guarantee
972
+ // ---------------------------------------------------------------------
973
+ async function executezaisdk(
974
+ cfg: gatewayconfig,
975
+ body: Record<string, unknown>,
976
+ messages: unknown[],
977
+ resolved: { model: modeldef | undefined; modelid: string; display: string; ismeta: boolean },
978
+ displaymodel: string,
979
+ stream: boolean,
980
+ meta: { sessionid: string; requestid: string; startedat: number },
981
+ ): Promise<Response> {
982
+ const budgets = cfg.thinking?.budgets ?? defaultbudgets;
983
+ const level = autothinking(body["thinking"] ?? body["reasoning_effort"]);
984
+ const thinkingbudget = budgets[level] ?? 0;
985
+ const responsemax = Number(body["max_tokens"] ?? cfg.metamodel.maxoutput ?? 32768);
986
+ const threshold = cfg.thinking?.twocalls?.threshold ?? 98304;
987
+ const twocalls = thinkingbudget + responsemax > threshold || level !== "none";
988
+
989
+ try {
990
+ if (twocalls) {
991
+ // call 1 — thinking enabled non stream
992
+ const call1 = await zaisdkcall(messages, {
993
+ thinking: { type: "enabled", budget: thinkingbudget },
994
+ max_tokens: Math.min(thinkingbudget, threshold),
995
+ });
996
+ const reasoning = call1.reasoning ?? "";
997
+
998
+ if (stream) {
999
+ // stream path — emit reasoning deltas then content
1000
+ const content = call1.content ?? "";
1001
+ const sametext = reasoning === content;
1002
+ let finalcontent = content;
1003
+ if (sametext && cfg.thinking?.twocalls?.freshresponseguarantee !== false) {
1004
+ // thinking equals response — force call 2 with original messages for a fresh response
1005
+ const call2 = await zaisdkcall(messages, {
1006
+ thinking: { type: "disabled" },
1007
+ max_tokens: Math.min(responsemax, threshold),
1008
+ });
1009
+ finalcontent = call2.content ?? content;
1010
+ }
1011
+ const id = genid();
1012
+ const encoder = new TextEncoder();
1013
+ const readable = new ReadableStream({
1014
+ async start(controller) {
1015
+ try {
1016
+ // emit reasoning deltas in 50 char chunks
1017
+ for (let i = 0; i < reasoning.length; i += 50) {
1018
+ const piece = reasoning.slice(i, i + 50);
1019
+ controller.enqueue(
1020
+ encoder.encode(`data: ${safestringify(makechunk(id, displaymodel, "", piece))}\n\n`),
1021
+ );
1022
+ }
1023
+ // emit content deltas in 50 char chunks
1024
+ for (let i = 0; i < finalcontent.length; i += 50) {
1025
+ const piece = finalcontent.slice(i, i + 50);
1026
+ controller.enqueue(encoder.encode(`data: ${safestringify(makechunk(id, displaymodel, piece))}\n\n`));
1027
+ }
1028
+ controller.enqueue(
1029
+ encoder.encode(`data: ${safestringify(makefinalchunk(id, displaymodel, "stop"))}\n\n`),
1030
+ );
1031
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
1032
+ controller.close();
1033
+ } catch {
1034
+ try {
1035
+ controller.close();
1036
+ } catch {
1037
+ /* already closed */
1038
+ }
1039
+ }
1040
+ },
1041
+ });
1042
+ await persist(cfg, {
1043
+ ...meta,
1044
+ route: "chat/completions",
1045
+ model: displaymodel,
1046
+ modelvariant: resolved.modelid,
1047
+ status: "completed",
1048
+ stream: true,
1049
+ streammode: "twocalls",
1050
+ content: finalcontent.slice(0, 2000),
1051
+ reasoningcontent: reasoning.slice(0, 2000),
1052
+ twocalls: true,
1053
+ thinkingbudget,
1054
+ });
1055
+ return new Response(readable, {
1056
+ headers: { ...corsheaders, "content-type": "text/event-stream" },
1057
+ });
1058
+ }
1059
+
1060
+ // non stream path
1061
+ const sametext = reasoning === call1.content;
1062
+ let content = call1.content ?? "";
1063
+ if (sametext && cfg.thinking?.twocalls?.freshresponseguarantee !== false) {
1064
+ const call2 = await zaisdkcall(messages, {
1065
+ thinking: { type: "disabled" },
1066
+ max_tokens: Math.min(responsemax, threshold),
1067
+ });
1068
+ content = call2.content ?? content;
1069
+ }
1070
+ const id = genid();
1071
+ const payload = chatcompletion(cfg, id, displaymodel, content, reasoning || undefined, {
1072
+ prompt_tokens: esttokens(safestringify(messages)),
1073
+ completion_tokens: Math.ceil((content.length + reasoning.length) / 4),
1074
+ total_tokens: esttokens(safestringify(messages)) + Math.ceil((content.length + reasoning.length) / 4),
1075
+ });
1076
+ await persist(cfg, {
1077
+ ...meta,
1078
+ route: "chat/completions",
1079
+ model: displaymodel,
1080
+ modelvariant: resolved.modelid,
1081
+ status: "completed",
1082
+ stream: false,
1083
+ content: content.slice(0, 2000),
1084
+ reasoningcontent: reasoning.slice(0, 2000),
1085
+ twocalls: true,
1086
+ thinkingbudget,
1087
+ totaltokens: Number(payload["usage"] ? (payload["usage"] as Record<string, unknown>)["total_tokens"] : 0),
1088
+ });
1089
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1090
+ }
1091
+
1092
+ // single call — no thinking
1093
+ const result = await zaisdkcall(messages, { max_tokens: responsemax });
1094
+ const id = genid();
1095
+ const payload = chatcompletion(cfg, id, displaymodel, result.content, result.reasoning || undefined, {
1096
+ prompt_tokens: esttokens(safestringify(messages)),
1097
+ completion_tokens: Math.ceil(result.content.length / 4),
1098
+ total_tokens: esttokens(safestringify(messages)) + Math.ceil(result.content.length / 4),
1099
+ });
1100
+ await persist(cfg, {
1101
+ ...meta,
1102
+ route: "chat/completions",
1103
+ model: displaymodel,
1104
+ modelvariant: resolved.modelid,
1105
+ status: "completed",
1106
+ stream: false,
1107
+ content: result.content.slice(0, 2000),
1108
+ reasoningcontent: (result.reasoning ?? "").slice(0, 2000),
1109
+ });
1110
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1111
+ } catch (err) {
1112
+ return new Response(
1113
+ safestringify({
1114
+ error: err instanceof Error ? err.message : "zai sdk call failed",
1115
+ object: "error",
1116
+ }),
1117
+ {
1118
+ status: 500,
1119
+ headers: jsonheaders(),
1120
+ },
1121
+ );
1122
+ }
1123
+ }
1124
+
1125
+ // ---------------------------------------------------------------------
1126
+ // handlecompletions — legacy completions route
1127
+ // ---------------------------------------------------------------------
1128
+ async function handlecompletions(req: Request): Promise<Response> {
1129
+ const cors = handlecors(req);
1130
+ if (cors) return cors;
1131
+ const paused = pausedresponse();
1132
+ if (paused) return paused;
1133
+ try {
1134
+ const body = await parsebody(req);
1135
+ if (body === null) return invalidbody("request body is not valid json");
1136
+ const prompt = body["prompt"];
1137
+ if (typeof prompt !== "string" && !Array.isArray(prompt)) {
1138
+ return invalidbody("prompt must be a string or an array of strings");
1139
+ }
1140
+ if (prompt.length === 0) return invalidbody("prompt must not be empty");
1141
+ const sessionid = extractsessionid(req, body) || `s_${Date.now()}`;
1142
+ const session = sessions.bump(sessionid);
1143
+ const resolved = resolvemodel(cfg, body["model"] as string | undefined, session, globalcounter);
1144
+
1145
+ // convert prompt to messages
1146
+ const messages =
1147
+ typeof prompt === "string"
1148
+ ? [{ role: "user", content: prompt }]
1149
+ : Array.isArray(prompt)
1150
+ ? prompt.map((p) => ({ role: "user", content: p }))
1151
+ : [];
1152
+
1153
+ if (cfg.transport?.type === "zai-sdk") {
1154
+ const result = await zaisdkcall(messages, {
1155
+ max_tokens: Number(body["max_tokens"] ?? defaultmaxtokens),
1156
+ });
1157
+ const payload = {
1158
+ id: genid(),
1159
+ object: "text_completion",
1160
+ created: Math.floor(Date.now() / 1000),
1161
+ model: resolved.display,
1162
+ choices: [{ index: 0, text: result.content, finish_reason: "stop" }],
1163
+ usage: {
1164
+ prompt_tokens: esttokens(safestringify(messages)),
1165
+ completion_tokens: Math.ceil(result.content.length / 4),
1166
+ },
1167
+ provider: cfg.providername,
1168
+ };
1169
+ await persist(cfg, {
1170
+ sessionid,
1171
+ requestid: genid("req"),
1172
+ route: "completions",
1173
+ model: resolved.display,
1174
+ status: "completed",
1175
+ startedat: Date.now(),
1176
+ });
1177
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1178
+ }
1179
+
1180
+ const upstream = cfg.upstreams[0];
1181
+ const upstreambody = buildbody(cfg, { ...body, messages, model: resolved.modelid }, resolved.model);
1182
+ delete upstreambody["prompt"];
1183
+ const url = upstream.baseurl + (upstream.endpoints?.completions ?? "/chat/completions");
1184
+ const headers = buildheaders(cfg, null, body["stream"] === true);
1185
+ const res = await executefetch(cfg, upstream, url, headers, upstreambody, body["stream"] === true, session);
1186
+
1187
+ if (body["stream"] === true && res.ok) {
1188
+ return makestreamresponse(res, { mask: cfg.metamodel.maskupstreammodel ? metaid : null });
1189
+ }
1190
+ const data = (await safejsonparse(await res.text())) as Record<string, unknown>;
1191
+ // adapt chat completion to text completion
1192
+ const choices = (data?.["choices"] as Array<Record<string, unknown>> | undefined) ?? [];
1193
+ const choice = choices[0]?.["message"] as Record<string, unknown> | undefined;
1194
+ const payload = {
1195
+ id: data?.["id"] ?? genid(),
1196
+ object: "text_completion",
1197
+ created: data?.["created"] ?? Math.floor(Date.now() / 1000),
1198
+ model: resolved.display,
1199
+ choices: [{ index: 0, text: String(choice?.["content"] ?? ""), finish_reason: "stop" }],
1200
+ usage: (data?.["usage"] as Record<string, unknown>) ?? {},
1201
+ provider: cfg.providername,
1202
+ };
1203
+ await persist(cfg, {
1204
+ sessionid,
1205
+ requestid: genid("req"),
1206
+ route: "completions",
1207
+ model: resolved.display,
1208
+ status: "completed",
1209
+ startedat: Date.now(),
1210
+ });
1211
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1212
+ } catch (err) {
1213
+ return new Response(safestringify({ error: err instanceof Error ? err.message : "internal error" }), {
1214
+ status: 500,
1215
+ headers: jsonheaders(),
1216
+ });
1217
+ }
1218
+ }
1219
+
1220
+ // ---------------------------------------------------------------------
1221
+ // handlemessages — anthropic messages format
1222
+ // ---------------------------------------------------------------------
1223
+ async function handlemessages(req: Request): Promise<Response> {
1224
+ const cors = handlecors(req);
1225
+ if (cors) return cors;
1226
+ const paused = pausedresponse();
1227
+ if (paused) return paused;
1228
+ try {
1229
+ const body = await parsebody(req);
1230
+ if (body === null) return invalidbody("request body is not valid json");
1231
+ const sessionid = extractsessionid(req, body) || `s_${Date.now()}`;
1232
+ const session = sessions.bump(sessionid);
1233
+ const resolved = resolvemodel(cfg, body["model"] as string | undefined, session, globalcounter);
1234
+
1235
+ // convert anthropic format to openai
1236
+ const anthropicmessages = Array.isArray(body["messages"]) ? body["messages"] : [];
1237
+ if (anthropicmessages.length === 0) {
1238
+ return invalidbody("messages must be a non-empty array");
1239
+ }
1240
+ const messages = anthropicmessages.map((m: Record<string, unknown>) => ({
1241
+ role: m["role"] === "assistant" ? "assistant" : "user",
1242
+ content:
1243
+ typeof m["content"] === "string"
1244
+ ? m["content"]
1245
+ : Array.isArray(m["content"])
1246
+ ? (m["content"] as Array<Record<string, unknown>>).map((c) => c["text"] ?? "").join("\n")
1247
+ : "",
1248
+ }));
1249
+ if (body["system"]) messages.unshift({ role: "system", content: String(body["system"]) });
1250
+
1251
+ if (cfg.transport?.type === "zai-sdk") {
1252
+ const result = await zaisdkcall(messages, {
1253
+ max_tokens: Number(body["max_tokens"] ?? 4096),
1254
+ });
1255
+ const payload = {
1256
+ id: `msg_${genid("")}`,
1257
+ type: "message",
1258
+ role: "assistant",
1259
+ model: resolved.display,
1260
+ content: [{ type: "text", text: result.content }],
1261
+ ...(result.reasoning ? { reasoning_content: result.reasoning } : {}),
1262
+ stop_reason: "end_turn",
1263
+ usage: {
1264
+ input_tokens: esttokens(safestringify(messages)),
1265
+ output_tokens: Math.ceil(result.content.length / 4),
1266
+ },
1267
+ };
1268
+ await persist(cfg, {
1269
+ sessionid,
1270
+ requestid: genid("req"),
1271
+ route: "messages",
1272
+ model: resolved.display,
1273
+ status: "completed",
1274
+ startedat: Date.now(),
1275
+ });
1276
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1277
+ }
1278
+
1279
+ const upstream = cfg.upstreams[0];
1280
+ const upstreambody = buildbody(
1281
+ cfg,
1282
+ { ...body, messages, model: resolved.modelid, max_tokens: body["max_tokens"] ?? 4096 },
1283
+ resolved.model,
1284
+ );
1285
+ const url = upstream.baseurl + (upstream.endpoints?.chat ?? "/chat/completions");
1286
+ const headers = buildheaders(cfg, null, body["stream"] === true);
1287
+ const res = await executefetch(cfg, upstream, url, headers, upstreambody, body["stream"] === true, session);
1288
+
1289
+ if (body["stream"] === true && res.ok) {
1290
+ return makestreamresponse(res, { mask: cfg.metamodel.maskupstreammodel ? metaid : null });
1291
+ }
1292
+ const data = (await safejsonparse(await res.text())) as Record<string, unknown>;
1293
+ const choices = (data?.["choices"] as Array<Record<string, unknown>> | undefined) ?? [];
1294
+ const choice = choices[0]?.["message"] as Record<string, unknown> | undefined;
1295
+ const payload = {
1296
+ id: `msg_${data?.["id"] ?? genid("")}`,
1297
+ type: "message",
1298
+ role: "assistant",
1299
+ model: resolved.display,
1300
+ content: [{ type: "text", text: String(choice?.["content"] ?? "") }],
1301
+ ...(choice?.["reasoning_content"] ? { reasoning_content: choice["reasoning_content"] } : {}),
1302
+ stop_reason: "end_turn",
1303
+ usage: {
1304
+ input_tokens: Number((data?.["usage"] as Record<string, unknown>)?.["prompt_tokens"] ?? 0),
1305
+ output_tokens: Number((data?.["usage"] as Record<string, unknown>)?.["completion_tokens"] ?? 0),
1306
+ },
1307
+ };
1308
+ await persist(cfg, {
1309
+ sessionid,
1310
+ requestid: genid("req"),
1311
+ route: "messages",
1312
+ model: resolved.display,
1313
+ status: "completed",
1314
+ startedat: Date.now(),
1315
+ });
1316
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1317
+ } catch (err) {
1318
+ return new Response(safestringify({ error: err instanceof Error ? err.message : "internal error" }), {
1319
+ status: 500,
1320
+ headers: jsonheaders(),
1321
+ });
1322
+ }
1323
+ }
1324
+
1325
+ // ---------------------------------------------------------------------
1326
+ // handleresponses — openai responses format
1327
+ // ---------------------------------------------------------------------
1328
+ async function handleresponses(req: Request): Promise<Response> {
1329
+ const cors = handlecors(req);
1330
+ if (cors) return cors;
1331
+ const paused = pausedresponse();
1332
+ if (paused) return paused;
1333
+ try {
1334
+ const body = await parsebody(req);
1335
+ if (body === null) return invalidbody("request body is not valid json");
1336
+ const sessionid = extractsessionid(req, body) || `s_${Date.now()}`;
1337
+ const session = sessions.bump(sessionid);
1338
+ const resolved = resolvemodel(cfg, body["model"] as string | undefined, session, globalcounter);
1339
+
1340
+ // responses format — input can be string or structured
1341
+ const input = body["input"] ?? body["messages"];
1342
+ if (typeof input !== "string" && !Array.isArray(input)) {
1343
+ return invalidbody("input must be a string or an array");
1344
+ }
1345
+ if (Array.isArray(input) && input.length === 0) {
1346
+ return invalidbody("input must not be empty");
1347
+ }
1348
+ const messages =
1349
+ typeof input === "string" ? [{ role: "user", content: input }] : Array.isArray(input) ? input : [];
1350
+
1351
+ if (cfg.transport?.type === "zai-sdk") {
1352
+ const result = await zaisdkcall(messages, {
1353
+ max_tokens: Number(body["max_output_tokens"] ?? 4096),
1354
+ });
1355
+ const responseid = `resp_${genid("")}`;
1356
+ const payload = {
1357
+ id: responseid,
1358
+ object: "response",
1359
+ created_at: Math.floor(Date.now() / 1000),
1360
+ model: resolved.display,
1361
+ status: "completed",
1362
+ output: [
1363
+ {
1364
+ type: "message",
1365
+ id: `msg_${genid("")}`,
1366
+ role: "assistant",
1367
+ content: [
1368
+ {
1369
+ type: "output_text",
1370
+ text: result.content,
1371
+ ...(result.reasoning ? { reasoning: result.reasoning } : {}),
1372
+ },
1373
+ ],
1374
+ },
1375
+ ],
1376
+ usage: {
1377
+ input_tokens: esttokens(safestringify(messages)),
1378
+ output_tokens: Math.ceil(result.content.length / 4),
1379
+ },
1380
+ };
1381
+ await persist(cfg, {
1382
+ sessionid,
1383
+ requestid: genid("req"),
1384
+ route: "responses",
1385
+ model: resolved.display,
1386
+ status: "completed",
1387
+ startedat: Date.now(),
1388
+ });
1389
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1390
+ }
1391
+
1392
+ const upstream = cfg.upstreams[0];
1393
+ const upstreambody = buildbody(cfg, { ...body, messages, model: resolved.modelid }, resolved.model);
1394
+ const url = upstream.baseurl + (upstream.endpoints?.chat ?? "/chat/completions");
1395
+ const headers = buildheaders(cfg, null, body["stream"] === true);
1396
+ const res = await executefetch(cfg, upstream, url, headers, upstreambody, body["stream"] === true, session);
1397
+ const data = (await safejsonparse(await res.text())) as Record<string, unknown>;
1398
+ const choices = (data?.["choices"] as Array<Record<string, unknown>> | undefined) ?? [];
1399
+ const choice = choices[0]?.["message"] as Record<string, unknown> | undefined;
1400
+ const payload = {
1401
+ id: `resp_${data?.["id"] ?? genid("")}`,
1402
+ object: "response",
1403
+ created_at: data?.["created"] ?? Math.floor(Date.now() / 1000),
1404
+ model: resolved.display,
1405
+ status: "completed",
1406
+ output: [
1407
+ {
1408
+ type: "message",
1409
+ id: `msg_${genid("")}`,
1410
+ role: "assistant",
1411
+ content: [{ type: "output_text", text: String(choice?.["content"] ?? "") }],
1412
+ },
1413
+ ],
1414
+ usage: (data?.["usage"] as Record<string, unknown>) ?? {},
1415
+ };
1416
+ await persist(cfg, {
1417
+ sessionid,
1418
+ requestid: genid("req"),
1419
+ route: "responses",
1420
+ model: resolved.display,
1421
+ status: "completed",
1422
+ startedat: Date.now(),
1423
+ });
1424
+ return new Response(safestringify(payload), { headers: jsonheaders() });
1425
+ } catch (err) {
1426
+ return new Response(safestringify({ error: err instanceof Error ? err.message : "internal error" }), {
1427
+ status: 500,
1428
+ headers: jsonheaders(),
1429
+ });
1430
+ }
1431
+ }
1432
+
1433
+ // ---------------------------------------------------------------------
1434
+ // handleembeddings — embeddings route
1435
+ // ---------------------------------------------------------------------
1436
+ async function handleembeddings(req: Request): Promise<Response> {
1437
+ const cors = handlecors(req);
1438
+ if (cors) return cors;
1439
+ const paused = pausedresponse();
1440
+ if (paused) return paused;
1441
+ const routes = cfg.routes ?? {};
1442
+ if (routes.embeddings === "disabled" || cfg.transport?.type === "zai-sdk") {
1443
+ return new Response(
1444
+ safestringify({
1445
+ error: routes.embeddingsmessage ?? `${cfg.providername} has no embeddings endpoint`,
1446
+ code: "embeddings_unavailable",
1447
+ ...(routes.fallbackroute ? { fallback: routes.fallbackroute } : {}),
1448
+ }),
1449
+ { status: 501, headers: jsonheaders() },
1450
+ );
1451
+ }
1452
+ try {
1453
+ const body = ((await safejsonparse(await req.text())) ?? {}) as Record<string, unknown>;
1454
+ const upstream = cfg.upstreams.find((u) => u.endpoints?.embeddings) ?? cfg.upstreams[0];
1455
+ const url = upstream.baseurl + (upstream.endpoints?.embeddings ?? "/embeddings");
1456
+ const headers = buildheaders(cfg, null, false);
1457
+ const key = await resolveauth(cfg);
1458
+ if (key) Object.assign(headers, buildauthheaders(cfg.auth, key.key));
1459
+ const res = await fetch(url, {
1460
+ method: "POST",
1461
+ headers,
1462
+ body: safestringify({ ...body, model: body["model"] ?? cfg.defaultmodel }),
1463
+ signal: AbortSignal.timeout(cfg.timeout?.requestms ?? 300000),
1464
+ });
1465
+ const text = await res.text();
1466
+ return new Response(text, { status: res.status, headers: jsonheaders() });
1467
+ } catch (err) {
1468
+ return new Response(safestringify({ error: err instanceof Error ? err.message : "internal error" }), {
1469
+ status: 500,
1470
+ headers: jsonheaders(),
1471
+ });
1472
+ }
1473
+ }
1474
+
1475
+ return {
1476
+ handleinfo,
1477
+ handlekeys,
1478
+ handlemodels,
1479
+ handlechatcompletions,
1480
+ handlecompletions,
1481
+ handlemessages,
1482
+ handleresponses,
1483
+ handleembeddings,
1484
+ };
1485
+ }
1486
+
1487
+ // ---------------------------------------------------------------------------
1488
+ // persistence — universal savemsg wrapper
1489
+ // ---------------------------------------------------------------------------
1490
+
1491
+ /** persist a message with version metadata — never throws
1492
+ * handlers capture startedat as epoch millis; the schema column is a
1493
+ * DateTime — a raw number fails validation and the create silently
1494
+ * no-ops (the exact failure the real-consumer run caught: zero rows for
1495
+ * every request). the wrapper coerces once, at the single choke point. */
1496
+ async function persist(cfg: gatewayconfig, fields: Record<string, unknown>): Promise<void> {
1497
+ try {
1498
+ await savemsg({
1499
+ ...fields,
1500
+ ...(fields["startedat"] !== undefined ? { startedat: new Date(Number(fields["startedat"])) } : {}),
1501
+ provider: cfg.providername,
1502
+ version: cfg.id,
1503
+ endpoint: cfg.upstreams[0]?.name ?? cfg.providername,
1504
+ });
1505
+ } catch {
1506
+ // never throw on db errors
1507
+ }
1508
+ }
1509
+
1510
+ /** export internals for testing and advanced usage */
1511
+ export const engineinternals = {
1512
+ resolvemodel,
1513
+ findmodel,
1514
+ fusioncontext,
1515
+ buildbody,
1516
+ classifystatus,
1517
+ backoffms,
1518
+ reconstructhistory,
1519
+ fitcontext,
1520
+ defaultbudgets,
1521
+ defaultretrystatuses,
1522
+ defaultrotatestatuses,
1523
+ defaultfallbackstatuses,
1524
+ };