aigetwey 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +302 -0
- package/assets/logo.svg +8 -0
- package/assets/screenshot.png +0 -0
- package/assets/wordmark.svg +9 -0
- package/config.example.yaml +56 -0
- package/dashboard/.env.example +12 -0
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/next.config.ts +12 -0
- package/dashboard/package-lock.json +1771 -0
- package/dashboard/package.json +29 -0
- package/dashboard/postcss.config.mjs +5 -0
- package/dashboard/src/app/(console)/combos/page.tsx +10 -0
- package/dashboard/src/app/(console)/config/page.tsx +5 -0
- package/dashboard/src/app/(console)/console/page.tsx +92 -0
- package/dashboard/src/app/(console)/endpoint/page.tsx +5 -0
- package/dashboard/src/app/(console)/layout.tsx +17 -0
- package/dashboard/src/app/(console)/page.tsx +8 -0
- package/dashboard/src/app/(console)/providers/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/providers/page.tsx +5 -0
- package/dashboard/src/app/(console)/quota/page.tsx +5 -0
- package/dashboard/src/app/(console)/tools/[id]/page.tsx +6 -0
- package/dashboard/src/app/(console)/tools/page.tsx +5 -0
- package/dashboard/src/app/(console)/usage/page.tsx +24 -0
- package/dashboard/src/app/api/cli-detect/[tool]/route.ts +253 -0
- package/dashboard/src/app/api/gw/[...path]/route.ts +89 -0
- package/dashboard/src/app/api/login/route.ts +30 -0
- package/dashboard/src/app/api/logout/route.ts +9 -0
- package/dashboard/src/app/api/password/route.ts +34 -0
- package/dashboard/src/app/globals.css +340 -0
- package/dashboard/src/app/icon.svg +8 -0
- package/dashboard/src/app/layout.tsx +28 -0
- package/dashboard/src/app/login/page.tsx +60 -0
- package/dashboard/src/components/AreaChart.tsx +115 -0
- package/dashboard/src/components/Badge.tsx +32 -0
- package/dashboard/src/components/Button.tsx +60 -0
- package/dashboard/src/components/CapacityBadges.tsx +40 -0
- package/dashboard/src/components/Checkbox.tsx +40 -0
- package/dashboard/src/components/CliToolConfig.tsx +63 -0
- package/dashboard/src/components/ConfigEditor.tsx +199 -0
- package/dashboard/src/components/ConfirmModal.tsx +36 -0
- package/dashboard/src/components/CooldownTimer.tsx +42 -0
- package/dashboard/src/components/EndpointView.tsx +439 -0
- package/dashboard/src/components/Icon.tsx +25 -0
- package/dashboard/src/components/KeyReveal.tsx +78 -0
- package/dashboard/src/components/Lamp.tsx +8 -0
- package/dashboard/src/components/LogTable.tsx +223 -0
- package/dashboard/src/components/LogoutButton.tsx +20 -0
- package/dashboard/src/components/ModelPicker.tsx +121 -0
- package/dashboard/src/components/ModelSelectModal.tsx +126 -0
- package/dashboard/src/components/PasswordEditor.tsx +86 -0
- package/dashboard/src/components/PricingEditor.tsx +171 -0
- package/dashboard/src/components/ProviderDetail.tsx +566 -0
- package/dashboard/src/components/ProviderManager.tsx +311 -0
- package/dashboard/src/components/QuotaView.tsx +78 -0
- package/dashboard/src/components/Rail.tsx +82 -0
- package/dashboard/src/components/RichCard.tsx +46 -0
- package/dashboard/src/components/RoutingView.tsx +329 -0
- package/dashboard/src/components/ThemeProvider.tsx +36 -0
- package/dashboard/src/components/ToastProvider.tsx +58 -0
- package/dashboard/src/components/ToolDetail.tsx +475 -0
- package/dashboard/src/components/TopBar.tsx +128 -0
- package/dashboard/src/components/UsageView.tsx +151 -0
- package/dashboard/src/components/ui.tsx +54 -0
- package/dashboard/src/lib/capabilities.ts +318 -0
- package/dashboard/src/lib/cliTools.ts +120 -0
- package/dashboard/src/lib/client.ts +190 -0
- package/dashboard/src/lib/gateway.ts +269 -0
- package/dashboard/src/lib/session.ts +71 -0
- package/dashboard/src/middleware.ts +37 -0
- package/dashboard/tsconfig.json +21 -0
- package/dist/adapters/anthropic.js +289 -0
- package/dist/adapters/anthropic.js.map +1 -0
- package/dist/adapters/gemini.js +268 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/openai.js +13 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/cli/tray/autostart.js +152 -0
- package/dist/cli/tray/autostart.js.map +1 -0
- package/dist/cli/tray/icon.js +4 -0
- package/dist/cli/tray/icon.js.map +1 -0
- package/dist/cli/tray/tray.js +141 -0
- package/dist/cli/tray/tray.js.map +1 -0
- package/dist/cli/tray/trayRuntime.js +91 -0
- package/dist/cli/tray/trayRuntime.js.map +1 -0
- package/dist/cli.js +361 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/core/authStore.js +78 -0
- package/dist/core/authStore.js.map +1 -0
- package/dist/core/canonical.js +9 -0
- package/dist/core/canonical.js.map +1 -0
- package/dist/core/console-buffer.js +25 -0
- package/dist/core/console-buffer.js.map +1 -0
- package/dist/core/fallback.js +62 -0
- package/dist/core/fallback.js.map +1 -0
- package/dist/core/handler.js +174 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/core/keypool.js +105 -0
- package/dist/core/keypool.js.map +1 -0
- package/dist/core/quota.js +165 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/state.js +52 -0
- package/dist/core/state.js.map +1 -0
- package/dist/db.js +193 -0
- package/dist/db.js.map +1 -0
- package/dist/headroom/compress.js +44 -0
- package/dist/headroom/compress.js.map +1 -0
- package/dist/headroom/detect.js +108 -0
- package/dist/headroom/detect.js.map +1 -0
- package/dist/headroom/process.js +158 -0
- package/dist/headroom/process.js.map +1 -0
- package/dist/inject/caveman.js +30 -0
- package/dist/inject/caveman.js.map +1 -0
- package/dist/inject/index.js +24 -0
- package/dist/inject/index.js.map +1 -0
- package/dist/inject/ponytail.js +19 -0
- package/dist/inject/ponytail.js.map +1 -0
- package/dist/middleware/auth.js +66 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/providers/capabilities.js +246 -0
- package/dist/providers/capabilities.js.map +1 -0
- package/dist/providers/free.js +43 -0
- package/dist/providers/free.js.map +1 -0
- package/dist/providers/pricing.js +224 -0
- package/dist/providers/pricing.js.map +1 -0
- package/dist/providers/vertex.js +97 -0
- package/dist/providers/vertex.js.map +1 -0
- package/dist/routes/admin.js +622 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/health.js +4 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/v1.js +75 -0
- package/dist/routes/v1.js.map +1 -0
- package/dist/rtk/detect.js +50 -0
- package/dist/rtk/detect.js.map +1 -0
- package/dist/rtk/filters.js +85 -0
- package/dist/rtk/filters.js.map +1 -0
- package/dist/rtk/index.js +39 -0
- package/dist/rtk/index.js.map +1 -0
- package/dist/server.js +100 -0
- package/dist/server.js.map +1 -0
- package/dist/stream/anthropic-stream.js +239 -0
- package/dist/stream/anthropic-stream.js.map +1 -0
- package/dist/stream/chunk.js +7 -0
- package/dist/stream/chunk.js.map +1 -0
- package/dist/stream/gemini-stream.js +135 -0
- package/dist/stream/gemini-stream.js.map +1 -0
- package/dist/stream/index.js +12 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/openai-stream.js +34 -0
- package/dist/stream/openai-stream.js.map +1 -0
- package/dist/stream/sse.js +64 -0
- package/dist/stream/sse.js.map +1 -0
- package/dist/translator/thinking.js +70 -0
- package/dist/translator/thinking.js.map +1 -0
- package/dist/translator/thinkingUnified.js +322 -0
- package/dist/translator/thinkingUnified.js.map +1 -0
- package/dist/upstream/client.js +120 -0
- package/dist/upstream/client.js.map +1 -0
- package/package.json +76 -0
- package/run.sh +27 -0
- package/src/adapters/anthropic.ts +377 -0
- package/src/adapters/gemini.ts +341 -0
- package/src/adapters/index.ts +17 -0
- package/src/adapters/openai.ts +22 -0
- package/src/cli/tray/autostart.ts +133 -0
- package/src/cli/tray/icon.ts +4 -0
- package/src/cli/tray/tray.ts +156 -0
- package/src/cli/tray/trayRuntime.ts +90 -0
- package/src/cli.ts +379 -0
- package/src/config.ts +777 -0
- package/src/core/authStore.ts +86 -0
- package/src/core/canonical.ts +93 -0
- package/src/core/console-buffer.ts +39 -0
- package/src/core/fallback.ts +116 -0
- package/src/core/handler.ts +236 -0
- package/src/core/keypool.ts +152 -0
- package/src/core/quota.ts +214 -0
- package/src/core/state.ts +65 -0
- package/src/db.ts +280 -0
- package/src/headroom/compress.ts +78 -0
- package/src/headroom/detect.ts +119 -0
- package/src/headroom/process.ts +166 -0
- package/src/inject/caveman.ts +35 -0
- package/src/inject/index.ts +46 -0
- package/src/inject/ponytail.ts +31 -0
- package/src/middleware/auth.ts +76 -0
- package/src/providers/capabilities.ts +297 -0
- package/src/providers/free.ts +53 -0
- package/src/providers/pricing.ts +261 -0
- package/src/providers/vertex.ts +117 -0
- package/src/routes/admin.ts +716 -0
- package/src/routes/health.ts +5 -0
- package/src/routes/index.ts +24 -0
- package/src/routes/v1.ts +87 -0
- package/src/rtk/detect.ts +55 -0
- package/src/rtk/filters.ts +94 -0
- package/src/rtk/index.ts +58 -0
- package/src/server.ts +108 -0
- package/src/stream/anthropic-stream.ts +310 -0
- package/src/stream/chunk.ts +46 -0
- package/src/stream/gemini-stream.ts +158 -0
- package/src/stream/index.ts +23 -0
- package/src/stream/openai-stream.ts +41 -0
- package/src/stream/sse.ts +72 -0
- package/src/translator/thinking.ts +64 -0
- package/src/translator/thinkingUnified.ts +319 -0
- package/src/upstream/client.ts +155 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { registerHealthRoute } from "./health.js";
|
|
3
|
+
import { registerV1Routes } from "./v1.js";
|
|
4
|
+
import { registerAdminRoutes } from "./admin.js";
|
|
5
|
+
import type { GatewayState } from "../core/state.js";
|
|
6
|
+
import type { UsageDB } from "../db.js";
|
|
7
|
+
import type { AuthStore } from "../core/authStore.js";
|
|
8
|
+
|
|
9
|
+
export function registerRoutes(
|
|
10
|
+
app: FastifyInstance,
|
|
11
|
+
state: GatewayState,
|
|
12
|
+
db: UsageDB | undefined,
|
|
13
|
+
auth: AuthStore,
|
|
14
|
+
): void {
|
|
15
|
+
registerHealthRoute(app);
|
|
16
|
+
registerV1Routes(app, state, db);
|
|
17
|
+
registerAdminRoutes(app, { state, db, auth });
|
|
18
|
+
|
|
19
|
+
if (state.config.server.api_keys.length === 0) {
|
|
20
|
+
app.log.warn(
|
|
21
|
+
"server.api_keys is empty — gateway auth is DISABLED. Safe only on localhost; set keys before exposing remotely.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/routes/v1.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { checkAuth } from "../middleware/auth.js";
|
|
3
|
+
import type { GatewayState } from "../core/state.js";
|
|
4
|
+
import { handle, GatewayError, type HandleDeps } from "../core/handler.js";
|
|
5
|
+
import type { WireFormat } from "../core/canonical.js";
|
|
6
|
+
import type { UsageDB } from "../db.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* /v1 proxy surface. Auth-gates on the gateway's own keys (read from state each
|
|
10
|
+
* request so a hot-reload takes effect immediately), then runs the translation
|
|
11
|
+
* pipeline (non-stream JSON or SSE stream).
|
|
12
|
+
*/
|
|
13
|
+
export function registerV1Routes(app: FastifyInstance, state: GatewayState, db?: UsageDB): void {
|
|
14
|
+
const requireAuth = {
|
|
15
|
+
preHandler: (req: FastifyRequest, reply: FastifyReply, done: (err?: Error) => void) => {
|
|
16
|
+
const res = checkAuth(req, state.config.server.api_keys);
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
reply.code(res.status ?? 401).send({ error: res.error });
|
|
19
|
+
return; // skip done() to short-circuit the route
|
|
20
|
+
}
|
|
21
|
+
done();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// build deps from the live holder per request (never close over config/pool).
|
|
26
|
+
const depsNow = (): HandleDeps => ({
|
|
27
|
+
config: state.config,
|
|
28
|
+
pool: state.pool,
|
|
29
|
+
quota: state.quota,
|
|
30
|
+
db,
|
|
31
|
+
log: (msg) => app.log.info(msg),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.post("/v1/chat/completions", requireAuth, (req, reply) => dispatch(depsNow(), "openai", req, reply));
|
|
35
|
+
app.post("/v1/messages", requireAuth, (req, reply) => dispatch(depsNow(), "anthropic", req, reply));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SSE_HEADERS = {
|
|
39
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
40
|
+
"cache-control": "no-cache, no-transform",
|
|
41
|
+
connection: "keep-alive",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async function dispatch(
|
|
45
|
+
deps: HandleDeps,
|
|
46
|
+
clientFormat: WireFormat,
|
|
47
|
+
req: FastifyRequest,
|
|
48
|
+
reply: FastifyReply,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
// abort upstream only if the CLIENT disconnects before we finish the reply.
|
|
52
|
+
reply.raw.on("close", () => {
|
|
53
|
+
if (!reply.raw.writableFinished) controller.abort();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = await handle(deps, clientFormat, req.body, controller.signal);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e instanceof GatewayError) {
|
|
61
|
+
reply.code(e.status).send(e.payload);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
req.log.error(e);
|
|
65
|
+
reply.code(500).send({ error: "internal gateway error" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (result.sse) {
|
|
70
|
+
reply.raw.writeHead(result.status, SSE_HEADERS);
|
|
71
|
+
try {
|
|
72
|
+
for await (const bytes of result.sse) {
|
|
73
|
+
// respect backpressure: wait for drain when the socket buffer is full.
|
|
74
|
+
if (!reply.raw.write(bytes)) {
|
|
75
|
+
await new Promise((r) => reply.raw.once("drain", r));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
req.log.error(e, "stream error");
|
|
80
|
+
} finally {
|
|
81
|
+
reply.raw.end();
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
reply.code(result.status).send(result.json);
|
|
87
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK (tool-output) autodetect. Peeks the first window of a tool_result string
|
|
3
|
+
* and classifies its format so the matching filter can compress it. Patterns are
|
|
4
|
+
* tried in order; the first match wins. Returns null when nothing matches (the
|
|
5
|
+
* text is then left untouched).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ToolOutputShape = "git-diff" | "git-status" | "grep" | "tree" | "ls" | "find";
|
|
9
|
+
|
|
10
|
+
const DETECT_WINDOW = 1024;
|
|
11
|
+
|
|
12
|
+
interface Detector {
|
|
13
|
+
shape: ToolOutputShape;
|
|
14
|
+
test: (head: string) => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DETECTORS: Detector[] = [
|
|
18
|
+
// unified diff: `diff --git a/... b/...` or leading `--- ` / `+++ `
|
|
19
|
+
{ shape: "git-diff", test: (h) => /^diff --git /m.test(h) || /^--- .+\n\+\+\+ /m.test(h) },
|
|
20
|
+
// git status --porcelain: two status columns then a path
|
|
21
|
+
{ shape: "git-status", test: (h) => /^[ MADRCU?!][ MADRCU?!] \S/m.test(h) },
|
|
22
|
+
// grep -n / rg: `path:line:content` on most lines
|
|
23
|
+
{
|
|
24
|
+
shape: "grep",
|
|
25
|
+
test: (h) => {
|
|
26
|
+
const lines = h.split("\n").filter((l) => l.trim()).slice(0, 8);
|
|
27
|
+
if (lines.length < 2) return false;
|
|
28
|
+
const hits = lines.filter((l) => /^[^:\n]+:\d+:/.test(l)).length;
|
|
29
|
+
return hits >= Math.ceil(lines.length / 2);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
// tree: box-drawing branch glyphs
|
|
33
|
+
{ shape: "tree", test: (h) => /[│├└]── /.test(h) },
|
|
34
|
+
// ls -l: permission string at line start
|
|
35
|
+
{ shape: "ls", test: (h) => /^[-dlbcps][rwx-]{9}[ @.+]?\s/m.test(h) },
|
|
36
|
+
// find / plain path list: most lines look like paths, no `:line:`
|
|
37
|
+
{
|
|
38
|
+
shape: "find",
|
|
39
|
+
test: (h) => {
|
|
40
|
+
const lines = h.split("\n").filter((l) => l.trim()).slice(0, 8);
|
|
41
|
+
if (lines.length < 3) return false;
|
|
42
|
+
const paths = lines.filter((l) => /^\.?\/?[\w.-]+(\/[\w.-]+)+\/?$/.test(l.trim())).length;
|
|
43
|
+
return paths >= Math.ceil(lines.length / 2);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export function detectShape(text: string): ToolOutputShape | null {
|
|
49
|
+
if (!text) return null;
|
|
50
|
+
const head = text.slice(0, DETECT_WINDOW);
|
|
51
|
+
for (const d of DETECTORS) {
|
|
52
|
+
if (d.test(head)) return d.shape;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK per-format compressors. Each takes a tool-output string and returns a
|
|
3
|
+
* shorter equivalent, trimming redundant bulk a model rarely needs while keeping
|
|
4
|
+
* the signal. Callers apply a safety net (never empty, never larger); filters
|
|
5
|
+
* here just do the format-specific trimming.
|
|
6
|
+
*/
|
|
7
|
+
import type { ToolOutputShape } from "./detect.js";
|
|
8
|
+
|
|
9
|
+
const MAX_HUNK_LINES = 80; // per diff hunk
|
|
10
|
+
const MAX_GREP_PER_FILE = 10; // matches kept per file
|
|
11
|
+
const MAX_LIST_LINES = 200; // ls / find / tree entries
|
|
12
|
+
|
|
13
|
+
function elide(n: number, noun: string): string {
|
|
14
|
+
return `… (${n} more ${noun} elided by rtk)`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Truncate long hunks in a unified diff, keeping headers + a bounded body. */
|
|
18
|
+
function filterGitDiff(text: string): string {
|
|
19
|
+
const lines = text.split("\n");
|
|
20
|
+
const out: string[] = [];
|
|
21
|
+
let hunkBody = 0;
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.startsWith("diff --git ") || line.startsWith("@@")) {
|
|
24
|
+
hunkBody = 0;
|
|
25
|
+
out.push(line);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (/^(---|\+\+\+|index |new file|deleted file|similarity|rename) /.test(line)) {
|
|
29
|
+
out.push(line);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
hunkBody++;
|
|
33
|
+
if (hunkBody <= MAX_HUNK_LINES) out.push(line);
|
|
34
|
+
else if (hunkBody === MAX_HUNK_LINES + 1) out.push(elide(0, "hunk lines"));
|
|
35
|
+
}
|
|
36
|
+
return out.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** git status --porcelain is already terse; just cap pathological lengths. */
|
|
40
|
+
function filterGitStatus(text: string): string {
|
|
41
|
+
return capLines(text, MAX_LIST_LINES, "changes");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Cap matches per file in grep/rg output, keeping the file grouping. */
|
|
45
|
+
function filterGrep(text: string): string {
|
|
46
|
+
const perFile = new Map<string, number>();
|
|
47
|
+
const out: string[] = [];
|
|
48
|
+
let skipped = 0;
|
|
49
|
+
for (const line of text.split("\n")) {
|
|
50
|
+
const m = /^([^:\n]+):(\d+):/.exec(line);
|
|
51
|
+
if (!m) {
|
|
52
|
+
out.push(line);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const file = m[1]!;
|
|
56
|
+
const n = (perFile.get(file) ?? 0) + 1;
|
|
57
|
+
perFile.set(file, n);
|
|
58
|
+
if (n <= MAX_GREP_PER_FILE) out.push(line);
|
|
59
|
+
else skipped++;
|
|
60
|
+
}
|
|
61
|
+
if (skipped > 0) out.push(elide(skipped, "matches"));
|
|
62
|
+
return out.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function filterTree(text: string): string {
|
|
66
|
+
return capLines(text, MAX_LIST_LINES, "entries");
|
|
67
|
+
}
|
|
68
|
+
function filterLs(text: string): string {
|
|
69
|
+
return capLines(text, MAX_LIST_LINES, "entries");
|
|
70
|
+
}
|
|
71
|
+
function filterFind(text: string): string {
|
|
72
|
+
return capLines(text, MAX_LIST_LINES, "paths");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function capLines(text: string, max: number, noun: string): string {
|
|
76
|
+
const lines = text.split("\n");
|
|
77
|
+
if (lines.length <= max) return text;
|
|
78
|
+
const kept = lines.slice(0, max);
|
|
79
|
+
kept.push(elide(lines.length - max, noun));
|
|
80
|
+
return kept.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FILTERS: Record<ToolOutputShape, (t: string) => string> = {
|
|
84
|
+
"git-diff": filterGitDiff,
|
|
85
|
+
"git-status": filterGitStatus,
|
|
86
|
+
grep: filterGrep,
|
|
87
|
+
tree: filterTree,
|
|
88
|
+
ls: filterLs,
|
|
89
|
+
find: filterFind,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export function applyFilter(shape: ToolOutputShape, text: string): string {
|
|
93
|
+
return FILTERS[shape](text);
|
|
94
|
+
}
|
package/src/rtk/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTK token saver. Compresses tool-output text inside tool messages before the
|
|
3
|
+
* request is sent upstream, trimming redundant bulk (long diffs, huge grep
|
|
4
|
+
* dumps, directory listings) that inflates input tokens without adding signal.
|
|
5
|
+
*
|
|
6
|
+
* Operates on the canonical request, so it's format-agnostic: an Anthropic
|
|
7
|
+
* tool_result and an OpenAI tool message both arrive here as role="tool".
|
|
8
|
+
*
|
|
9
|
+
* Fail-open + safety net: a filtered result is only used when it's non-empty AND
|
|
10
|
+
* smaller than the original. A detector/filter that throws is swallowed and the
|
|
11
|
+
* original text kept — RTK must never break a request.
|
|
12
|
+
*/
|
|
13
|
+
import type { CanonicalMessage } from "../core/canonical.js";
|
|
14
|
+
import { detectShape } from "./detect.js";
|
|
15
|
+
import { applyFilter } from "./filters.js";
|
|
16
|
+
|
|
17
|
+
export interface RtkStats {
|
|
18
|
+
/** number of tool outputs compressed */
|
|
19
|
+
hits: number;
|
|
20
|
+
bytesIn: number;
|
|
21
|
+
bytesOut: number;
|
|
22
|
+
shapes: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compressText(text: string, stats: RtkStats): string {
|
|
26
|
+
let filtered: string;
|
|
27
|
+
try {
|
|
28
|
+
const shape = detectShape(text);
|
|
29
|
+
if (!shape) return text;
|
|
30
|
+
filtered = applyFilter(shape, text);
|
|
31
|
+
// safety: never blank the content, never grow it
|
|
32
|
+
if (!filtered || filtered.length >= text.length) return text;
|
|
33
|
+
|
|
34
|
+
stats.hits++;
|
|
35
|
+
stats.bytesIn += text.length;
|
|
36
|
+
stats.bytesOut += filtered.length;
|
|
37
|
+
stats.shapes.push(shape);
|
|
38
|
+
return filtered;
|
|
39
|
+
} catch {
|
|
40
|
+
// fail-open: a buggy filter must not break the request
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compress tool-output content in place. Returns stats (hits=0 when nothing was
|
|
47
|
+
* compressible). Only touches role="tool" messages with string content.
|
|
48
|
+
*/
|
|
49
|
+
export function compressMessages(messages: CanonicalMessage[]): RtkStats {
|
|
50
|
+
const stats: RtkStats = { hits: 0, bytesIn: 0, bytesOut: 0, shapes: [] };
|
|
51
|
+
for (const msg of messages) {
|
|
52
|
+
if (msg.role !== "tool") continue;
|
|
53
|
+
if (typeof msg.content === "string") {
|
|
54
|
+
msg.content = compressText(msg.content, stats);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return stats;
|
|
58
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { registerRoutes } from "./routes/index.js";
|
|
5
|
+
import { GatewayState } from "./core/state.js";
|
|
6
|
+
import { UsageDB } from "./db.js";
|
|
7
|
+
import { QuotaTracker } from "./core/quota.js";
|
|
8
|
+
import { AuthStore } from "./core/authStore.js";
|
|
9
|
+
import { consoleBuffer } from "./core/console-buffer.js";
|
|
10
|
+
|
|
11
|
+
async function main(): Promise<void> {
|
|
12
|
+
const configPath = resolve(process.env.AIGETWEY_CONFIG ?? "config.yaml");
|
|
13
|
+
|
|
14
|
+
let config;
|
|
15
|
+
try {
|
|
16
|
+
config = loadConfig(configPath);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error((e as Error).message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tee pino's output into the console buffer for the dashboard's live SSE viewer.
|
|
23
|
+
// pino (sonic-boom) writes straight to fd 1, bypassing process.stdout.write — so
|
|
24
|
+
// we hand it an explicit destination stream instead of patching stdout, otherwise
|
|
25
|
+
// app.log.* lines never reach the buffer and the Server Console looks dead.
|
|
26
|
+
const logStream = {
|
|
27
|
+
write(line: string): boolean {
|
|
28
|
+
process.stdout.write(line);
|
|
29
|
+
for (const raw of line.split("\n")) {
|
|
30
|
+
if (!raw.trim()) continue;
|
|
31
|
+
try {
|
|
32
|
+
const o = JSON.parse(raw) as { level?: number; msg?: unknown; reqId?: string };
|
|
33
|
+
const lvl =
|
|
34
|
+
(o.level ?? 30) >= 50 ? "ERROR" : (o.level ?? 30) >= 40 ? "WARN" : (o.level ?? 30) >= 20 ? "INFO" : "DEBUG";
|
|
35
|
+
const msg = typeof o.msg === "string" ? o.msg : o.msg !== undefined ? JSON.stringify(o.msg) : raw.trim();
|
|
36
|
+
consoleBuffer.push(lvl, o.reqId ? `[${o.reqId}] ${msg}` : msg);
|
|
37
|
+
} catch {
|
|
38
|
+
consoleBuffer.push("LOG", raw.trim());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const app = Fastify({
|
|
46
|
+
logger: { level: process.env.LOG_LEVEL ?? "info", stream: logStream },
|
|
47
|
+
// gateway proxies large prompts; raise the JSON body cap.
|
|
48
|
+
bodyLimit: 32 * 1024 * 1024,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// unified data dir (default ./data); usage tracking lives here.
|
|
52
|
+
const dataDir = resolve(process.env.AIGETWEY_DATA_DIR ?? "data");
|
|
53
|
+
const db = new UsageDB(join(dataDir, "usage.sqlite"));
|
|
54
|
+
|
|
55
|
+
// quota counts persist via the DB so a restart within a window keeps the budget.
|
|
56
|
+
const quota = new QuotaTracker(Date.now, {
|
|
57
|
+
load: () => db.loadQuota(),
|
|
58
|
+
save: (id, start, consumed) => db.saveQuota(id, start, consumed),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// holder enables runtime config edits (hot-reload) from the dashboard.
|
|
62
|
+
const state = new GatewayState(configPath, config, quota);
|
|
63
|
+
// admin password lives in a hash store (seeded from the env on first run,
|
|
64
|
+
// changeable at runtime from the dashboard).
|
|
65
|
+
const auth = AuthStore.open(dataDir, process.env.AIGETWEY_ADMIN_PASSWORD);
|
|
66
|
+
|
|
67
|
+
registerRoutes(app, state, db, auth);
|
|
68
|
+
|
|
69
|
+
// Single-URL mode: when the launcher runs the dashboard on an internal port,
|
|
70
|
+
// reverse-proxy everything the gateway doesn't own (the UI, /api/gw, /_next…)
|
|
71
|
+
// to it. The API routes above (/v1, /admin, /health) are more specific than the
|
|
72
|
+
// proxy's catch-all, so client traffic stays direct on Fastify — only the
|
|
73
|
+
// low-traffic dashboard is proxied. One address serves both.
|
|
74
|
+
const dashUpstream = process.env.AIGETWEY_DASHBOARD_PORT;
|
|
75
|
+
if (dashUpstream) {
|
|
76
|
+
await app.register(import("@fastify/http-proxy"), {
|
|
77
|
+
upstream: `http://127.0.0.1:${dashUpstream}`,
|
|
78
|
+
prefix: "/",
|
|
79
|
+
// forward the whole HTTP surface the dashboard needs (pages + its API).
|
|
80
|
+
httpMethods: ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
|
|
81
|
+
// keep the ORIGINAL Host so Next builds redirects (e.g. → /login) against
|
|
82
|
+
// the gateway's address, not the internal dashboard port.
|
|
83
|
+
replyOptions: {
|
|
84
|
+
rewriteRequestHeaders: (req, headers) => ({ ...headers, host: req.headers.host ?? headers.host }),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const close = () => {
|
|
90
|
+
db.close();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
};
|
|
93
|
+
process.on("SIGINT", close);
|
|
94
|
+
process.on("SIGTERM", close);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// AIGETWEY_PORT (set by the CLI launcher) overrides the config port so the
|
|
98
|
+
// launcher can pin the gateway port without editing config.yaml.
|
|
99
|
+
const port = process.env.AIGETWEY_PORT ? Number(process.env.AIGETWEY_PORT) : config.server.port;
|
|
100
|
+
await app.listen({ host: config.server.host, port });
|
|
101
|
+
app.log.info(`aigetwey listening on http://${config.server.host}:${port}`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
app.log.error(e);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
main();
|