Skip to content

Commit b5e3b81

Browse files
committed
Merge live request routing visualization
2 parents 5c8a50b + c559c05 commit b5e3b81

13 files changed

Lines changed: 809 additions & 60 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rerouted",
3-
"version": "0.4.10",
3+
"version": "0.4.11",
44
"description": "A macOS menu-bar router for accounts, models, and automatic fallback.",
55
"author": "gitcommit90",
66
"license": "MIT",

scripts/capture-ui.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,31 @@ function demoUsage(period = "24h") {
105105
};
106106
}
107107

108+
function demoActiveRequests() {
109+
return [
110+
{
111+
id: "request-demo-chatgpt",
112+
model: "coding",
113+
stream: true,
114+
startedAt: demoStartedAt - 4_200,
115+
providerId: "prov_chatgpt_demo",
116+
providerType: "chatgpt",
117+
providerName: "ChatGPT Plus",
118+
upstreamModel: "gpt-5",
119+
},
120+
{
121+
id: "request-demo-claude",
122+
model: "claude/claude-sonnet-4-5",
123+
stream: true,
124+
startedAt: demoStartedAt - 1_800,
125+
providerId: "prov_claude_demo",
126+
providerType: "claude",
127+
providerName: "Claude Pro",
128+
upstreamModel: "claude-sonnet-4-5",
129+
},
130+
];
131+
}
132+
108133
let demoLogEntries = [
109134
{ at: Date.now() - 35_000, level: "info", msg: "Gateway request completed", meta: { route: "coding", status: 200 } },
110135
{ at: Date.now() - 7 * 60_000, level: "info", msg: "OAuth token refreshed", meta: { provider: "ChatGPT Plus" } },
@@ -242,6 +267,7 @@ function registerIpc() {
242267
combos: (cfg.combos || []).map(publicCombo),
243268
stats: demoStats(),
244269
usage: demoUsage(),
270+
activeRequests: demoActiveRequests(),
245271
unlocked: true,
246272
hasAdminPassword: false,
247273
oauthProviders: Object.keys(OAUTH).map((k) => ({ id: k, name: OAUTH[k].name })),
@@ -709,18 +735,22 @@ app.whenReady().then(async () => {
709735
await win.webContents.executeJavaScript(`
710736
(() => {
711737
const details = document.querySelector("[data-home-credentials]");
712-
const routeMap = document.querySelector("[data-home-route-map]");
713-
const track = routeMap?.querySelector(".route-track");
738+
const routeStage = document.querySelector("[data-live-router-stage]");
739+
const path = routeStage?.querySelector(".live-request-path");
714740
const copyButton = document.getElementById("copy-url");
715-
if (!details || !routeMap || !track || !copyButton) {
741+
if (
742+
!details ||
743+
!routeStage ||
744+
!path ||
745+
routeStage.querySelectorAll(".live-request-path").length !== 2 ||
746+
routeStage.querySelectorAll(".live-provider").length !== 5 ||
747+
!copyButton
748+
) {
716749
throw new Error("Status persistence controls did not render");
717750
}
718751
details.open = true;
719752
copyButton.focus();
720-
window.__rr_home_poll_test = { routeMap, track, copyButton, animationStarts: 0 };
721-
routeMap.addEventListener("animationstart", () => {
722-
window.__rr_home_poll_test.animationStarts += 1;
723-
});
753+
window.__rr_home_poll_test = { routeStage, path, copyButton };
724754
return true;
725755
})()
726756
`);
@@ -729,16 +759,13 @@ app.whenReady().then(async () => {
729759
(() => {
730760
const test = window.__rr_home_poll_test;
731761
const details = document.querySelector("[data-home-credentials]");
732-
if (document.querySelector("[data-home-route-map] .route-track") !== test.track) {
733-
throw new Error("Status polling replaced the route animation DOM");
762+
if (document.querySelector("[data-live-router-stage] .live-request-path") !== test.path) {
763+
throw new Error("Status polling replaced an unchanged live request path");
734764
}
735765
if (!details?.open) throw new Error("Status polling collapsed Credentials and network");
736766
if (document.activeElement !== test.copyButton) {
737767
throw new Error("Status polling moved focus away from the endpoint controls");
738768
}
739-
if (test.animationStarts !== 0) {
740-
throw new Error("Status polling restarted the route animation without new traffic");
741-
}
742769
return true;
743770
})()
744771
`);

src/lib/gateway.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function createGateway({
1717
port = DEFAULT_PORT,
1818
host = "127.0.0.1",
1919
maxBodyBytes = MAX_JSON_BODY_BYTES,
20+
requestActivity,
2021
} = {}) {
2122
let server = null;
2223
let listeningPort = null;
@@ -202,15 +203,27 @@ function createGateway({
202203
model: body.model,
203204
stream: !!body.stream,
204205
});
206+
const activityId = requestActivity?.begin({
207+
model: body.model,
208+
stream: !!body.stream,
209+
});
210+
let activityStatus = 500;
211+
let activityOutcome = "error";
205212
const clientAbort = new AbortController();
206213
const onClientAbort = () => {
207214
if (!clientAbort.signal.aborted) clientAbort.abort(new Error("Client disconnected"));
208215
};
209216
req.once("aborted", onClientAbort);
210217
res.once("close", onClientAbort);
211218
try {
212-
const result = await router.chatCompletions({ body, signal: clientAbort.signal });
219+
const result = await router.chatCompletions({
220+
body,
221+
signal: clientAbort.signal,
222+
onProviderSelected: (provider) => requestActivity?.route(activityId, provider),
223+
});
213224
if (!result.ok) {
225+
activityStatus = result.status || 502;
226+
activityOutcome = result.status === 499 ? "canceled" : "error";
214227
logger.error("chat/completions failed", {
215228
model: body.model,
216229
status: result.status,
@@ -235,7 +248,11 @@ function createGateway({
235248
});
236249
try {
237250
await result.streamPipe(res);
251+
activityStatus = 200;
252+
activityOutcome = "success";
238253
} catch (e) {
254+
activityStatus = clientAbort.signal.aborted ? 499 : 502;
255+
activityOutcome = clientAbort.signal.aborted ? "canceled" : "error";
239256
if (!res.writableEnded) {
240257
res.write(
241258
`data: ${JSON.stringify({ error: { message: e.message } })}\n\n`
@@ -248,10 +265,16 @@ function createGateway({
248265

249266
res.writeHead(200, { "Content-Type": "application/json" });
250267
res.end(JSON.stringify(result.openAiJson));
268+
activityStatus = 200;
269+
activityOutcome = "success";
251270
return;
252271
} finally {
253272
req.removeListener("aborted", onClientAbort);
254273
res.removeListener("close", onClientAbort);
274+
requestActivity?.end(activityId, {
275+
status: activityStatus,
276+
outcome: activityOutcome,
277+
});
255278
}
256279
}
257280

src/lib/request-activity.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use strict";
2+
3+
const { randomUUID } = require("node:crypto");
4+
5+
function createRequestActivity({ now = Date.now, idFactory = randomUUID } = {}) {
6+
const active = new Map();
7+
const listeners = new Set();
8+
9+
function snapshot() {
10+
return [...active.values()].map((request) => ({ ...request }));
11+
}
12+
13+
function publish(type, request) {
14+
const event = { type, request: { ...request }, active: snapshot() };
15+
for (const listener of listeners) {
16+
try {
17+
listener(event);
18+
} catch {
19+
// Activity is decorative telemetry and must never affect request routing.
20+
}
21+
}
22+
}
23+
24+
function begin({ model, stream = false } = {}) {
25+
const request = {
26+
id: idFactory(),
27+
model: String(model || ""),
28+
stream: !!stream,
29+
startedAt: now(),
30+
providerId: null,
31+
providerType: null,
32+
providerName: null,
33+
};
34+
active.set(request.id, request);
35+
publish("started", request);
36+
return request.id;
37+
}
38+
39+
function route(id, provider = {}) {
40+
const current = active.get(id);
41+
if (!current) return false;
42+
const request = {
43+
...current,
44+
providerId: provider.providerId || null,
45+
providerType: provider.providerType || null,
46+
providerName: provider.providerName || null,
47+
upstreamModel: provider.upstreamModel || provider.model || null,
48+
routedAt: now(),
49+
};
50+
active.set(id, request);
51+
publish("routed", request);
52+
return true;
53+
}
54+
55+
function end(id, { status = 200, outcome = "success" } = {}) {
56+
const current = active.get(id);
57+
if (!current) return false;
58+
active.delete(id);
59+
publish("finished", {
60+
...current,
61+
status,
62+
outcome,
63+
finishedAt: now(),
64+
});
65+
return true;
66+
}
67+
68+
function subscribe(listener) {
69+
if (typeof listener !== "function") return () => {};
70+
listeners.add(listener);
71+
return () => listeners.delete(listener);
72+
}
73+
74+
return { begin, route, end, snapshot, subscribe };
75+
}
76+
77+
module.exports = { createRequestActivity };

src/lib/router.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
880880
}
881881
}
882882

883-
async function chatCompletions({ body, signal } = {}) {
883+
async function chatCompletions({ body, signal, onProviderSelected } = {}) {
884884
const cfg = getConfig();
885885
const modelId = body.model;
886886
const stream = !!body.stream;
@@ -977,6 +977,19 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
977977
}
978978

979979
const attemptMember = { ...member, provider };
980+
if (typeof onProviderSelected === "function") {
981+
try {
982+
onProviderSelected({
983+
providerId: provider.id,
984+
providerType: canonicalProviderType(provider.type),
985+
providerName: provider.name,
986+
accountAlias: provider.accountAlias || null,
987+
upstreamModel: member.upstreamModel,
988+
});
989+
} catch {
990+
// UI telemetry must remain isolated from routing.
991+
}
992+
}
980993
let result;
981994
try {
982995
result = await tryMember(attemptMember, body, stream, signal);

src/main.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
const { createStore } = require("./lib/store");
2020
const { createRouter } = require("./lib/router");
2121
const { createGateway } = require("./lib/gateway");
22+
const { createRequestActivity } = require("./lib/request-activity");
2223
const { createUsageStore, hydrateUsageIdentity } = require("./lib/usage");
2324
const logger = require("./lib/logger");
2425
const { hashPassword, verifyPassword, generateId, generateApiKey } = require("./lib/password");
@@ -75,8 +76,9 @@ logger.info("ReRouted starting", { userData, logPath });
7576

7677
const store = createStore(configPath);
7778
const usage = createUsageStore(usagePath, { legacyPath: legacyUsagePath });
79+
const requestActivity = createRequestActivity();
7880
const router = createRouter({ store, usage });
79-
const gateway = createGateway({ store, router });
81+
const gateway = createGateway({ store, router, requestActivity });
8082
const sessionAuth = createSessionAuth();
8183
const quota = createQuotaService({ store, refreshProvider: refreshProviderForQuota });
8284

@@ -87,6 +89,15 @@ let detectedCache = [];
8789
let updateService = null;
8890
let updatePromptShown = false;
8991

92+
requestActivity.subscribe((activity) => {
93+
const cfg = store.load();
94+
const canPublish =
95+
!hasAdminPassword(cfg) || sessionAuth.isUnlocked(true) || harnessModeEnabled();
96+
if (canPublish && panel && !panel.isDestroyed()) {
97+
panel.webContents.send("app:request-activity", activity);
98+
}
99+
});
100+
90101
function publishUpdateState(update) {
91102
if (panel && !panel.isDestroyed()) {
92103
panel.webContents.send("app:update-state", update);
@@ -387,6 +398,7 @@ function registerIpc() {
387398
combos: (cfg.combos || []).map(publicCombo),
388399
stats: publicStats(router.stats(), cfg),
389400
usage: publicUsage(router.usageAggregate("24h"), cfg),
401+
activeRequests: requestActivity.snapshot(),
390402
unlocked,
391403
hasAdminPassword: hasPassword,
392404
oauthProviders: Object.keys(OAUTH).map((k) => ({

src/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const EVENT_CHANNELS = new Set([
5454
"app:update-state",
5555
"app:session-lock-changed",
5656
"app:provider-identities-updated",
57+
"app:request-activity",
5758
"app:open-settings",
5859
]);
5960

0 commit comments

Comments
 (0)