@wenathlan/getry 2.0.163 → 2.0.165

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/cli.ts CHANGED
@@ -44,7 +44,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
44
44
  import { basename, join, relative, resolve } from "node:path";
45
45
  import { stdin, stdout } from "node:process";
46
46
  import { createInterface, type Interface } from "node:readline/promises";
47
- import { main } from "./server.js";
48
47
 
49
48
  // ---------------------------------------------------------------------------
50
49
  // arg parsing — zero dependency
@@ -795,6 +794,13 @@ async function cmdserve(portarg: string | undefined): Promise<void> {
795
794
  const port = Number(portarg) || Number(process.env.PORT) || 3001;
796
795
  process.env.PORT = String(port);
797
796
  console.log(`${colors.cyan}starting gateway on port ${port}${colors.reset}`);
797
+ /* the transport main loads lazily (the published-envelope law): the
798
+ * console family — init through export, chat, help — runs on node
799
+ * builtins alone inside the published envelope, and the serve seat
800
+ * imports the engine transport only when the caller actually serves,
801
+ * so a documentation envelope never crashes on a module it does not
802
+ * ship before a command ever dispatches. */
803
+ const { main } = await import("./server.js");
798
804
  await main();
799
805
  }
800
806
 
@@ -820,6 +826,93 @@ async function cmdexport(dirarg: string | undefined): Promise<void> {
820
826
  console.log(`\nexported to ${target} — deploy this folder to vercel or netlify`);
821
827
  }
822
828
 
829
+ // ---------------------------------------------------------------------------
830
+ // chat — the windows 11 console seat of the clibin family (pr13: same
831
+ // design as the web / desktop surface, the difference is only the
832
+ // responsiveness / dpi — native detection, 1x / 1.5x / 2x / 3x, never xdpi)
833
+ // ---------------------------------------------------------------------------
834
+
835
+ /** the chat accent of the prompt and the titlebar name (ansi 117 — the family accent #a3d7e6). */
836
+ const CHATACCENT = "\x1b[38;5;117m" as const;
837
+ /** the focus ink of the active top rule (ansi 75 — the family focus #4cc2ff). */
838
+ const CHATFOCUS = "\x1b[38;5;75m" as const;
839
+ /** the hover red of the unknown-command rows (ansi 203 — rgb(232, 17, 35)). */
840
+ const CHATHOVER = "\x1b[38;5;203m" as const;
841
+ /** the dim ink of the description rows (the minimal text law). */
842
+ const CHATINK = "\x1b[2m" as const;
843
+ /** the reset token that closes every colored run. */
844
+ const CHATRESET = "\x1b[0m" as const;
845
+ /** the single glyph of the rules and the rails. */
846
+ const CHATBAR = "─" as const;
847
+ /** the engine ownership one-liner of the seat, quoted from the help banner. */
848
+ const CHATENGINE = "the universal ai gateway library — any llm any baseurl any api key (the help banner)" as const;
849
+ /** the scale ladder the native detection resolves (the automatic scaling law). */
850
+ const CHATSCALESTEPS = [1, 1.5, 2, 3] as const;
851
+
852
+ /** the dpi frame the chat chrome renders inside: the resolved scale seat, the terminal columns and the rail length the titlebar draws. */
853
+ interface chatframe {
854
+ /** the scale seat of the ladder (1, 1.5, 2 or 3). */
855
+ scale: number;
856
+ /** the columns the terminal answers (windows terminal, conpty, xterm, tmux — every pty reports them). */
857
+ columns: number;
858
+ /** the titlebar rail length in cells (the pixel rail folded to the grid and clamped so the line never wraps). */
859
+ rail: number;
860
+ }
861
+
862
+ /** resolves the dpi frame natively, zero dependencies: the columns come from stdout (every windows and linux terminal answers them), the scale seat from the column ladder — the explicit GETRY_DPI environment override wins first (the native scaling knob). */
863
+ function chatdetectframe(): chatframe {
864
+ const columns = Number.isFinite(stdout.columns) && Number(stdout.columns) > 0 ? Number(stdout.columns) : 80;
865
+ const override = Number(process.env.GETRY_DPI ?? "");
866
+ const scale =
867
+ Number.isFinite(override) && override > 0 && CHATSCALESTEPS.includes(override as (typeof CHATSCALESTEPS)[number])
868
+ ? override
869
+ : columns >= 200
870
+ ? 3
871
+ : columns >= 160
872
+ ? 2
873
+ : columns >= 120
874
+ ? 1.5
875
+ : 1;
876
+ const rail = Math.max(4, Math.min(Math.round(24 * scale), columns - "getry".length - 16));
877
+ return { scale, columns, rail };
878
+ }
879
+
880
+ /** runs the chat console: the titlebar, the top rule, the ❯ prompt and the dispatch (help, version, dpi, engine, exit) — the same chat look the web / desktop surfaces carry, an eof on stdin closing the console the same way exit does. */
881
+ async function cmdchat(): Promise<void> {
882
+ const frame = chatdetectframe();
883
+ const seatversion = version();
884
+ const rl = createInterface({ input: stdin, output: stdout });
885
+ let eof = false;
886
+ rl.on("close", () => { eof = true; });
887
+ const rule = CHATBAR.repeat(Math.max(3, Math.round(3 * frame.scale)));
888
+ console.log(`${CHATACCENT}┌ getry ─ cli${CHATRESET} ${CHATINK}${CHATBAR.repeat(frame.rail)} ─ ❐ ✕${CHATRESET}`);
889
+ console.log(`${CHATFOCUS}${rule} getry ${rule}${CHATRESET}`);
890
+ console.log(`${CHATACCENT}getry${CHATRESET} ${CHATINK}v${seatversion}${CHATRESET}`);
891
+ console.log(`${CHATINK}the getry console — same design as web / desktop, only the DPI differs. type help.${CHATRESET}`);
892
+ rl.setPrompt(`${CHATACCENT}❯${CHATRESET} `);
893
+ rl.prompt();
894
+ for await (const raw of rl) {
895
+ const line = raw.trim();
896
+ const command = line.length > 0 ? line.split(/\s+/)[0]!.toLowerCase() : "";
897
+ if (command === "exit") rl.close();
898
+ else if (command === "help") {
899
+ console.log(`${CHATINK}commands${CHATRESET}`);
900
+ console.log(` help ${CHATINK}this list${CHATRESET}`);
901
+ console.log(` version ${CHATINK}the package version, read from package.json at runtime${CHATRESET}`);
902
+ console.log(` dpi ${CHATINK}the resolved dpi seat of this terminal (scale, columns, rail)${CHATRESET}`);
903
+ console.log(` engine ${CHATINK}the engine ownership one-liner of the app${CHATRESET}`);
904
+ console.log(` exit ${CHATINK}close the console${CHATRESET}`);
905
+ } else if (command === "version") console.log(`${CHATACCENT}getry${CHATRESET} ${seatversion}`);
906
+ else if (command === "dpi") console.log(`${CHATACCENT}getry${CHATRESET} ${CHATINK}dpi seat ${frame.scale}x on ${frame.columns} columns — rail ${frame.rail} cells, ladder 1x / 1.5x / 2x / 3x${CHATRESET}`);
907
+ else if (command === "engine") console.log(`${CHATINK}${CHATENGINE}${CHATRESET}`);
908
+ else if (command.length > 0) console.log(`${CHATHOVER}✕${CHATRESET} ${CHATINK}unknown command: ${command} — type help${CHATRESET}`);
909
+ if (command === "exit") break;
910
+ /* the prompt is cosmetic chrome: a piped stdin that already reached eof skips it, the buffered lines still answer. */
911
+ if (!eof) rl.prompt();
912
+ }
913
+ console.log(`${CHATINK}getry console closed.${CHATRESET}`);
914
+ }
915
+
823
916
  // ---------------------------------------------------------------------------
