Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ body:
id: version
attributes:
label: ReRouted version
placeholder: "For example: 0.4.2"
placeholder: "Shown in Settings under Application"
validations:
required: true
- type: input
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/question.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ body:
id: version
attributes:
label: ReRouted version
placeholder: "For example: 0.4.2"
placeholder: "Shown in Settings under Application"
validations:
required: true
- type: input
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ReRouted is an independent project and is not affiliated with or endorsed by any

### 1. Install

[Download ReRouted 0.4.2 for Apple Silicon](https://github.com/gitcommit90/rerouted/releases/download/v0.4.2/ReRouted-0.4.2-arm64.dmg), open the DMG, and drag ReRouted to Applications.
[Download ReRouted 0.4.3 for Apple Silicon](https://github.com/gitcommit90/rerouted/releases/download/v0.4.3/ReRouted-0.4.3-arm64.dmg), open the DMG, and drag ReRouted to Applications.

ReRouted requires Apple Silicon and macOS 12 Monterey or newer.

Expand Down Expand Up @@ -200,7 +200,7 @@ The implementation is intentionally small: Electron, Node's built-in HTTP server

## Current release

ReRouted `0.4.2` ships for Apple Silicon macOS with hardened OAuth callbacks and renderer lock boundaries, uncapped SQLite usage history, 60-second quota refreshes while the Quota page is open, masked account identities, and image inputs in chat completions. Public builds are Developer ID signed, notarized, stapled, and distributed through stable GitHub Releases with in-app updates. The public API is intentionally limited to health, model discovery, and chat completions; a published third-party client compatibility matrix is still forthcoming.
ReRouted `0.4.3` fixes compact usage metrics so billion and trillion totals display with `B` and `T` units instead of continuing to grow in millions. It includes the hardened OAuth callbacks and renderer lock boundaries, uncapped SQLite usage history, 60-second quota refreshes while the Quota page is open, masked account identities, and image inputs introduced in `0.4.2`. Public builds are Developer ID signed, notarized, stapled, and distributed through stable GitHub Releases with in-app updates. The public API is intentionally limited to health, model discovery, and chat completions; a published third-party client compatibility matrix is still forthcoming.

ReRouted is an independent personal project.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rerouted",
"version": "0.4.2",
"version": "0.4.3",
"description": "A macOS menu-bar router for accounts, models, and automatic fallback.",
"author": "gitcommit90",
"homepage": "https://rerouted.dev",
Expand Down
28 changes: 24 additions & 4 deletions scripts/capture-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ function demoUsage(period = "24h") {
requests: 148,
ok: 143,
errors: 5,
prompt_tokens: 184_200,
completion_tokens: 61_480,
cached_tokens: 92_600,
total_tokens: 338_280,
prompt_tokens: 1_486_000_000,
completion_tokens: 661_000_000,
cached_tokens: 483_648,
total_tokens: 2_147_483_648,
byModel: [
{ model: "coding", requests: 92, prompt_tokens: 121_000, completion_tokens: 42_300 },
{ model: "chatgpt/gpt-5", requests: 38, prompt_tokens: 45_800, completion_tokens: 13_100 },
Expand Down Expand Up @@ -543,6 +543,26 @@ app.whenReady().then(async () => {
settings: ".settings-group",
}[p] || "#view > *";
await capture(`app-${p}.png`, selector);
if (p === "home") {
await win.webContents.executeJavaScript(`
(() => {
const value = document.querySelector("[data-home-tokens]")?.textContent.trim();
if (value !== "2.1B") throw new Error("Unexpected billion token format: " + value);
return true;
})()
`);
}
if (p === "stats") {
await win.webContents.executeJavaScript(`
(() => {
const metrics = [...document.querySelectorAll(".metric")];
const tokens = metrics.find((metric) => metric.querySelector(".metric-label")?.textContent.trim() === "Tokens");
const value = tokens?.querySelector(".metric-value")?.textContent.trim();
if (value !== "2.1B") throw new Error("Unexpected activity token format: " + value);
return true;
})()
`);
}
if (p === "providers") {
await win.webContents.executeJavaScript(`
(() => {
Expand Down
8 changes: 1 addition & 7 deletions src/renderer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const api = window.rerouted;
const { accountDisplayName, accountIdentityLabel, maskAccountEmail } =
window.ReroutedAccountIdentity;
const { compactNumber: fmtNum } = window.ReroutedNumberFormat;
const { createLatestRequestGate, guardSensitiveRender } = window.ReroutedRendererLockState;
const $ = (sel, el = document) => el.querySelector(sel);
const view = $("#view");
Expand Down Expand Up @@ -800,13 +801,6 @@ function fmtTime(at) {
}
}

function fmtNum(n) {
const x = Number(n) || 0;
if (x >= 1_000_000) return (x / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
if (x >= 10_000) return (x / 1000).toFixed(1).replace(/\.0$/, "") + "k";
return x.toLocaleString();
}

function fmtRelativeTime(at) {
if (!at) return "Never";
const seconds = Math.max(0, Math.round((Date.now() - Number(at)) / 1000));
Expand Down
1 change: 1 addition & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
</nav>
</div>
<script src="account-identity.js"></script>
<script src="number-format.js"></script>
<script src="lock-state.js"></script>
<script src="app.js"></script>
</body>
Expand Down
34 changes: 34 additions & 0 deletions src/renderer/number-format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use strict";

(function exposeNumberFormat(root) {
const UNITS = [
{ value: 1_000, suffix: "k" },
{ value: 1_000_000, suffix: "M" },
{ value: 1_000_000_000, suffix: "B" },
{ value: 1_000_000_000_000, suffix: "T" },
];

function compactNumber(value) {
const parsed = Number(value);
const number = Number.isFinite(parsed) ? parsed : 0;
const absolute = Math.abs(number);
if (absolute < 10_000) return number.toLocaleString("en-US");

let unitIndex = 0;
while (unitIndex < UNITS.length - 1 && absolute >= UNITS[unitIndex + 1].value) {
unitIndex += 1;
}

let unit = UNITS[unitIndex];
let rounded = Number((number / unit.value).toFixed(1));
if (Math.abs(rounded) >= 1_000 && unitIndex < UNITS.length - 1) {
unit = UNITS[++unitIndex];
rounded = Number((number / unit.value).toFixed(1));
}
return `${rounded.toFixed(1).replace(/\.0$/, "")}${unit.suffix}`;
}

const api = { compactNumber };
if (typeof module !== "undefined" && module.exports) module.exports = api;
if (root) root.ReroutedNumberFormat = api;
})(typeof window !== "undefined" ? window : null);
37 changes: 37 additions & 0 deletions tests/number-format.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use strict";

const assert = require("node:assert/strict");
const { describe, it } = require("node:test");
const { compactNumber } = require("../src/renderer/number-format");

describe("compact usage number formatting", () => {
it("uses the expected suffix at each magnitude", () => {
assert.equal(compactNumber(0), "0");
assert.equal(compactNumber(9_999), "9,999");
assert.equal(compactNumber(10_000), "10k");
assert.equal(compactNumber(12_345), "12.3k");
assert.equal(compactNumber(1_000_000), "1M");
assert.equal(compactNumber(12_345_678), "12.3M");
assert.equal(compactNumber(1_000_000_000), "1B");
assert.equal(compactNumber(12_345_678_901), "12.3B");
assert.equal(compactNumber(1_000_000_000_000), "1T");
assert.equal(compactNumber(12_345_678_901_234), "12.3T");
});

it("promotes values that round into the next unit", () => {
assert.equal(compactNumber(999_949), "999.9k");
assert.equal(compactNumber(999_950), "1M");
assert.equal(compactNumber(999_949_999), "999.9M");
assert.equal(compactNumber(999_950_000), "1B");
assert.equal(compactNumber(999_949_999_999), "999.9B");
assert.equal(compactNumber(999_950_000_000), "1T");
});

it("handles exact, negative, and invalid values", () => {
assert.equal(compactNumber(2_000_000_000), "2B");
assert.equal(compactNumber(-2_500_000_000), "-2.5B");
assert.equal(compactNumber("1234567890"), "1.2B");
assert.equal(compactNumber(Number.NaN), "0");
assert.equal(compactNumber(Number.POSITIVE_INFINITY), "0");
});
});
Loading