@wenathlan/getry 2.0.165 → 2.0.167

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