fest-gateway 0.1.0

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 (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/dist/cli/claude.js +326 -0
  4. package/dist/cli/claude.js.map +1 -0
  5. package/dist/cli/config.js +59 -0
  6. package/dist/cli/config.js.map +1 -0
  7. package/dist/cli/login.js +130 -0
  8. package/dist/cli/login.js.map +1 -0
  9. package/dist/cli/logout.js +20 -0
  10. package/dist/cli/logout.js.map +1 -0
  11. package/dist/cli/whoami.js +34 -0
  12. package/dist/cli/whoami.js.map +1 -0
  13. package/dist/server/adapters/anthropic.js +74 -0
  14. package/dist/server/adapters/anthropic.js.map +1 -0
  15. package/dist/server/adapters/fireworks.js +54 -0
  16. package/dist/server/adapters/fireworks.js.map +1 -0
  17. package/dist/server/adapters/index.js +16 -0
  18. package/dist/server/adapters/index.js.map +1 -0
  19. package/dist/server/adapters/registry.js +38 -0
  20. package/dist/server/adapters/registry.js.map +1 -0
  21. package/dist/server/adapters/rewrite.js +152 -0
  22. package/dist/server/adapters/rewrite.js.map +1 -0
  23. package/dist/server/adapters/url.js +24 -0
  24. package/dist/server/adapters/url.js.map +1 -0
  25. package/dist/server/api/auth.js +213 -0
  26. package/dist/server/api/auth.js.map +1 -0
  27. package/dist/server/api/live.js +108 -0
  28. package/dist/server/api/live.js.map +1 -0
  29. package/dist/server/api/models.js +129 -0
  30. package/dist/server/api/models.js.map +1 -0
  31. package/dist/server/api/oauth.js +300 -0
  32. package/dist/server/api/oauth.js.map +1 -0
  33. package/dist/server/api/routes.js +164 -0
  34. package/dist/server/api/routes.js.map +1 -0
  35. package/dist/server/api/routing-view.js +47 -0
  36. package/dist/server/api/routing-view.js.map +1 -0
  37. package/dist/server/auth/accounts.js +111 -0
  38. package/dist/server/auth/accounts.js.map +1 -0
  39. package/dist/server/auth/gateway-401.js +85 -0
  40. package/dist/server/auth/gateway-401.js.map +1 -0
  41. package/dist/server/auth/guard.js +74 -0
  42. package/dist/server/auth/guard.js.map +1 -0
  43. package/dist/server/auth/oauth.js +43 -0
  44. package/dist/server/auth/oauth.js.map +1 -0
  45. package/dist/server/auth/password.js +104 -0
  46. package/dist/server/auth/password.js.map +1 -0
  47. package/dist/server/auth/posture.js +145 -0
  48. package/dist/server/auth/posture.js.map +1 -0
  49. package/dist/server/auth/session.js +161 -0
  50. package/dist/server/auth/session.js.map +1 -0
  51. package/dist/server/bin/fest.js +589 -0
  52. package/dist/server/bin/fest.js.map +1 -0
  53. package/dist/server/config.js +109 -0
  54. package/dist/server/config.js.map +1 -0
  55. package/dist/server/credentials/provider.js +70 -0
  56. package/dist/server/credentials/provider.js.map +1 -0
  57. package/dist/server/credentials/resolve.js +89 -0
  58. package/dist/server/credentials/resolve.js.map +1 -0
  59. package/dist/server/http/body.js +66 -0
  60. package/dist/server/http/body.js.map +1 -0
  61. package/dist/server/http/errors.js +144 -0
  62. package/dist/server/http/errors.js.map +1 -0
  63. package/dist/server/http/headers.js +189 -0
  64. package/dist/server/http/headers.js.map +1 -0
  65. package/dist/server/http/pipe.js +97 -0
  66. package/dist/server/http/pipe.js.map +1 -0
  67. package/dist/server/http/server.js +226 -0
  68. package/dist/server/http/server.js.map +1 -0
  69. package/dist/server/http/sse.js +167 -0
  70. package/dist/server/http/sse.js.map +1 -0
  71. package/dist/server/http/static.js +138 -0
  72. package/dist/server/http/static.js.map +1 -0
  73. package/dist/server/ingest/live-bus.js +67 -0
  74. package/dist/server/ingest/live-bus.js.map +1 -0
  75. package/dist/server/ingest/sink.js +141 -0
  76. package/dist/server/ingest/sink.js.map +1 -0
  77. package/dist/server/log.js +31 -0
  78. package/dist/server/log.js.map +1 -0
  79. package/dist/server/pipeline/count-tokens.js +48 -0
  80. package/dist/server/pipeline/count-tokens.js.map +1 -0
  81. package/dist/server/pipeline/dispatch.js +138 -0
  82. package/dist/server/pipeline/dispatch.js.map +1 -0
  83. package/dist/server/pipeline/passthrough.js +392 -0
  84. package/dist/server/pipeline/passthrough.js.map +1 -0
  85. package/dist/server/pipeline/preflight.js +66 -0
  86. package/dist/server/pipeline/preflight.js.map +1 -0
  87. package/dist/server/pipeline/record.js +49 -0
  88. package/dist/server/pipeline/record.js.map +1 -0
  89. package/dist/server/pipeline/substitute.js +287 -0
  90. package/dist/server/pipeline/substitute.js.map +1 -0
  91. package/dist/server/routes/resolve.js +176 -0
  92. package/dist/server/routes/resolve.js.map +1 -0
  93. package/dist/server/routes/table.js +247 -0
  94. package/dist/server/routes/table.js.map +1 -0
  95. package/dist/server/routes/watch.js +101 -0
  96. package/dist/server/routes/watch.js.map +1 -0
  97. package/dist/server/secret/fingerprint.js +107 -0
  98. package/dist/server/secret/fingerprint.js.map +1 -0
  99. package/dist/server/secret/non-persistable.js +69 -0
  100. package/dist/server/secret/non-persistable.js.map +1 -0
  101. package/dist/server/store/audit.js +53 -0
  102. package/dist/server/store/audit.js.map +1 -0
  103. package/dist/server/store/bootstrap.js +53 -0
  104. package/dist/server/store/bootstrap.js.map +1 -0
  105. package/dist/server/store/db.js +157 -0
  106. package/dist/server/store/db.js.map +1 -0
  107. package/dist/server/store/ids.js +14 -0
  108. package/dist/server/store/ids.js.map +1 -0
  109. package/dist/server/store/migrations/001-init.sql +213 -0
  110. package/dist/server/store/migrations/002-feed-and-error-indexes.sql +26 -0
  111. package/dist/server/store/migrations/003-routing.sql +42 -0
  112. package/dist/server/store/migrations/004-admin-auth.sql +72 -0
  113. package/dist/server/store/migrations/005-notional-cost.sql +28 -0
  114. package/dist/server/store/queries.js +904 -0
  115. package/dist/server/store/queries.js.map +1 -0
  116. package/dist/server/store/retention.js +79 -0
  117. package/dist/server/store/retention.js.map +1 -0
  118. package/dist/server/store/seed.js +203 -0
  119. package/dist/server/store/seed.js.map +1 -0
  120. package/dist/server/store/tokens.js +133 -0
  121. package/dist/server/store/tokens.js.map +1 -0
  122. package/dist/server/store/write.js +330 -0
  123. package/dist/server/store/write.js.map +1 -0
  124. package/dist/server/usage/accumulator.js +209 -0
  125. package/dist/server/usage/accumulator.js.map +1 -0
  126. package/dist/server/usage/cost.js +57 -0
  127. package/dist/server/usage/cost.js.map +1 -0
  128. package/dist/server/usage/prices/catalog.js +136 -0
  129. package/dist/server/usage/prices/catalog.js.map +1 -0
  130. package/dist/server/usage/prices/refresh.js +125 -0
  131. package/dist/server/usage/prices/refresh.js.map +1 -0
  132. package/dist/server/usage/prices/snapshot.json.gz +0 -0
  133. package/dist/server/usage/prices/table.js +102 -0
  134. package/dist/server/usage/prices/table.js.map +1 -0
  135. package/dist/server/usage/pricing.js +103 -0
  136. package/dist/server/usage/pricing.js.map +1 -0
  137. package/dist/shared/api.js +19 -0
  138. package/dist/shared/api.js.map +1 -0
  139. package/dist/shared/base-models.js +28 -0
  140. package/dist/shared/base-models.js.map +1 -0
  141. package/dist/shared/demotion-vars.js +17 -0
  142. package/dist/shared/demotion-vars.js.map +1 -0
  143. package/dist/shared/series.js +50 -0
  144. package/dist/shared/series.js.map +1 -0
  145. package/dist/shared/types.js +40 -0
  146. package/dist/shared/types.js.map +1 -0
  147. package/dist/web/dist/assets/index-8HlKsJqt.css +2 -0
  148. package/dist/web/dist/assets/index-DQ7fyPVm.js +56 -0
  149. package/dist/web/dist/assets/index-DQ7fyPVm.js.map +1 -0
  150. package/dist/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  151. package/dist/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  152. package/dist/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  153. package/dist/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  154. package/dist/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  155. package/dist/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  156. package/dist/web/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  157. package/dist/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  158. package/dist/web/dist/index.html +44 -0
  159. package/dist/web/dist/nearform-icon.svg +4 -0
  160. package/package.json +82 -0
  161. package/routes.example.json +22 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The developer-facing `~/.fest` config.
3
+ *
4
+ * Distinct from `server/config.ts`: nothing here is read by the server, and
5
+ * nothing in `server/config.ts` is read by the CLI. `fest login` writes this
6
+ * file; `fest whoami`/`fest claude`/`fest logout` read or delete it. It never
7
+ * touches the database — everything under `cli/` runs entirely on a
8
+ * developer's own machine.
9
+ */
10
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ export const CONFIG_DIR = join(homedir(), ".fest");
14
+ export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
15
+ function isCliConfig(v) {
16
+ if (typeof v !== "object" || v === null)
17
+ return false;
18
+ const r = v;
19
+ return (typeof r["serverUrl"] === "string" &&
20
+ typeof r["identityToken"] === "string" &&
21
+ typeof r["email"] === "string" &&
22
+ (r["provider"] === "google" || r["provider"] === "github" || r["provider"] === "env"));
23
+ }
24
+ /**
25
+ * `FEST_TOKEN` / `FEST_SERVER_URL` override the file when both are set — the
26
+ * CI path, where writing a config file to a shared runner's home directory
27
+ * would be the wrong kind of persistence.
28
+ */
29
+ export async function readCliConfig() {
30
+ const envToken = process.env.FEST_TOKEN;
31
+ const envServer = process.env.FEST_SERVER_URL;
32
+ if (envToken !== undefined && envToken !== "" && envServer !== undefined && envServer !== "") {
33
+ return {
34
+ serverUrl: envServer.replace(/\/+$/, ""),
35
+ identityToken: envToken,
36
+ email: "(FEST_TOKEN)",
37
+ provider: "env",
38
+ };
39
+ }
40
+ let raw;
41
+ try {
42
+ raw = await readFile(CONFIG_FILE, "utf8");
43
+ }
44
+ catch (err) {
45
+ if (err.code === "ENOENT")
46
+ return null;
47
+ throw err;
48
+ }
49
+ const parsed = JSON.parse(raw);
50
+ return isCliConfig(parsed) ? parsed : null;
51
+ }
52
+ export async function writeCliConfig(cfg) {
53
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
54
+ await writeFile(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
55
+ }
56
+ export async function clearCliConfig() {
57
+ await rm(CONFIG_FILE, { force: true });
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../cli/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAS3D,SAAS,WAAW,CAAC,CAAU;IAC7B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,OAAO,CACL,OAAO,CAAC,CAAC,WAAW,CAAC,KAAK,QAAQ;QAClC,OAAO,CAAC,CAAC,eAAe,CAAC,KAAK,QAAQ;QACtC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ;QAC9B,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC,CACtF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IACxC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC9C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE,EAAE,CAAC;QAC7F,OAAO;YACL,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;YACxC,aAAa,EAAE,QAAQ;YACvB,KAAK,EAAE,cAAc;YACrB,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAClE,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAc;IACjD,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1D,MAAM,SAAS,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `fest login`: the loopback OAuth flow.
3
+ *
4
+ * Opens the server's OAuth start URL in the developer's browser with a
5
+ * `cli_redirect_uri` pointing at a temporary local server on 127.0.0.1. The
6
+ * server, after completing Google/GitHub OAuth, redirects the browser back
7
+ * here with a freshly minted identity token in the query string — see
8
+ * `server/api/oauth.ts` and docs/CLI-AUTH.md. This is a loopback-redirect
9
+ * flow, not device-code polling: nothing is ever typed back into the CLI.
10
+ */
11
+ import { createServer } from "node:http";
12
+ import open from "open";
13
+ import { writeCliConfig } from "./config.js";
14
+ const PORT_RANGE_START = 8865;
15
+ const PORT_RANGE_END = 8875;
16
+ const TIMEOUT_MS = 5 * 60_000;
17
+ function escapeHtml(s) {
18
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
19
+ return s.replace(/[&<>"']/g, (c) => map[c]);
20
+ }
21
+ const SUCCESS_HTML = "<!doctype html><title>fest login</title>" +
22
+ '<body style="font-family:sans-serif">Signed in. You can close this window and return to your terminal.</body>';
23
+ function errorHtml(message) {
24
+ return ("<!doctype html><title>fest login</title>" +
25
+ `<body style="font-family:sans-serif">Sign-in failed: ${escapeHtml(message)}</body>`);
26
+ }
27
+ async function listenOnFreePort(handler) {
28
+ for (let port = PORT_RANGE_START; port <= PORT_RANGE_END; port += 1) {
29
+ const server = createServer(handler);
30
+ try {
31
+ await new Promise((resolve, reject) => {
32
+ server.once("error", reject);
33
+ server.listen(port, "127.0.0.1", () => resolve());
34
+ });
35
+ return { server, port };
36
+ }
37
+ catch (err) {
38
+ if (err.code !== "EADDRINUSE")
39
+ throw err;
40
+ }
41
+ }
42
+ throw new Error(`no free port in ${PORT_RANGE_START}-${PORT_RANGE_END} for the login callback`);
43
+ }
44
+ /**
45
+ * `--server` must be the GATEWAY's own address — the same one you'd hand to
46
+ * `ANTHROPIC_BASE_URL` by hand — not the Vite dashboard dev server. The two
47
+ * are easy to confuse (`npm run dev` prints both, and the dashboard is the
48
+ * one you actually open in a browser), but Vite only proxies `/api/*`, not
49
+ * `/v1/messages`, so a `fest claude` pointed at the dashboard port fails
50
+ * downstream inside Claude Code with a confusing "issue with the selected
51
+ * model" rather than here, where the mistake is actually legible. Caught
52
+ * before ever opening a browser, not after minting a token that then quietly
53
+ * doesn't work.
54
+ */
55
+ async function checkIsGateway(serverUrl) {
56
+ let ok = false;
57
+ try {
58
+ const res = await fetch(`${serverUrl}/healthz`);
59
+ const body = (await res.json().catch(() => null));
60
+ ok = res.ok && body?.ok === true;
61
+ }
62
+ catch {
63
+ ok = false;
64
+ }
65
+ if (!ok) {
66
+ throw new Error(`${serverUrl} does not look like a Fest gateway (GET /healthz failed).\n` +
67
+ "Use the gateway's own address — the same one you'd set ANTHROPIC_BASE_URL\n" +
68
+ "to by hand — not a dashboard dev server. `npm run dev` prints both; the\n" +
69
+ "one you want here is the \"gateway\" line, not the \"dashboard\" line.");
70
+ }
71
+ }
72
+ export async function runLogin(opts) {
73
+ const serverUrl = opts.serverUrl.replace(/\/+$/, "");
74
+ await checkIsGateway(serverUrl);
75
+ let resolveResult;
76
+ const done = new Promise((resolve) => {
77
+ resolveResult = resolve;
78
+ });
79
+ const { server, port } = await listenOnFreePort((req, res) => {
80
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
81
+ if (url.pathname !== "/callback") {
82
+ res.writeHead(404).end();
83
+ return;
84
+ }
85
+ const token = url.searchParams.get("token");
86
+ const email = url.searchParams.get("email");
87
+ const error = url.searchParams.get("error");
88
+ if (error !== null) {
89
+ res.writeHead(200, { "content-type": "text/html" }).end(errorHtml(error));
90
+ resolveResult({ error });
91
+ return;
92
+ }
93
+ if (token === null || email === null) {
94
+ res.writeHead(400, { "content-type": "text/html" }).end(errorHtml("missing token"));
95
+ resolveResult({ error: "callback did not include a token" });
96
+ return;
97
+ }
98
+ res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
99
+ resolveResult({ token, email });
100
+ });
101
+ try {
102
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
103
+ const startUrl = `${serverUrl}/api/auth/oauth/${opts.provider}/start?cli_redirect_uri=${encodeURIComponent(callbackUrl)}`;
104
+ process.stdout.write(`Opening your browser to sign in with ${opts.provider}...\n`);
105
+ process.stdout.write(`If it doesn't open, visit:\n ${startUrl}\n`);
106
+ (opts.openBrowser ?? open)(startUrl).catch(() => { });
107
+ const timeout = new Promise((resolve) => {
108
+ const t = setTimeout(() => resolve({ error: "timed out waiting for sign-in" }), TIMEOUT_MS);
109
+ t.unref();
110
+ });
111
+ const result = await Promise.race([done, timeout]);
112
+ if ("error" in result)
113
+ throw new Error(`login failed: ${result.error}`);
114
+ await writeCliConfig({
115
+ serverUrl,
116
+ identityToken: result.token,
117
+ email: result.email,
118
+ provider: opts.provider,
119
+ });
120
+ // The token is already written to disk; echoing it here would only add it
121
+ // to shell scrollback for no benefit, unlike `fest token create`, which is
122
+ // designed to be copied by hand into an env var.
123
+ process.stdout.write(`Logged in as ${result.email} via ${opts.provider}.\n`);
124
+ process.stdout.write("Token stored in ~/.fest/config.json (mode 600).\n");
125
+ }
126
+ finally {
127
+ await new Promise((resolve) => server.close(() => resolve()));
128
+ }
129
+ }
130
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../cli/login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `fest logout`: forget the local token.
3
+ *
4
+ * Deletes `~/.fest/config.json` only. It does NOT revoke the token
5
+ * server-side — that stays an explicit `fest token revoke <id>`, an operator
6
+ * action against the database. This command is "forget this on my machine",
7
+ * not a security control.
8
+ */
9
+ import { clearCliConfig, readCliConfig } from "./config.js";
10
+ export async function runLogout() {
11
+ const cfg = await readCliConfig();
12
+ await clearCliConfig();
13
+ if (cfg === null) {
14
+ process.stdout.write("was not logged in.\n");
15
+ return;
16
+ }
17
+ process.stdout.write(`removed local login for ${cfg.email}.\n`);
18
+ process.stdout.write("the token itself is still valid until an operator revokes it with `fest token revoke <id>`.\n");
19
+ }
20
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../cli/logout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,GAAG,GAAG,MAAM,aAAa,EAAE,CAAC;IAClC,MAAM,cAAc,EAAE,CAAC;IACvB,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC7C,OAAO;IACT,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+FAA+F,CAAC,CAAC;AACxH,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `fest whoami`: what's stored locally, plus a live check against the server.
3
+ */
4
+ import { readCliConfig } from "./config.js";
5
+ const out = (s) => void process.stdout.write(s + "\n");
6
+ export async function runWhoami() {
7
+ const cfg = await readCliConfig();
8
+ if (cfg === null) {
9
+ out("not logged in. Run `fest login` first.");
10
+ return;
11
+ }
12
+ out(`email : ${cfg.email}`);
13
+ out(`provider : ${cfg.provider}`);
14
+ out(`server : ${cfg.serverUrl}`);
15
+ try {
16
+ const res = await fetch(`${cfg.serverUrl}/api/auth/identity`, {
17
+ headers: { authorization: `Bearer ${cfg.identityToken}` },
18
+ });
19
+ if (res.ok) {
20
+ const body = (await res.json());
21
+ out(`status : token is live (server sees ${body.email})`);
22
+ }
23
+ else if (res.status === 401) {
24
+ out("status : token was revoked or is invalid. Run `fest login` again.");
25
+ }
26
+ else {
27
+ out(`status : could not confirm (HTTP ${res.status})`);
28
+ }
29
+ }
30
+ catch (err) {
31
+ out(`status : could not reach ${cfg.serverUrl} (${err instanceof Error ? err.message : String(err)})`);
32
+ }
33
+ }
34
+ //# sourceMappingURL=whoami.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../cli/whoami.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,GAAG,GAAG,CAAC,CAAS,EAAQ,EAAE,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAErE,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,GAAG,GAAG,MAAM,aAAa,EAAE,CAAC;IAClC,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,GAAG,CAAC,wCAAwC,CAAC,CAAC;QAC9C,OAAO;IACT,CAAC;IAED,GAAG,CAAC,cAAc,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/B,GAAG,CAAC,cAAc,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAClC,GAAG,CAAC,cAAc,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;IAEnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,SAAS,oBAAoB,EAAE;YAC5D,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,GAAG,CAAC,aAAa,EAAE,EAAE;SAC1D,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAsB,CAAC;YACrD,GAAG,CAAC,yCAAyC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;QAC9D,CAAC;aAAM,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC9B,GAAG,CAAC,qEAAqE,CAAC,CAAC;QAC7E,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,sCAAsC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,8BAA8B,GAAG,CAAC,SAAS,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3G,CAAC;AACH,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Anthropic as an EXPLICIT upstream — i.e. an org's own API key, not a
3
+ * developer's subscription.
4
+ *
5
+ * This is not the pass-through path and must not be confused with it. It exists
6
+ * for the case where an operator deliberately routes some model to Anthropic on
7
+ * a key the SERVER holds, which is org spend and is recorded as
8
+ * `fallback_server`. The subscription path never comes through here; it never
9
+ * touches an adapter at all.
10
+ */
11
+ import { rewriteModel } from "./rewrite.js";
12
+ import { joinUpstreamUrl } from "./url.js";
13
+ /**
14
+ * The OAuth beta flag, which must NOT be forwarded from here.
15
+ *
16
+ * `oauth-2025-04-20` is the flag that says "this request authenticates with a
17
+ * Claude subscription OAuth bearer". This path authenticates with an org
18
+ * `x-api-key`, so sending it asserts something untrue about the credential in
19
+ * the very same request. The pass-through path forwards the beta list verbatim
20
+ * for the opposite reason — there the assertion is true, and the set is part of
21
+ * what the token is validated against.
22
+ */
23
+ const OAUTH_BETA = "oauth-2025-04-20";
24
+ /** Drop the OAuth flag, keep the order and spelling of everything else. */
25
+ function forwardableBetas(raw) {
26
+ if (raw === null)
27
+ return null;
28
+ const kept = raw
29
+ .split(",")
30
+ .map((v) => v.trim())
31
+ .filter((v) => v !== "" && v !== OAUTH_BETA);
32
+ return kept.length === 0 ? null : kept.join(", ");
33
+ }
34
+ export const anthropicAdapter = {
35
+ id: "anthropic",
36
+ transforms: [
37
+ "model id rewritten to the configured id",
38
+ "server-held API key substituted for the caller's credential",
39
+ "Anthropic beta flags forwarded, except the OAuth one",
40
+ ],
41
+ plan(req) {
42
+ const url = joinUpstreamUrl(req.upstream.baseUrl, req.path);
43
+ const headers = new Headers({
44
+ // An org API key goes in x-api-key. Deliberately NOT `authorization`:
45
+ // that is where a subscription bearer lives, and the two must never be
46
+ // interchangeable in this codebase even by accident.
47
+ "x-api-key": req.secret.expose(),
48
+ "anthropic-version": "2023-06-01",
49
+ "content-type": "application/json",
50
+ accept: req.stream ? "text/event-stream" : "application/json",
51
+ "accept-encoding": "identity",
52
+ "user-agent": "fest",
53
+ });
54
+ /**
55
+ * Beta flags must ride along, because they are what makes the BODY legal.
56
+ *
57
+ * Claude Code sends top-level `context_management`, `output_config` and
58
+ * `safeguards`, each gated by a beta in this header. Anthropic validates
59
+ * the body strictly, so dropping the header does not degrade gracefully to
60
+ * "feature off" — it fails the whole request with
61
+ * `400 context_management: Extra inputs are not permitted`, on the first
62
+ * message, for a model the developer legitimately selected.
63
+ *
64
+ * Unlike the Fireworks adapter, which drops these because its destination
65
+ * cannot understand them, this destination IS Anthropic, so the flags mean
66
+ * exactly what they say.
67
+ */
68
+ const betas = forwardableBetas(req.betas);
69
+ if (betas !== null)
70
+ headers.set("anthropic-beta", betas);
71
+ return { url, headers, body: rewriteModel(req.body, req.servedModel) };
72
+ },
73
+ };
74
+ //# sourceMappingURL=anthropic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../server/adapters/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG,kBAAkB,CAAC;AAEtC,2EAA2E;AAC3E,SAAS,gBAAgB,CAAC,GAAkB;IAC1C,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,GAAG;SACb,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,UAAU,CAAC,CAAC;IAC/C,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAY;IACvC,EAAE,EAAE,WAAW;IAEf,UAAU,EAAE;QACV,yCAAyC;QACzC,6DAA6D;QAC7D,sDAAsD;KACvD;IAED,IAAI,CAAC,GAAmB;QACtB,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QAE5D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;YAC1B,sEAAsE;YACtE,uEAAuE;YACvE,qDAAqD;YACrD,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE;YAChC,mBAAmB,EAAE,YAAY;YACjC,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,kBAAkB;YAC7D,iBAAiB,EAAE,UAAU;YAC7B,YAAY,EAAE,MAAM;SACrB,CAAC,CAAC;QAEH;;;;;;;;;;;;;WAaG;QACH,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;QAEzD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;IACzE,CAAC;CACF,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Fireworks.
3
+ *
4
+ * The important discovery, and the reason this adapter is twenty lines of
5
+ * substance rather than a wire-format translator: **Fireworks serves an
6
+ * Anthropic-compatible Messages API**. `fireconnect` points Claude Code
7
+ * straight at `https://api.fireworks.ai/inference` as `ANTHROPIC_BASE_URL`,
8
+ * which only works if `/v1/messages` speaks the Anthropic request and SSE
9
+ * format. So there is no Anthropic↔OpenAI translation here, and — critically —
10
+ * no need to re-frame the event stream, which means the existing SSE parser and
11
+ * usage accumulator keep working unchanged on this path.
12
+ *
13
+ * What the adapter therefore does:
14
+ *
15
+ * - swaps the base URL,
16
+ * - swaps the credential to the server-held one (`Authorization: Bearer`),
17
+ * - rewrites the `model` field to the provider's id.
18
+ *
19
+ * That last one is the only body edit, and it is why this path is allowed to
20
+ * re-serialise the body at all: the request is going to a different vendor on a
21
+ * different credential, so there is no Anthropic-issued OAuth token whose
22
+ * signature depends on the exact bytes. The byte-for-byte rule protects the
23
+ * subscription path; it does not apply here, and pretending otherwise would
24
+ * make model rewriting impossible.
25
+ *
26
+ * ⚠ Not yet verified against the live API — there was no Fireworks key
27
+ * available when this was written. The compatibility claim is inferred from
28
+ * `fireconnect`'s configuration, which is strong evidence but not a test.
29
+ * Integration tests run against a mock. See docs/PHASE4.md.
30
+ */
31
+ import { rewriteModel } from "./rewrite.js";
32
+ import { joinUpstreamUrl } from "./url.js";
33
+ export const fireworksAdapter = {
34
+ id: "fireworks",
35
+ transforms: [
36
+ "model id rewritten to the provider's id",
37
+ "server-held credential substituted for the caller's",
38
+ "Anthropic-specific beta headers dropped",
39
+ ],
40
+ plan(req) {
41
+ const url = joinUpstreamUrl(req.upstream.baseUrl, req.path);
42
+ const headers = new Headers({
43
+ // Fireworks authenticates with a bearer, not Anthropic's x-api-key.
44
+ authorization: `Bearer ${req.secret.expose()}`,
45
+ "content-type": "application/json",
46
+ accept: req.stream ? "text/event-stream" : "application/json",
47
+ // The tee needs unencoded bytes to meter, exactly as on the other path.
48
+ "accept-encoding": "identity",
49
+ "user-agent": "fest",
50
+ });
51
+ return { url, headers, body: rewriteModel(req.body, req.servedModel) };
52
+ },
53
+ };
54
+ //# sourceMappingURL=fireworks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fireworks.js","sourceRoot":"","sources":["../../../server/adapters/fireworks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,CAAC,MAAM,gBAAgB,GAAY;IACvC,EAAE,EAAE,WAAW;IAEf,UAAU,EAAE;QACV,yCAAyC;QACzC,qDAAqD;QACrD,yCAAyC;KAC1C;IAED,IAAI,CAAC,GAAmB;QACtB,MAAM,GAAG,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QAE5D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC;YAC1B,oEAAoE;YACpE,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE;YAC9C,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,kBAAkB;YAC7D,wEAAwE;YACxE,iBAAiB,EAAE,UAAU;YAC7B,YAAY,EAAE,MAAM;SACrB,CAAC,CAAC;QAEH,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;IACzE,CAAC;CACF,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Provider adapters.
3
+ *
4
+ * An adapter's job is narrow by construction: given a validated request that is
5
+ * already in Anthropic Messages shape, produce the URL, headers and body to
6
+ * send to one provider, and describe how to read usage back.
7
+ *
8
+ * Note what an adapter is NOT allowed to be used for: the pass-through pipeline
9
+ * does not go through this interface at all. Adapters exist to transform;
10
+ * `pipeline/passthrough.ts` exists precisely not to. Keeping them apart is what
11
+ * makes "bytes are forwarded verbatim on the subscription path" a structural
12
+ * property rather than a convention someone can erode one helpful rewrite at a
13
+ * time.
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Adapter descriptions, separated from the adapters themselves.
3
+ *
4
+ * The API layer needs to tell an operator what an adapter changes about a
5
+ * request, but importing `adapters/fireworks.ts` into the dashboard API would
6
+ * drag the whole request-building path — and a live secret type — into a module
7
+ * whose entire job is to emit JSON to a browser. This holds the descriptions
8
+ * and nothing else.
9
+ */
10
+ export const ADAPTER_TRANSFORMS = {
11
+ fireworks: [
12
+ "model id rewritten to the provider's id",
13
+ "server-held credential substituted for the caller's",
14
+ "Anthropic beta headers not forwarded",
15
+ ],
16
+ anthropic: [
17
+ "model id rewritten to the configured id",
18
+ "server-held API key substituted for the caller's credential",
19
+ "Anthropic beta flags forwarded, except the OAuth one",
20
+ ],
21
+ };
22
+ /**
23
+ * Fest's adapter id → litellm's provider key, for price lookup.
24
+ *
25
+ * Needed because a substitute route sends a provider-native model id
26
+ * (`accounts/fireworks/models/deepseek-v4-pro`) that is ambiguous on its own —
27
+ * litellm files it under `fireworks_ai/accounts/fireworks/models/deepseek-v4-pro`.
28
+ *
29
+ * A new adapter that forgets to add a line here prices its traffic as unknown
30
+ * rather than as some other vendor's rate. That is the correct failure: an
31
+ * "n/a" on the dashboard is a question someone asks, and a wrong dollar figure
32
+ * is one nobody does.
33
+ */
34
+ export const ADAPTER_PRICE_PROVIDERS = {
35
+ fireworks: "fireworks_ai",
36
+ anthropic: "anthropic",
37
+ };
38
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../server/adapters/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,CAAC,MAAM,kBAAkB,GAAmD;IAChF,SAAS,EAAE;QACT,yCAAyC;QACzC,qDAAqD;QACrD,sCAAsC;KACvC;IACD,SAAS,EAAE;QACT,yCAAyC;QACzC,6DAA6D;QAC7D,sDAAsD;KACvD;CACF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAwC;IAC1E,SAAS,EAAE,cAAc;IACzB,SAAS,EAAE,WAAW;CACvB,CAAC"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Rewrite the `model` field of a request body, and nothing else.
3
+ *
4
+ * This is the one body edit Fest performs, and it is confined to the substitute
5
+ * path. It is safe there and would not be on the pass-through path: an
6
+ * Anthropic OAuth token is validated against the request shape, so a
7
+ * parse/re-serialise round trip (which changes key order, whitespace, number
8
+ * formatting and unicode escaping) can invalidate it. A substitute request goes
9
+ * to a different vendor on a different credential, so no signature depends on
10
+ * the bytes.
11
+ *
12
+ * The rewrite is still done surgically rather than by round-tripping the whole
13
+ * document, for two reasons that have nothing to do with signatures:
14
+ *
15
+ * 1. **Prompt caching.** `cache_control` markers must survive untouched, and
16
+ * the cheapest way to guarantee that is to not re-serialise the parts of
17
+ * the body that carry them.
18
+ * 2. **Size.** A Claude Code request is routinely megabytes of context.
19
+ * `JSON.parse` → mutate → `JSON.stringify` copies all of it to change
20
+ * twenty bytes, on the request path, for every request.
21
+ *
22
+ * So: find the top-level `"model"` string and splice. If anything about the
23
+ * body is not what we expect, the ORIGINAL bytes are returned unchanged. A
24
+ * failed rewrite must never corrupt a request — the worst acceptable outcome is
25
+ * that the provider rejects an unknown model id with its own clear error.
26
+ */
27
+ /**
28
+ * Matches a top-level `"model": "..."` pair.
29
+ *
30
+ * Anchored to the start of the document and allowed to skip over preceding
31
+ * members only at nesting depth zero, which is checked separately below — the
32
+ * regex alone cannot express that, and a naive search would happily rewrite a
33
+ * `model` key nested inside a tool definition or a message.
34
+ */
35
+ const MODEL_KEY = /"model"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
36
+ /** JSON string escaping for the replacement value. */
37
+ function jsonString(value) {
38
+ return JSON.stringify(value);
39
+ }
40
+ /**
41
+ * Find the offset of the top-level `"model"` key, or -1.
42
+ *
43
+ * Walks the document tracking string state and brace/bracket depth, so a
44
+ * `"model"` inside a nested object, an array, or a string literal is skipped.
45
+ * This is the difference between rewriting the request's model and silently
46
+ * rewriting a model name a developer mentioned in a prompt.
47
+ */
48
+ function findTopLevelModel(text) {
49
+ MODEL_KEY.lastIndex = 0;
50
+ let depth = 0;
51
+ let inString = false;
52
+ let escaped = false;
53
+ // Offsets at which a top-level key may begin.
54
+ const topLevelRanges = [];
55
+ let rangeStart = -1;
56
+ for (let i = 0; i < text.length; i += 1) {
57
+ const ch = text[i];
58
+ if (inString) {
59
+ if (escaped)
60
+ escaped = false;
61
+ else if (ch === "\\")
62
+ escaped = true;
63
+ else if (ch === '"')
64
+ inString = false;
65
+ continue;
66
+ }
67
+ if (ch === '"') {
68
+ inString = true;
69
+ continue;
70
+ }
71
+ if (ch === "{" || ch === "[") {
72
+ depth += 1;
73
+ if (depth === 1 && ch === "{")
74
+ rangeStart = i;
75
+ continue;
76
+ }
77
+ if (ch === "}" || ch === "]") {
78
+ if (depth === 1 && rangeStart !== -1) {
79
+ topLevelRanges.push([rangeStart, i]);
80
+ rangeStart = -1;
81
+ }
82
+ depth -= 1;
83
+ continue;
84
+ }
85
+ }
86
+ if (rangeStart !== -1)
87
+ topLevelRanges.push([rangeStart, text.length]);
88
+ // Re-scan only the top-level object, tracking depth so nested matches are
89
+ // rejected rather than merely deprioritised.
90
+ const range = topLevelRanges[0];
91
+ if (range === undefined)
92
+ return null;
93
+ let d = 0;
94
+ let str = false;
95
+ let esc = false;
96
+ for (let i = range[0]; i < range[1]; i += 1) {
97
+ const ch = text[i];
98
+ if (str) {
99
+ if (esc)
100
+ esc = false;
101
+ else if (ch === "\\")
102
+ esc = true;
103
+ else if (ch === '"')
104
+ str = false;
105
+ continue;
106
+ }
107
+ if (ch === "{" || ch === "[") {
108
+ d += 1;
109
+ continue;
110
+ }
111
+ if (ch === "}" || ch === "]") {
112
+ d -= 1;
113
+ continue;
114
+ }
115
+ if (ch === '"') {
116
+ // Only consider a key match when we are directly inside the root object.
117
+ if (d === 1) {
118
+ MODEL_KEY.lastIndex = i;
119
+ const m = MODEL_KEY.exec(text);
120
+ if (m !== null && m.index === i) {
121
+ return { start: i, end: i + m[0].length };
122
+ }
123
+ }
124
+ str = true;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Replace the top-level `model` with `servedModel`.
131
+ *
132
+ * Returns the input unchanged when `servedModel` is null, when the body is not
133
+ * a JSON object, or when no top-level `model` key is present.
134
+ */
135
+ export function rewriteModel(body, servedModel) {
136
+ if (servedModel === null || servedModel === "")
137
+ return body;
138
+ let text;
139
+ try {
140
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
141
+ }
142
+ catch {
143
+ // Not UTF-8 text; nothing safe to do but pass it along.
144
+ return body;
145
+ }
146
+ const found = findTopLevelModel(text);
147
+ if (found === null)
148
+ return body;
149
+ const replaced = `${text.slice(0, found.start)}"model": ${jsonString(servedModel)}${text.slice(found.end)}`;
150
+ return new TextEncoder().encode(replaced);
151
+ }
152
+ //# sourceMappingURL=rewrite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rewrite.js","sourceRoot":"","sources":["../../../server/adapters/rewrite.ts"],"names":[],"mappings":"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"}