@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.
- package/CHANGELOG.md +45 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /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
|
-
};
|