@wenathlan/devthink 2.0.15 → 2.0.19

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 +45 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/engine.ts DELETED
@@ -1,1524 +0,0 @@
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
- };