824
917
  // help
825
918
  // ---------------------------------------------------------------------------
@@ -843,6 +936,7 @@ function printhelp(): void {
843
936
  " models <version> list models for a version",
844
937
  " serve [--port <n>] start the gateway server",
845
938
  " export [--dir <path>] export the web folder scaffold",
939
+ " chat the windows 11 chat console seat (same design as web / desktop)",
846
940
  " help show this help",
847
941
  "",
848
942
  "Library usage:",
@@ -910,6 +1004,9 @@ async function consolemain(): Promise<void> {
910
1004
  case "export":
911
1005
  await cmdexport(stringflag(parsed, "dir"));
912
1006
  break;
1007
+ case "chat":
1008
+ await cmdchat();
1009
+ break;
913
1010
  case "help":
914
1011
  case "--help":
915
1012
  case "-h":
@@ -0,0 +1,19 @@
1
+ # Distribution lanes
2
+
3
+ The eleven distribution lanes of the PR14 law and the honest status each lane answers for getry at the 2.0.163 family rung. A lane never ships metadata its real surface cannot back: a lane without a real carrier stays a documented stub until the surface lands, and no version ever bumps to fake a lane.
4
+
5
+ | Lane | Carrier | Status | Note |
6
+ | --- | --- | --- | --- |
7
+ | npmjs | package.json — @wenathlan/getry | live | the family-npmjs lane publishes the envelope every release rung |
8
+ | GitHub Packages npm | package.json — @wenathlan/getry | live | the family-githubnpm lane publishes the same envelope idempotently |
9
+ | Maven | pom.xml — io.github.wenathlan:getry | live | the family-maven lane deploys the jar |
10
+ | NuGet | getry.csproj | live | the family-nuget lane packs the envelope |
11
+ | Gradle | build.gradle.kts — com.wenathlan:getry | live | the family-gradle lane publishes the module |
12
+ | Ruby Gems | getry.gemspec | live | the family-rubygems lane pushes the gem |
13
+ | Docker Container | Dockerfile | live | the family-ghcr lane pushes ghcr.io/wenathlan/getry (the vault, saddle, devthink and debonair siblings its Dockerfile COPYs ride the staged context) and the family-container lane attaches the release archive |
14
+ | Go Get | go.mod | stub | no Go source surface ships yet — the lane lands when a real Go adapter exists, never as an empty module |
15
+ | Cargo Crates | Cargo.toml | stub | no Rust source surface ships yet — the lane lands with a real crate, never as an empty manifest |
16
+ | PyPi | pyproject.toml | stub | no Python package surface ships yet — the lane lands with a real wheel, never as an empty project table |
17
+ | Homebrew | Formula | stub | no tap repository ships yet — the formula lands pointing at the real release binaries with real checksums, never at a placeholder |
18
+
19
+ The engines block of package.json keeps Go, Rust and Python as declared toolchain floors — a catalog row is not a distribution lane. The dual npm law answers through the npmjs and GitHub Packages rows; the release binaries (the desktop bundles, the mobile artifacts, the container archive) ride the family-desktop, family-mobile and family-container lanes every release rung.
package/docs/gateway.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  ## module inventory
11
11
 
12
- - `gateway.ts` — the gateway versions domain: `GATEWAYROUTES` (the 7 route kinds), `GATEWAYVERSIONS` (the v1–v5 catalog), `PARSERSTEPS` (the streaming parser pattern), `listversions`, `findversion`, `totalroutes`, `liveversions`.
12
+ - `gateway.ts` — the gateway versions domain: `GATEWAYROUTES` (the 7 route kinds), `GATEWAYVERSIONS` (the v1–v5 catalog), `PARSERSTEPS` (the streaming parser pattern), `listversions`, `findversion`, `totalroutes`, `liveversions`, and `thinkingchain` (the level-routed cross-provider chain the thinking seat computes over this catalog).
13
13
  - `gatewayconfig.ts` — the embedded single-version gateway definition (v1 zai sdk, keyless).
14
14
  - `gatewayviewconfig.ts` — the shipped multi-version catalog (v1–v5) the console page renders; the config loader probes `sol/config.mjs|ts|js` for user overrides.
15
15
  - `engine.ts` — the universal gateway engine: `createversion(cfg)` produces all route handlers from one `gatewayconfig`.
@@ -20,13 +20,14 @@
20
20
  - `llm.ts` — the model call core: request building, provider calls, stream delta parsing, command guard, plan drafting, replan on fail, step reflection, usage accounting.
21
21
  - `streaming.ts` — the SSE parser and stream normalizer: split-tolerant parsing, payload normalization, thinking signature cache, OpenAI/Anthropic transformers, auto-recovery.
22
22
  - `streamfallback.ts` — the upstream error catalog and the fallback runner that walks the route chain when one model dies.
23
- - `routerotation.ts` — weighted pool rotation with cooldown ledger (litellm-style knobs).
23
+ - `routerotation.ts` — the model pool rotation math: weighted draws, health-aware round-robin under a persisted cursor (`pickroundrobin`, `rotatepool`) and the cooldown ledger (litellm-style knobs).
24
24
  - `sessions.ts` — the per-(sessionId, provider) session context store and rotation index.
25
+ - `thinking.ts` — the thinking seat (`@wenathlan/getry/thinking`): the registerable 7-level reasoning ladder plus the routing the rungs drive — tier routing (`targettier`), the budget clamp (`clampbudget`), the two-calls verdict (`twocallspattern`, `TWOCALLSTHRESHOLD`), the fallback chain (`fallbackchain`, `modelcapable`), the session/stream lanes (`lanesof`) and the provider catalog chain (`providersserving`, `providerchain`) — pure dependency-free math over structural rows.
25
26
  - `quota.ts` — dual-source (Antigravity + Gemini CLI) quota manager with adaptive TTL cache.
26
27
  - `models.ts` — the merged model catalog (frozen 25/08/2026): model definitions, classifiers, thinking tables, endpoint routing.
27
28
  - `intelligence.ts` — provenance, token estimation, markdown chunking, rag manifest, llms.txt, metric store.
28
29
  - `oauth.ts` / `accounts.ts` / `recovery.ts` / `project.ts` — the provider oauth family, multi-account manager, recovery and project id discovery.
29
- - `db.ts` — the database contract: typed rows, in-memory seeds, HTTPS accessors, better-sqlite3 mirror.
30
+ - `db.ts` — the database contract: typed rows, in-memory seeds, HTTPS accessors, the health-aware round-robin key picker (`nextkeyrow`) and the better-sqlite3 mirror (onboarding states, labels and the `gateway_versions` route table).
30
31
  - `prismaconfig.ts` / `prismashim.ts` / `nodeshim.ts` — prisma schema path resolution and runtime shims.
31
32
  - `gatewayprobe.ts` — manual chat round trip against a deployed gateway (`GET /v1/models`, `POST /v1/chat/completions`).
32
33
  - `gatewaysmoke.ts` — offline container smoke: one full request cycle against the recorded openaicompat fixture.
@@ -69,8 +70,14 @@ the 7-level thinking ladder (`thinking.ts`), each level bound to a token budget:
69
70
  - `xhigh` — budget 68000, extra deep.
70
71
  - `max` — budget 68000, maximum.
71
72
 
72
- - helpers: `listlevels`, `budgetof`, `islevel`, `formatbudget`.
73
- - the engine maps the budget onto the provider param (`chattemplatekwargs` for v3, `thinking` for v1) and runs the two-calls pattern when the thinking budget plus the response cap crosses the threshold (default 98304).
73
+ - helpers: `listlevels`, `budgetof`, `islevel`, `formatbudget`; the registration surface (`registerthinkinglevels`, `GETRY_THINKING_LEVELS`) replaces the whole ladder with any count of rungs.
74
+ - the engine maps the budget onto the provider param (`chattemplatekwargs` for v3, `thinking` for v1) and takes the two-calls verdict from the seat (`twocallspattern`): the pattern fires when the thinking budget plus the response cap crosses the threshold (`TWOCALLSTHRESHOLD`, default 98304) or whenever the level thinks at all.
75
+ - the routing the level drives (all pure math in `thinking.ts`):
76
+ - tier routing — `targettier` maps the rung POSITION (never the name) to the model tier: none/minimal → 4 (economy), low/medium → 3 (mid), high → 2 (strong), xhigh/max → 1 (flagship); registered ladders route by the same fraction.
77
+ - token budgets — `clampbudget` clamps the level budget to the model's output ceiling.
78
+ - fallback chains — `fallbackchain` orders a catalog by tier distance from the target tier, then full-budget fit, then leaderboard score, then id; `modelcapable` never serves a reasoning-false model a thinking level.
79
+ - lanes — `lanesof` resolves the bind order: the session lane (per-session rotation cursor and budget) before the stream lane (reasoning deltas before content).
80
+ - provider catalog — `providersserving`/`providerchain` build the cross-provider walk (live providers first, paused providers last); `gateway.ts` wires the real v1–v5 rows through `thinkingchain`.
74
81
 
75
82
  ## auth and key rotation
76
83
 
@@ -78,7 +85,8 @@ the 7-level thinking ladder (`thinking.ts`), each level bound to a token budget:
78
85
  - key sources resolved in configured order, first hit wins: `db` (prisma model lookup), `env` (single or separator-joined), `inline` (config provided), `ephemeralurl` (babel town pattern).
79
86
  - key rotation modes when multiple keys exist: `single`, `roundrobin`, `lru`.
80
87
  - v3 carries the key pool: 22 nvidia keys registered into the `nvidiaKey` table, round-robin per request.
81
- - pool-level rotation (`routerotation.ts`): weight-based picks with a cooldown ledger — 3 allowed fails, 5s cooldown, 5s escalation step, 60s ceiling (litellm router defaults lifted as pure math).
88
+ - pool-level rotation (`routerotation.ts`): weight-based picks, health-aware round-robin under a persisted cursor and the cooldown ledger — 3 allowed fails, 5s cooldown, 5s escalation step, 60s ceiling (litellm router defaults lifted as pure math).
89
+ - key-row rotation (`db.ts` `nextkeyrow`): health-aware round-robin over the `ApiKeyRow` pool — active, unexpired keys only; never rate-limited, never erroring seats lead; an erroring key rides only when nothing healthy remains; selection answers the row, rendering masks (`maskkey`).
82
90
  - session-level rotation (`sessions.ts`): the devthink meta model rotates every `ROTATIONEVERY = 6` turns within a session.
83
91
  - errors from upstream land on the key counters (`applyerrortokey` on `ApiKeyRow`) and feed the fallback chain.
84
92
 
@@ -93,14 +101,15 @@ the 7-level thinking ladder (`thinking.ts`), each level bound to a token budget:
93
101
 
94
102
  - `schema.prisma` — sqlite datasource (prisma client with driver adapters; libsql http/https or file url for serverless deploy via `DATABASE_URL`).
95
103
  - six models: `ChatMessage` (every interaction log across v1–v5), `NvidiaKey`, `TownKey`, `ApiKey` (the key pools), `SessionContext` (per-session rotation and thinking state), `OnboardingState` (first-run flags).
96
- - `db.ts` — typed rows plus in-memory seeds (`KEYSEED`, `MESSAGESEED`, `SESSIONSEED`) the static build falls back to; the self-hosted deploy answers the same rows over HTTPS; a better-sqlite3 mirror at the foot of the file runs the local gateway runtime (prepared statements only, idempotent schema, dynamic import the browser never bundles).
104
+ - `db.ts` — typed rows plus in-memory seeds (`KEYSEED`, `MESSAGESEED`, `SESSIONSEED`, the `GATEWAYVERSIONS` family seed) the static build falls back to; the self-hosted deploy answers the same rows over HTTPS; a better-sqlite3 mirror at the foot of the file runs the local gateway runtime (prepared statements only, idempotent schema, dynamic import the browser never bundles).
105
+ - the route table is data-driven: the `gateway_versions` mirror table holds the instance rows (seed-on-first-run from the `GATEWAYVERSIONS` family seed, registration-safe upsert through `savegatewayversion`), the internal api (`readgatewayversionrows`, `listversionrows`) serves them, and the versions/home wingets read the rows — never a hardcoded TSX table.
97
106
  - family sync contract: identity pointers are text, counts are 32-bit ints, time is datetime or iso text.
98
107
 
99
108
  ## sol surface
100
109
 
101
110
  - `sol/Sol.tsx` mounts the route table: `/intro`, `/onboarding`, `/`, `/versions`, `/thinking`, `/sessions`, `/404`.
102
111
  - subapps: `home`, `intro`, `notfound`, `onboarding`, `sessions`, `shell`, `thinking`, `toast`, `versions`, `wingets` (HomeEntrygate, ShellBrandmark, ShellEmptystate).
103
- - `versions` — the gateway registry: five replica rows (version tag, provider chip, live dot, the 7 route chips), the parser pattern rail, quick-start calls. every row reads `gateway.ts`.
112
+ - `versions` — the gateway registry: five replica rows (version tag, provider chip, live dot, the 7 route chips), the parser pattern rail, quick-start calls. every row reads the db route table lane (`listversionrows`: the sqlite mirror of the gateway runtime, the family seed of `gateway.ts` under the static build).
104
113
  - `thinking` — the reasoning ladder: 7 rungs with hairline budget bars against the 68k ceiling, the default rung marked. every number reads `thinking.ts`.
105
114
  - `sessions` — the session registry: session contexts as the ledger table, rotation summary and masked provider keys as the meta rail, the recent chat log below. rows arrive over HTTPS from the self-hosted database with seed fallback.
106
115
 
@@ -113,4 +122,4 @@ the 7-level thinking ladder (`thinking.ts`), each level bound to a token budget:
113
122
 
114
123
  ## tests
115
124
 
116
- - 27 test files under `tests/` cover the gateway surface: gateway, server (auth, engine, property, utils), llm, modelroute, promptlibrary, routerotation, streamfallback, intelligence, jsonpolicy, family, design, cli, providercli, onboardingstate, sandboxmodality, debonairlane, torture.llm, plus the provider lineage (constants, fingerprint, models, oauth) and the shared fixtures (`gatewayserver.ts`, `providerfixtures.ts`, `researchfixtures.ts`).
125
+ - 29 test files under `tests/` cover the gateway surface: gateway, gatewayversions (the 5/35 route table law, the sqlite mirror rows, the thinking chain), server (auth, engine, property, utils), llm, modelroute, promptlibrary, keyrotation (the health-aware key round-robin and the masking discipline), routerotation, thinkingladder (the registerable ladder and the tier/budget/chain/lane routing), streamfallback, intelligence, jsonpolicy, family, design, cli, providercli, onboardingstate, sandboxmodality, debonairlane, torture.llm, plus the provider lineage (constants, fingerprint, models, oauth) and the shared fixtures (`gatewayserver.ts`, `providerfixtures.ts`, `researchfixtures.ts`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/getry",
3
- "version": "2.0.163",
3
+ "version": "2.0.165",
4
4
  "description": "getry is the AI gateway of the wenathlan family: five provider gateways (v1 zai, v2 babel, v3 nvidia, v4 opencode+kilo, v5 openrouter) serving 35 OpenAI-compatible routes with the 7-level thinking system, the session store and the key rotation, described by schema.prisma and served as one static Sol surface. The envelope distributes the documentation set and the tests directory as one package.",
5
5
  "keywords": [
6
6
  "getry",
@@ -30,6 +30,11 @@
30
30
  "types": "./thinking.ts",
31
31
  "default": "./thinking.ts"
32
32
  },
33
+ "./validate": {
34
+ "types": "./validate.ts",
35
+ "import": "./validate.ts",
36
+ "default": "./validate.ts"
37
+ },
33
38
  "./engine": {
34
39
  "types": "./engine.ts",
35
40
  "import": "./engine.ts",
@@ -194,9 +199,11 @@
194
199
  "files": [
195
200
  "README.md",
196
201
  "docs",
197
- "tests"
202
+ "tests",
203
+ "cli.ts"
198
204
  ],
199
205
  "engines": {
206
+ "babel": "^7.28.0",
200
207
  "bare": "^1.18.0",
201
208
  "bun": "^1.4.4",
202
209
  "cargo": ">=1.85.0",
@@ -219,6 +226,7 @@
219
226
  "typescript": "^5.8.0",
220
227
  "v": "^0.5.2",
221
228
  "vscode": "^1.118.0",
229
+ "yaml": "^2.9.1",
222
230
  "yarn": "^4.17.1"
223
231
  },
224
232
  "peerDependencies": {
@@ -246,12 +254,12 @@
246
254
  "@libsql/client": "^0.18.0",
247
255
  "@prisma/adapter-libsql": "^7.10.1",
248
256
  "@prisma/client": "^7.10.1",
257
+ "@wenathlan/argan": "~2.0.155",
249
258
  "@wenathlan/cadria": "~2.0.155",
250
259
  "@wenathlan/debonair": "~2.0.155",
251
260
  "@wenathlan/devthink": "~2.0.154",
252
261
  "@wenathlan/getry": "~2.0.155",
253
262
  "@wenathlan/saddle": "~2.0.155",
254
- "@wenathlan/argan": "~2.0.155",
255
263
  "@wenathlan/vault": "~2.0.155",
256
264
  "better-sqlite3": "^13.0.3",
257
265
  "drizzle-orm": "^0.45.4",
@@ -364,22 +372,26 @@
364
372
  "zod": "^4.6.7"
365
373
  },
366
374
  "overrides": {
367
- "esbuild": "^0.28.2",
375
+ "uuid": "^11.1.1",
368
376
  "deepmerge-ts": "^8.0.0",
377
+ "esbuild": "^0.28.2",
378
+ "follow-redirects": "^1.16.2",
379
+ "minimatch": "^11.0.3",
380
+ "glob": "^14.0.3",
369
381
  "mysql2": "^3.24.6",
370
- "uuid": "^11.1.1",
371
382
  "postcss-selector-parser": "^7.1.6",
372
383
  "source-map-js": "1.2.2",
373
- "katex": "^0.18.2",
374
384
  "proxy-addr": "2.0.8",
375
385
  "body-parser": "^2.4.0",
376
386
  "fast-uri": "^3.1.9",
377
- "follow-redirects": "^1.16.2",
378
- "minimatch": "^11.0.3",
379
- "glob": "^14.0.3",
380
387
  "zod": "^4.6.7",
381
388
  "undici": "^7.30.1",
382
- "shell-quote": "^1.11.0"
389
+ "katex": "^0.18.2",
390
+ "shell-quote": "^1.11.0",
391
+ "xcode": {
392
+ "uuid": "11.1.1"
393
+ },
394
+ "@grpc/grpc-js": "1.14.5"
383
395
  },
384
396
  "publishConfig": {
385
397
  "access": "public",
@@ -0,0 +1,145 @@
1
+ /**
2
+ * gatewayversions.test.ts — the data-driven route table of the gateway: the
3
+ * family seed counts (5 providers, 7 route kinds, 35 routes), the sqlite
4
+ * mirror rows the node runtime serves (seed-on-first-run, registration-safe
5
+ * upsert) and the thinking chain the gateway wires over the catalog rows
6
+ * through the thinking seat. the suite pins its own DATABASE_URL before the
7
+ * db import so the mirror opens its own seat (one writer per wal file — the
8
+ * SQLITE_BUSY lesson). runnable with vitest:
9
+ * pnpm vitest run tests/gatewayversions.test.ts
10
+ */
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { afterAll, describe, it } from "vitest";
16
+
17
+ /* the db seat isolation law: DATABASE_URL is read at the db module eval, so
18
+ * the seat lands before the dynamic import below opens the mirror. */
19
+ process.env.DATABASE_URL = `file:${join(mkdtempSync(join(tmpdir(), "gateway-versions-")), "route-table.test.db")}`;
20
+
21
+ const db = await import("../db.ts");
22
+ const { GATEWAYROUTES, GATEWAYVERSIONS, thinkingchain, totalroutes } = await import("../gateway.ts");
23
+
24
+ afterAll(() => {
25
+ rmSync(join(process.env.DATABASE_URL!.slice("file:".length), ".."), { recursive: true, force: true });
26
+ });
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // the family seed counts — the pr19 by-example law: 5 providers, 35 routes
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("the family seed route table (5 providers, 35 routes)", () => {
33
+ it("carries the five provider gateways", () => {
34
+ assert.equal(GATEWAYVERSIONS.length, 5);
35
+ assert.deepEqual(
36
+ GATEWAYVERSIONS.map((version) => version.provider),
37
+ ["zai", "babel town", "nvidia nim", "opencode zen plus kilo", "openrouter"],
38
+ );
39
+ });
40
+
41
+ it("serves the seven OpenAI-compatible route kinds per gateway", () => {
42
+ assert.equal(GATEWAYROUTES.length, 7);
43
+ for (const version of GATEWAYVERSIONS) {
44
+ assert.deepEqual(version.routes, [...GATEWAYROUTES], `${version.v} carries the seven route kinds`);
45
+ }
46
+ });
47
+
48
+ it("totals the thirty-five routes the gateway serves", () => {
49
+ assert.equal(totalroutes(), 35);
50
+ });
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // the sqlite mirror rows — instance rows live in the database
55
+ // ---------------------------------------------------------------------------
56
+
57
+ describe("the gateway_versions mirror (the data-driven route table)", () => {
58
+ it("seeds the five family rows on first use and answers them in version order", async () => {
59
+ const rows = await db.readgatewayversionrows();
60
+ assert.equal(rows.length, 5);
61
+ assert.deepEqual(
62
+ rows.map((row) => row.v),
63
+ ["v1", "v2", "v3", "v4", "v5"],
64
+ );
65
+ assert.equal(rows.reduce((sum, row) => sum + row.routes.length, 0), 35);
66
+ // the routes ride the json cell and parse back to the route kinds
67
+ assert.deepEqual(rows[0].routes, [...GATEWAYROUTES]);
68
+ });
69
+
70
+ it("keeps the seeding idempotent — a second pass lands nothing", async () => {
71
+ const seeded = await db.seedGatewayVersionRows();
72
+ assert.equal(seeded, 0);
73
+ assert.equal((await db.readgatewayversionrows()).length, 5);
74
+ });
75
+
76
+ it("writes and answers a registered row a later seed never overwrites", async () => {
77
+ const written = await db.savegatewayversion({
78
+ v: "v6",
79
+ provider: "a person registered gateway",
80
+ pattern: "registered through the internal api",
81
+ badge: "live",
82
+ models: "registered-model",
83
+ note: "the route table answers the registration, never a hardcoded row",
84
+ routes: ["chat/completions", "models"],
85
+ });
86
+ assert.equal(written?.provider, "a person registered gateway");
87
+ const rows = await db.readgatewayversionrows();
88
+ assert.equal(rows.length, 6);
89
+ const registered = rows.find((row) => row.v === "v6");
90
+ assert.deepEqual(registered?.routes, ["chat/completions", "models"]);
91
+ // the seed re-run respects the registration: v6 stays, nothing resets
92
+ assert.equal(await db.seedGatewayVersionRows(), 0);
93
+ assert.equal((await db.readgatewayversionrows()).find((row) => row.v === "v6")?.provider, "a person registered gateway");
94
+ });
95
+
96
+ it("falls back to the canonical route kinds when a routes cell arrives broken", async () => {
97
+ await db.savegatewayversion({
98
+ v: "v7",
99
+ provider: "broken cell gateway",
100
+ pattern: "a bad write never loses the routes",
101
+ badge: "paused",
102
+ models: "any",
103
+ note: "the projection keeps the family route kinds",
104
+ routes: [],
105
+ });
106
+ const rows = await db.readgatewayversionrows();
107
+ const broken = rows.find((row) => row.v === "v7");
108
+ assert.deepEqual(broken?.routes, [...GATEWAYROUTES]);
109
+ assert.equal(broken?.badge, "paused");
110
+ });
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // the thinking chain over the catalog — the level routes the provider rows
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe("thinkingchain — the level-routed cross-provider chain over the catalog", () => {
118
+ it("orders the live providers first and the paused provider last", () => {
119
+ const chain = thinkingchain("max");
120
+ const order: string[] = [];
121
+ for (const candidate of chain) {
122
+ if (order[order.length - 1] !== candidate.provider) order.push(candidate.provider);
123
+ }
124
+ assert.deepEqual(order, ["zai", "nvidia nim", "opencode zen plus kilo", "openrouter", "babel town"]);
125
+ });
126
+
127
+ it("leads with the flagship tier for the maximum level and never serves a reasoning-false model", () => {
128
+ const chain = thinkingchain("max");
129
+ assert.equal(chain[0].provider, "zai");
130
+ assert.equal(chain[0].model.id, "glm-5.3");
131
+ for (const candidate of chain) {
132
+ assert.notEqual(candidate.model.reasoning, false, `${candidate.provider}/${candidate.model.id} never carries a thinking level`);
133
+ }
134
+ // the first nvidia candidate is the tier-1 rotation backend the v3 lane rides
135
+ const firstnvidia = chain.find((candidate) => candidate.provider === "nvidia nim");
136
+ assert.equal(firstnvidia?.model.id, "moonshotai/kimi-k3");
137
+ });
138
+
139
+ it("walks the strong tier first for the default high level", () => {
140
+ const chain = thinkingchain("high");
141
+ assert.equal(chain[0].model.id, "glm-5.1");
142
+ const ids = chain.slice(0, 4).map((candidate) => candidate.model.id);
143
+ assert.deepEqual(ids, ["glm-5.1", "glm-5v", "glm-5.3", "glm-5.3-flash"]);
144
+ });
145
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * keyrotation.test.ts — the provider key rotation over the ApiKeyRow pool:
3
+ * the health-aware round-robin of nextkeyrow (the pr19 key rotation law —
4
+ * never expose keys, rotation periods, per-provider health) beside the
5
+ * masking discipline the render paths owe. runnable with vitest:
6
+ * pnpm vitest run tests/keyrotation.test.ts
7
+ */
8
+ import assert from "node:assert/strict";
9
+ import { describe, it } from "vitest";
10
+ import { type ApiKeyRow, maskkey, nextkeyrow, rotationsummary } from "../db.ts";
11
+
12
+ /** the reference clock of the fixtures (a fixed epoch, no wall time). */
13
+ const NOW = 1_800_000_000_000;
14
+
15
+ /** Builds one key row fixture with every value the rotation reads. */
16
+ function key(over: Partial<ApiKeyRow> & { id: string }): ApiKeyRow {
17
+ return {
18
+ provider: "nvidia",
19
+ key: `nvapi-fixed-${over.id.padStart(4, "0")}-credential-shape`,
20
+ label: `nim rotation ${over.id}`,
21
+ active: true,
22
+ status: "active",
23
+ useCount: 0,
24
+ rotationCount: 0,
25
+ errorCount: 0,
26
+ rateLimitHit: false,
27
+ createdAt: "2026-09-12T10:00:00.000Z",
28
+ expiresAt: null,
29
+ ...over,
30
+ };
31
+ }
32
+
33
+ describe("nextkeyrow — the health-aware round-robin of the key pool", () => {
34
+ it("walks the pool in stable order and wraps the cursor", () => {
35
+ const pool = [key({ id: "k1" }), key({ id: "k2" }), key({ id: "k3" })];
36
+ const walk: string[] = [];
37
+ let cursor = 0;
38
+ for (let step = 0; step < 5; step++) {
39
+ const pick = nextkeyrow(pool, cursor, NOW);
40
+ assert.ok(pick, "an eligible pool always answers");
41
+ walk.push(pick.row.id);
42
+ cursor = pick.cursor;
43
+ }
44
+ assert.deepEqual(walk, ["k1", "k2", "k3", "k1", "k2"]);
45
+ });
46
+
47
+ it("never answers a revoked, inactive or expired key", () => {
48
+ const pool = [
49
+ key({ id: "revoked", status: "revoked" }),
50
+ key({ id: "inactive", active: false }),
51
+ key({ id: "expired", expiresAt: "2026-01-01T00:00:00.000Z" }),
52
+ key({ id: "live" }),
53
+ ];
54
+ for (let cursor = 0; cursor < 4; cursor++) {
55
+ const pick = nextkeyrow(pool, cursor, NOW);
56
+ assert.ok(pick);
57
+ assert.equal(pick.row.id, "live");
58
+ }
59
+ assert.equal(nextkeyrow(pool.slice(0, 3), 0, NOW), null);
60
+ });
61
+
62
+ it("keeps an unparseable expiry honest — only a real parse retires a key", () => {
63
+ const pool = [key({ id: "odd", expiresAt: "not-a-date" }), key({ id: "live" })];
64
+ const walk: string[] = [];
65
+ let cursor = 0;
66
+ for (let step = 0; step < 4; step++) {
67
+ const pick = nextkeyrow(pool, cursor, NOW);
68
+ assert.ok(pick);
69
+ walk.push(pick.row.id);
70
+ cursor = pick.cursor;
71
+ }
72
+ assert.deepEqual(walk, ["odd", "live", "odd", "live"]);
73
+ });
74
+
75
+ it("leads with the healthy seats — an erroring or rate-limited key rides only when nothing healthy remains", () => {
76
+ const pool = [
77
+ key({ id: "healthy-a" }),
78
+ key({ id: "erroring", errorCount: 3 }),
79
+ key({ id: "ratelimited", rateLimitHit: true }),
80
+ key({ id: "healthy-b" }),
81
+ ];
82
+ const walk: string[] = [];
83
+ let cursor = 0;
84
+ for (let step = 0; step < 4; step++) {
85
+ const pick = nextkeyrow(pool, cursor, NOW);
86
+ assert.ok(pick);
87
+ walk.push(pick.row.id);
88
+ cursor = pick.cursor;
89
+ }
90
+ assert.deepEqual(walk, ["healthy-a", "healthy-b", "healthy-a", "healthy-b"]);
91
+ // every healthy seat out of the pool: the erroring seats take the walk
92
+ const degraded = [key({ id: "erroring", errorCount: 3 }), key({ id: "ratelimited", rateLimitHit: true })];
93
+ const fallback = nextkeyrow(degraded, 0, NOW);
94
+ assert.ok(fallback);
95
+ assert.equal(fallback.row.id, "erroring");
96
+ assert.equal(fallback.cursor, 1);
97
+ });
98
+
99
+ it("normalizes any cursor the caller persisted", () => {
100
+ const pool = [key({ id: "k1" }), key({ id: "k2" }), key({ id: "k3" })];
101
+ assert.equal(nextkeyrow(pool, -1, NOW)?.row.id, "k3");
102
+ assert.equal(nextkeyrow(pool, 7, NOW)?.row.id, "k2");
103
+ });
104
+
105
+ it("answers an empty pool with null, never a thrown rotation", () => {
106
+ assert.equal(nextkeyrow([], 0, NOW), null);
107
+ });
108
+ });
109
+
110
+ describe("the key render discipline — selection answers rows, rendering masks", () => {
111
+ it("masks every key shape the routes answer", () => {
112
+ const short = "nvapi-short";
113
+ assert.equal(maskkey(short), "nvap****");
114
+ const long = "nvapi-1234567890abcdef-ijklmnop";
115
+ assert.equal(maskkey(long), "nvapi-12****mnop");
116
+ const masked = maskkey(key({ id: "k9" }).key);
117
+ assert.ok(!masked.includes("credential-shape"), "the mask never carries the key body");
118
+ });
119
+
120
+ it("summarizes the per-provider rotation health the rail renders", () => {
121
+ const pool = [
122
+ key({ id: "k1" }),
123
+ key({ id: "k2", rotationCount: 18 }),
124
+ key({ id: "k3", errorCount: 2 }),
125
+ key({ id: "k4", status: "revoked" }),
126
+ ];
127
+ assert.deepEqual(rotationsummary(pool), { active: 3, rotating: 1, erroring: 1 });
128
+ });
129
+ });
@@ -12,8 +12,10 @@ import {
12
12
  notefailure,
13
13
  notesuccess,
14
14
  pickroute,
15
+ pickroundrobin,
15
16
  ROTATIONDEFAULTS,
16
17
  remainingcooldown,
18
+ rotatepool,
17
19
  } from "../routerotation.ts";
18
20
 
19
21
  /** the three-route pool the tests rotate over. */
@@ -95,3 +97,101 @@ describe("routerotation cooldown ledger", () => {
95
97
  assert.equal(healthyroutes(POOL, ledger, 1000).length, 3);
96
98
  });
97
99
  });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // round robin + the unified rotation entry (pr19: round-robin + weighted +
103
+ // health-aware in one rotation math)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ describe("routerotation round robin", () => {
107
+ it("walks the healthy pool in stable order and wraps the cursor", () => {
108
+ const ledger = newledger();
109
+ const walk: string[] = [];
110
+ let cursor = 0;
111
+ for (let step = 0; step < 7; step++) {
112
+ const pick = pickroundrobin(POOL, ledger, cursor, 1000);
113
+ assert.ok(pick, "a healthy pool always answers");
114
+ walk.push(pick.route.id);
115
+ cursor = pick.cursor;
116
+ }
117
+ assert.deepEqual(walk, ["r1", "r2", "r3", "r1", "r2", "r3", "r1"]);
118
+ });
119
+
120
+ it("gives every healthy route an equal share over a full cycle", () => {
121
+ const ledger = newledger();
122
+ const shares = new Map<string, number>([["r1", 0], ["r2", 0], ["r3", 0]]);
123
+ let cursor = 0;
124
+ for (let step = 0; step < 300; step++) {
125
+ const pick = pickroundrobin(POOL, ledger, cursor, 1000);
126
+ assert.ok(pick);
127
+ shares.set(pick.route.id, (shares.get(pick.route.id) ?? 0) + 1);
128
+ cursor = pick.cursor;
129
+ }
130
+ assert.equal(shares.get("r1"), 100);
131
+ assert.equal(shares.get("r2"), 100);
132
+ assert.equal(shares.get("r3"), 100);
133
+ });
134
+
135
+ it("stays health-aware: cooling and zero-weight routes leave the walk", () => {
136
+ const ledger = newledger();
137
+ notefailure(ledger, "r2", { allowedFails: 0 }, 1000);
138
+ const routes = [...POOL, { id: "r0", provider: "babeltown", model: "paused", weight: 0 }];
139
+ const walk: string[] = [];
140
+ let cursor = 0;
141
+ for (let step = 0; step < 4; step++) {
142
+ const pick = pickroundrobin(routes, ledger, cursor, 1000);
143
+ assert.ok(pick);
144
+ walk.push(pick.route.id);
145
+ cursor = pick.cursor;
146
+ }
147
+ assert.deepEqual(walk, ["r1", "r3", "r1", "r3"]);
148
+ // the cooldown expiry walks r2 back in: the cursor resumes over the
149
+ // restored three-route pool (0 → r1) and the very next pick reaches r2
150
+ const back = pickroundrobin(routes, ledger, cursor, 1000 + ROTATIONDEFAULTS.cooldownMs + 1);
151
+ assert.ok(back);
152
+ assert.equal(back.route.id, "r1");
153
+ const then = pickroundrobin(routes, ledger, back.cursor, 1000 + ROTATIONDEFAULTS.cooldownMs + 1);
154
+ assert.equal(then?.route.id, "r2");
155
+ });
156
+
157
+ it("answers null when every route cools down, whatever the cursor", () => {
158
+ const ledger = newledger();
159
+ for (const route of POOL) notefailure(ledger, route.id, { allowedFails: 0 }, 1000);
160
+ assert.equal(pickroundrobin(POOL, ledger, 0, 1000), null);
161
+ assert.equal(pickroundrobin(POOL, ledger, 41, 1000), null);
162
+ });
163
+
164
+ it("normalizes any cursor the caller persisted onto the healthy pool", () => {
165
+ const ledger = newledger();
166
+ assert.equal(pickroundrobin(POOL, ledger, -1, 1000)?.route.id, "r3");
167
+ assert.equal(pickroundrobin(POOL, ledger, 7, 1000)?.route.id, "r2");
168
+ assert.equal(pickroundrobin(POOL, ledger, 8, 1000)?.route.id, "r3");
169
+ });
170
+ });
171
+
172
+ describe("rotatepool — the unified rotation entry", () => {
173
+ it("defaults to the round-robin walk", () => {
174
+ const ledger = newledger();
175
+ const first = rotatepool({ routes: POOL, ledger, cursor: 0, now: 1000 });
176
+ const second = rotatepool({ routes: POOL, ledger, cursor: first?.cursor, now: 1000 });
177
+ assert.equal(first?.route.id, "r1");
178
+ assert.equal(second?.route.id, "r2");
179
+ });
180
+
181
+ it("answers the weighted draw under the weighted strategy (cursor untouched)", () => {
182
+ const ledger = newledger();
183
+ const pick = rotatepool({ routes: POOL, ledger, strategy: "weighted", cursor: 9, rand: () => 0, now: 1000 });
184
+ assert.equal(pick?.route.id, "r1");
185
+ assert.equal(pick?.cursor, 9);
186
+ // rand just below 1 lands on the last healthy route
187
+ const tail = rotatepool({ routes: POOL, ledger, strategy: "weighted", rand: () => 0.9999, now: 1000 });
188
+ assert.equal(tail?.route.id, "r3");
189
+ });
190
+
191
+ it("answers null under both strategies when nothing is healthy", () => {
192
+ const ledger = newledger();
193
+ for (const route of POOL) notefailure(ledger, route.id, { allowedFails: 0 }, 1000);
194
+ assert.equal(rotatepool({ routes: POOL, ledger, strategy: "round-robin", now: 1000 }), null);
195
+ assert.equal(rotatepool({ routes: POOL, ledger, strategy: "weighted", rand: () => 0, now: 1000 }), null);
196
+ });
197
+ });
@@ -2,11 +2,21 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  DEFAULTTHINKINGLEVEL,
4
4
  THINKINGLEVELS,
5
+ TWOCALLSTHRESHOLD,
6
+ type ThinkingLevelName,
5
7
  budgetof,
8
+ clampbudget,
6
9
  defaultthinkinglevel,
10
+ fallbackchain,
7
11
  islevel,
12
+ lanesof,
8
13
  listlevels,
14
+ providerchain,
15
+ type thinkingproviderrow,
16
+ providersserving,
9
17
  registerthinkinglevels,
18
+ targettier,
19
+ twocallspattern,
10
20
  } from "../thinking.ts";
11
21
  import { autothinking } from "../utils.ts";
12
22
 
@@ -75,3 +85,137 @@ describe("the thinking ladder registration (the engine freedom law)", () => {
75
85
  expect(defaultthinkinglevel()).toBe("high");
76
86
  });
77
87
  });
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // the thinking seat routing — the tier ladder, the budget clamp, the
91
+ // two-calls verdict, the fallback chain, the lanes and the provider catalog
92
+ // (the pr19 thinking seat: the level selects the model tier, the token
93
+ // budget and the fallback chain — pure math, no gateway import)
94
+ // ---------------------------------------------------------------------------
95
+
96
+ describe("the thinking seat routing (the family seed ladder)", () => {
97
+ it("routes each seed rung to its model tier by ladder position", () => {
98
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
99
+ expect(targettier("none")).toBe(4);
100
+ expect(targettier("minimal")).toBe(4);
101
+ expect(targettier("low")).toBe(3);
102
+ expect(targettier("medium")).toBe(3);
103
+ expect(targettier("high")).toBe(2);
104
+ expect(targettier("xhigh")).toBe(1);
105
+ expect(targettier("max")).toBe(1);
106
+ });
107
+
108
+ it("routes an unknown level to the default rung's tier", () => {
109
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
110
+ expect(targettier("bogus" as ThinkingLevelName)).toBe(2);
111
+ });
112
+
113
+ it("clamps the level budget to the model output ceiling", () => {
114
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
115
+ expect(clampbudget("high")).toBe(68000);
116
+ expect(clampbudget("high", 16384)).toBe(16384);
117
+ expect(clampbudget("none", 16384)).toBe(0);
118
+ // a missing or invalid ceiling never clamps
119
+ expect(clampbudget("high", undefined)).toBe(68000);
120
+ expect(clampbudget("high", Number.NaN)).toBe(68000);
121
+ expect(clampbudget("high", -1)).toBe(68000);
122
+ });
123
+
124
+ it("answers the two-calls verdict the engine's zai lane always carried", () => {
125
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
126
+ // a thinking level always rides the pattern
127
+ expect(twocallspattern({ level: "minimal", responsemax: 0 })).toBe(true);
128
+ expect(twocallspattern({ level: "high", responsemax: 1 })).toBe(true);
129
+ // none rides the pattern only when the windows cross the threshold
130
+ expect(twocallspattern({ level: "none", responsemax: 1000 })).toBe(false);
131
+ expect(twocallspattern({ level: "none", responsemax: TWOCALLSTHRESHOLD + 1 })).toBe(true);
132
+ // a config threshold and a config budget override win
133
+ expect(twocallspattern({ level: "none", responsemax: 100, threshold: 50 })).toBe(true);
134
+ expect(twocallspattern({ level: "none", responsemax: 100, budget: TWOCALLSTHRESHOLD })).toBe(true);
135
+ });
136
+
137
+ it("orders the fallback chain by tier distance, budget fit, score and id", () => {
138
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
139
+ const chain = fallbackchain(
140
+ [
141
+ { id: "tier1-small", reasoning: true, maxoutput: 4096, rank: { score: 60, tier: 1 } },
142
+ { id: "tier2-a", reasoning: true, maxoutput: 131072, rank: { score: 40, tier: 2 } },
143
+ { id: "tier2-b", reasoning: true, maxoutput: 131072, rank: { score: 44, tier: 2 } },
144
+ { id: "tier1-big", reasoning: true, maxoutput: 131072, rank: { score: 55, tier: 1 } },
145
+ { id: "tier3", reasoning: true, maxoutput: 131072, rank: { score: 30, tier: 3 } },
146
+ { id: "unranked", reasoning: true, maxoutput: 131072 },
147
+ { id: "nonthinking", reasoning: false, maxoutput: 131072, rank: { score: 90, tier: 1 } },
148
+ ],
149
+ "high",
150
+ );
151
+ // level high targets tier 2: tier-2 rows first (score desc), then the
152
+ // tier-1 row that holds the full budget, then every remaining tier-1
153
+ // and tier-3 row by distance (fullfit before score — the small tier-1
154
+ // rides below the full-fit tier-3), the unranked row last; the
155
+ // reasoning-false row never serves
156
+ expect(chain.map((row) => row.id)).toEqual([
157
+ "tier2-b",
158
+ "tier2-a",
159
+ "tier1-big",
160
+ "tier3",
161
+ "tier1-small",
162
+ "unranked",
163
+ ]);
164
+ // the none level serves every row (no thinking budget to carry)
165
+ expect(fallbackchain([{ id: "nonthinking", reasoning: false }], "none")).toHaveLength(1);
166
+ });
167
+
168
+ it("resolves the session and stream lanes in bind order", () => {
169
+ expect(lanesof({})).toEqual([]);
170
+ expect(lanesof({ stream: false, sessionid: null })).toEqual([]);
171
+ expect(lanesof({ stream: true })).toEqual(["stream"]);
172
+ expect(lanesof({ sessionid: "chat-9f21" })).toEqual(["session"]);
173
+ expect(lanesof({ stream: true, sessionid: "chat-9f21" })).toEqual(["session", "stream"]);
174
+ expect(lanesof({ sessionid: " " })).toEqual([]);
175
+ });
176
+
177
+ it("orders the provider chain live first and serves only capable catalogs", () => {
178
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
179
+ const catalog: thinkingproviderrow[] = [
180
+ { provider: "pausedone", badge: "paused", models: [{ id: "p1", reasoning: true, rank: { score: 50, tier: 2 } }] },
181
+ { provider: "liveone", badge: "live", models: [{ id: "l1", reasoning: true, rank: { score: 40, tier: 2 } }] },
182
+ { provider: "blind", badge: "live", models: [{ id: "b1", reasoning: false }] },
183
+ ];
184
+ expect(providersserving(catalog, "high").map((row) => row.provider)).toEqual(["liveone", "pausedone"]);
185
+ const chain = providerchain(catalog, "high");
186
+ expect(chain.map((candidate) => `${candidate.provider}/${candidate.model.id}`)).toEqual([
187
+ "liveone/l1",
188
+ "pausedone/p1",
189
+ ]);
190
+ });
191
+ });
192
+
193
+ describe("the thinking seat routing (a registered ladder)", () => {
194
+ it("routes the tier by position over any registered ladder length", () => {
195
+ registerthinkinglevels(
196
+ [
197
+ { level: "quick", budget: 100, desc: "registered floor" },
198
+ { level: "mid", budget: 900, desc: "registered middle" },
199
+ { level: "deep", budget: 90000, desc: "registered ceiling" },
200
+ ],
201
+ { default: "mid" },
202
+ );
203
+ expect(targettier("quick")).toBe(4);
204
+ expect(targettier("mid")).toBe(3);
205
+ expect(targettier("deep")).toBe(1);
206
+ expect(clampbudget("deep", 16384)).toBe(16384);
207
+ });
208
+
209
+ it("answers the ceiling a single-rung registration carries", () => {
210
+ registerthinkinglevels([{ level: "only", budget: 500, desc: "the only rung" }], { default: "only" });
211
+ expect(targettier("only")).toBe(1);
212
+ registerthinkinglevels([{ level: "off", budget: 0, desc: "no thinking at all" }], { default: "off" });
213
+ expect(targettier("off")).toBe(4);
214
+ });
215
+
216
+ it("restores the family seed for the suites that follow", () => {
217
+ registerthinkinglevels(THINKINGLEVELS, { default: "high" });
218
+ expect(listlevels()).toEqual(THINKINGLEVELS);
219
+ expect(targettier("high")).toBe(2);
220
+ });
221
+ });