feat(KB-152): add Minimax and Zai providers with usage pace indicators

- Add Minimax provider backend with full test coverage
- Add Zai (Zhipu AI) provider backend for additional model options
- Implement pace calculation in UsageWindow with ahead/behind/on-pace detection
- Update UsageIndicator frontend component to consume backend pace data
- Fix pace indicator colors and icons to match design specification
- Resolve ListView QuickEntryBox type error for type safety
This commit is contained in:
gsxdsm
2026-03-30 13:45:52 -07:00
parent f78f01ac5b
commit 7e1fdfeaf2
8 changed files with 826 additions and 61 deletions

View File

@@ -3,6 +3,7 @@ import {
fetchAllProviderUsage,
clearUsageCache,
ProviderUsage,
calculatePace,
} from "./usage.js";
// Mock the https module
@@ -38,10 +39,12 @@ describe("usage", () => {
const providers = await fetchAllProviderUsage();
expect(providers).toHaveLength(3);
expect(providers).toHaveLength(5);
expect(providers.map((p) => p.name)).toContain("Claude");
expect(providers.map((p) => p.name)).toContain("Codex");
expect(providers.map((p) => p.name)).toContain("Gemini");
expect(providers.map((p) => p.name)).toContain("Minimax");
expect(providers.map((p) => p.name)).toContain("Zai");
// All should be no-auth status
for (const p of providers) {
@@ -76,7 +79,7 @@ describe("usage", () => {
// Should be different array reference
expect(second).not.toBe(first);
expect(second).toHaveLength(3);
expect(second).toHaveLength(5);
});
});
@@ -406,6 +409,463 @@ describe("usage", () => {
});
});
describe("Minimax provider", () => {
it("detects no auth when credentials file doesn't exist", async () => {
mockReadFileSync.mockImplementation(() => {
throw new Error("File not found");
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax");
expect(minimax).toBeDefined();
expect(minimax!.status).toBe("no-auth");
expect(minimax!.error).toContain("No Minimax credentials");
});
it("detects no auth when access_token is missing", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("minimax")) {
return JSON.stringify({
// missing access_token
refresh_token: "test-refresh",
});
}
throw new Error("File not found");
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax");
expect(minimax!.status).toBe("no-auth");
expect(minimax!.error).toContain("No Minimax access token");
});
it("parses usage data from API response", async () => {
const mockResponse = {
quota: {
total: 1000,
used: 350,
remaining: 650,
},
reset_at: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days
};
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("minimax")) {
return JSON.stringify({
access_token: "test-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
expect(minimax.windows).toHaveLength(1);
const weeklyWindow = minimax.windows[0];
expect(weeklyWindow.label).toBe("Weekly");
expect(weeklyWindow.percentUsed).toBe(35); // 350/1000 * 100
expect(weeklyWindow.percentLeft).toBe(65);
expect(weeklyWindow.resetText).toContain("resets in");
expect(weeklyWindow.resetMs).toBeDefined();
expect(weeklyWindow.windowDurationMs).toBe(7 * 24 * 60 * 60 * 1000);
});
it("handles 401 auth error", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("minimax")) {
return JSON.stringify({
access_token: "expired-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "unauthorized"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("error");
expect(minimax.error).toContain("Auth expired");
});
it("handles 403 auth error", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("minimax")) {
return JSON.stringify({
access_token: "forbidden-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "forbidden"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("error");
expect(minimax.error).toContain("Auth expired");
});
});
describe("Zai provider", () => {
it("detects no auth when credentials file doesn't exist", async () => {
mockReadFileSync.mockImplementation(() => {
throw new Error("File not found");
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai");
expect(zai).toBeDefined();
expect(zai!.status).toBe("no-auth");
expect(zai!.error).toContain("No Zai credentials");
});
it("detects no auth when access_token is missing", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("zai")) {
return JSON.stringify({
// missing access_token
refresh_token: "test-refresh",
});
}
throw new Error("File not found");
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai");
expect(zai!.status).toBe("no-auth");
expect(zai!.error).toContain("No Zai access token");
});
it("parses daily usage data from API response", async () => {
const mockResponse = {
data: {
total_credits: 10000,
used_credits: 2500,
reset_date: new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString(), // 8 hours (daily)
},
};
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("zai")) {
return JSON.stringify({
access_token: "test-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
expect(zai.windows).toHaveLength(1);
const dailyWindow = zai.windows[0];
expect(dailyWindow.label).toBe("Daily");
expect(dailyWindow.percentUsed).toBe(25); // 2500/10000 * 100
expect(dailyWindow.percentLeft).toBe(75);
expect(dailyWindow.resetText).toContain("resets in");
expect(dailyWindow.resetMs).toBeDefined();
expect(dailyWindow.windowDurationMs).toBe(24 * 60 * 60 * 1000);
});
it("parses monthly usage data from API response", async () => {
const mockResponse = {
data: {
total_credits: 10000,
used_credits: 5000,
reset_date: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), // 15 days (monthly)
},
};
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("zai")) {
return JSON.stringify({
access_token: "test-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
expect(zai.windows).toHaveLength(2);
const monthlyWindow = zai.windows[1];
expect(monthlyWindow.label).toBe("Monthly");
expect(monthlyWindow.percentUsed).toBe(50); // 5000/10000 * 100
expect(monthlyWindow.percentLeft).toBe(50);
expect(monthlyWindow.resetText).toContain("resets in");
expect(monthlyWindow.resetMs).toBeDefined();
expect(monthlyWindow.windowDurationMs).toBe(30 * 24 * 60 * 60 * 1000);
});
it("handles 401 auth error", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("zai")) {
return JSON.stringify({
access_token: "expired-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "unauthorized"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("error");
expect(zai.error).toContain("Auth expired");
});
it("handles 403 auth error", async () => {
mockReadFileSync.mockImplementation((path: string) => {
if (path.includes("zai")) {
return JSON.stringify({
access_token: "forbidden-token",
});
}
throw new Error("File not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "forbidden"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("error");
expect(zai.error).toContain("Auth expired");
});
});
describe("calculatePace helper", () => {
it("returns ahead status when usage exceeds elapsed time by >5%", () => {
// 70% used, 50% elapsed = 20% ahead (3 days remaining out of 7 = 57% elapsed, 70 - 57 = 13 > 5)
// Actually: 100 - (3/7 * 100) = 57.14% elapsed
// 70 - 57.14 = 12.86% > 5% → ahead
const pace = calculatePace(70, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("ahead");
expect(pace!.percentElapsed).toBe(57);
expect(pace!.message).toContain("over pace");
});
it("returns behind status when usage is under elapsed time by >5%", () => {
// 20% used, 57% elapsed = 37% behind
const pace = calculatePace(20, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("behind");
expect(pace!.percentElapsed).toBe(57);
expect(pace!.message).toContain("under pace");
});
it("returns on-track status when within 5% of elapsed time", () => {
// 52% used, 57% elapsed = 5% difference (within threshold)
const pace = calculatePace(52, 3.5 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("on-track");
expect(pace!.message).toBe("On pace with time elapsed");
});
it("returns undefined when resetMs is undefined", () => {
const pace = calculatePace(50, undefined, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeUndefined();
});
it("returns undefined when windowDurationMs is undefined", () => {
const pace = calculatePace(50, 3 * 24 * 60 * 60 * 1000, undefined);
expect(pace).toBeUndefined();
});
it("returns undefined when resetMs is 0 or negative", () => {
expect(calculatePace(50, 0, 7 * 24 * 60 * 60 * 1000)).toBeUndefined();
expect(calculatePace(50, -1000, 7 * 24 * 60 * 60 * 1000)).toBeUndefined();
});
it("returns undefined when windowDurationMs is 0 or negative", () => {
expect(calculatePace(50, 3 * 24 * 60 * 60 * 1000, 0)).toBeUndefined();
expect(calculatePace(50, 3 * 24 * 60 * 60 * 1000, -1000)).toBeUndefined();
});
it("clamps percentUsed to 0-100 range", () => {
// Test with negative percentUsed
let pace = calculatePace(-10, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("behind");
// Test with percentUsed > 100
pace = calculatePace(150, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("ahead");
});
});
describe("error handling", () => {
it("handles network errors gracefully", async () => {
mockReadFileSync.mockImplementation((path: string) => {

View File

@@ -2,6 +2,15 @@ import * as fs from "node:fs";
import * as path from "node:path";
import * as https from "node:https";
/**
* Pace information for weekly usage windows
*/
export interface UsagePace {
status: "ahead" | "on-track" | "behind";
percentElapsed: number; // 0-100
message: string;
}
/**
* Usage window for a provider (e.g., "Session (5h)", "Weekly")
*/
@@ -12,6 +21,7 @@ export interface UsageWindow {
resetText: string | null; // e.g., "resets in 2h"
resetMs?: number; // ms until reset
windowDurationMs?: number; // total window length
pace?: UsagePace; // pace indicator for weekly windows
}
/**
@@ -44,6 +54,77 @@ interface CacheEntry {
let usageCache: CacheEntry | null = null;
const CACHE_TTL_MS = 30_000; // 30 seconds
// Pace threshold - matches frontend UsageIndicator.tsx
const PACE_THRESHOLD = 5; // 5% threshold for "on pace"
/**
* Calculate pace information for a usage window.
* Returns undefined if pace cannot be calculated (e.g., missing timing data or window reset).
*/
export function calculatePace(
percentUsed: number,
resetMs: number | undefined,
windowDurationMs: number | undefined
): UsagePace | undefined {
// Validate inputs
if (resetMs === undefined || windowDurationMs === undefined) {
return undefined;
}
// Window already reset or invalid duration
if (resetMs <= 0 || windowDurationMs <= 0) {
return undefined;
}
// Clamp percentUsed to valid range
const clampedPercentUsed = Math.min(100, Math.max(0, percentUsed));
// Calculate percent of time elapsed in the window
// percentElapsed = 100 - (remainingTime / totalTime * 100)
const percentElapsed = 100 - (resetMs / windowDurationMs * 100);
// Calculate delta between usage and elapsed time
const paceDelta = clampedPercentUsed - percentElapsed;
// Determine status based on threshold
if (paceDelta > PACE_THRESHOLD) {
return {
status: "ahead",
percentElapsed: Math.round(percentElapsed),
message: `Using ${Math.abs(Math.round(paceDelta))}% over pace`,
};
} else if (paceDelta < -PACE_THRESHOLD) {
return {
status: "behind",
percentElapsed: Math.round(percentElapsed),
message: `Using ${Math.abs(Math.round(paceDelta))}% under pace`,
};
} else {
return {
status: "on-track",
percentElapsed: Math.round(percentElapsed),
message: "On pace with time elapsed",
};
}
}
/**
* Apply pace calculation to a usage window if applicable.
* Only applies to weekly windows with valid timing data.
*/
function applyPaceToWindow(window: UsageWindow): UsageWindow {
// Only apply pace to weekly windows
if (!window.label.toLowerCase().includes("weekly")) {
return window;
}
const pace = calculatePace(window.percentUsed, window.resetMs, window.windowDurationMs);
if (pace) {
return { ...window, pace };
}
return window;
}
/**
* Format duration in milliseconds to human-readable string
*/
@@ -484,6 +565,203 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
return usage;
}
// ── Minimax fetcher ─────────────────────────────────────────────────────────
async function fetchMinimaxUsage(): Promise<ProviderUsage> {
const usage: ProviderUsage = {
name: "Minimax",
icon: "🟣",
status: "no-auth",
windows: [],
};
// Load Minimax credentials
const credPath = path.join(process.env.HOME || "~", ".minimax", "credentials.json");
let creds: any = null;
try {
creds = JSON.parse(fs.readFileSync(credPath, "utf-8"));
} catch {
usage.error = "No Minimax credentials configured";
return usage;
}
const accessToken = creds?.access_token;
if (!accessToken) {
usage.error = "No Minimax access token found";
return usage;
}
try {
const res = await httpsRequest("https://api.minimaxi.com/user/quota", {
method: "GET",
headers: {
authorization: `Bearer ${accessToken}`,
},
});
if (res.status === 401 || res.status === 403) {
usage.status = "error";
usage.error = "Auth expired";
return usage;
}
if (res.status !== 200) {
usage.status = "error";
usage.error = `HTTP ${res.status}: ${res.body.slice(0, 200)}`;
return usage;
}
const data = JSON.parse(res.body);
usage.status = "ok";
const quota = data?.quota;
if (quota && typeof quota === "object") {
const total: number = quota.total ?? 0;
const used: number = quota.used ?? 0;
const remaining: number = quota.remaining ?? Math.max(0, total - used);
const percentUsed = total > 0 ? (used / total) * 100 : 0;
let resetText: string | null = null;
let resetMs: number | undefined;
let windowDurationMs: number | undefined;
const resetAt = data?.reset_at;
if (resetAt) {
const msLeft = new Date(resetAt).getTime() - Date.now();
resetMs = msLeft > 0 ? msLeft : 0;
resetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
// Weekly window duration (7 days)
windowDurationMs = 7 * 24 * 60 * 60 * 1000;
}
usage.windows.push({
label: "Weekly",
percentUsed: Math.min(100, Math.max(0, percentUsed)),
percentLeft: Math.min(100, Math.max(0, 100 - percentUsed)),
resetText,
resetMs,
windowDurationMs,
});
}
} catch (e: any) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
}
return usage;
}
// ── Zai (Zhipu AI) fetcher ──────────────────────────────────────────────────
async function fetchZaiUsage(): Promise<ProviderUsage> {
const usage: ProviderUsage = {
name: "Zai",
icon: "🟡",
status: "no-auth",
windows: [],
};
// Load Zai credentials
const authPath = path.join(process.env.HOME || "~", ".zai", "auth.json");
let auth: any = null;
try {
auth = JSON.parse(fs.readFileSync(authPath, "utf-8"));
} catch {
usage.error = "No Zai credentials configured";
return usage;
}
const accessToken = auth?.access_token;
if (!accessToken) {
usage.error = "No Zai access token found";
return usage;
}
try {
const res = await httpsRequest("https://api.zhipuai.com/v1/user/usage", {
method: "GET",
headers: {
authorization: `Bearer ${accessToken}`,
},
});
if (res.status === 401 || res.status === 403) {
usage.status = "error";
usage.error = "Auth expired";
return usage;
}
if (res.status !== 200) {
usage.status = "error";
usage.error = `HTTP ${res.status}: ${res.body.slice(0, 200)}`;
return usage;
}
const data = JSON.parse(res.body);
usage.status = "ok";
const usageData = data?.data;
if (usageData && typeof usageData === "object") {
const totalCredits: number = usageData.total_credits ?? 0;
const usedCredits: number = usageData.used_credits ?? 0;
const percentUsed = totalCredits > 0 ? (usedCredits / totalCredits) * 100 : 0;
let dailyResetText: string | null = null;
let dailyResetMs: number | undefined;
let monthlyResetText: string | null = null;
let monthlyResetMs: number | undefined;
const resetDate = usageData.reset_date;
if (resetDate) {
const resetTime = new Date(resetDate).getTime();
const msLeft = resetTime - Date.now();
// Determine if this is daily or monthly based on time until reset
const hoursLeft = msLeft / (1000 * 60 * 60);
if (hoursLeft <= 24) {
// Daily window
dailyResetMs = msLeft > 0 ? msLeft : 0;
dailyResetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
} else {
// Monthly window
monthlyResetMs = msLeft > 0 ? msLeft : 0;
monthlyResetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
}
}
// Add Daily window
usage.windows.push({
label: "Daily",
percentUsed: Math.min(100, Math.max(0, percentUsed)),
percentLeft: Math.min(100, Math.max(0, 100 - percentUsed)),
resetText: dailyResetText,
resetMs: dailyResetMs,
windowDurationMs: dailyResetMs ? 24 * 60 * 60 * 1000 : undefined,
});
// Add Monthly window if applicable
if (monthlyResetMs) {
usage.windows.push({
label: "Monthly",
percentUsed: Math.min(100, Math.max(0, percentUsed)),
percentLeft: Math.min(100, Math.max(0, 100 - percentUsed)),
resetText: monthlyResetText,
resetMs: monthlyResetMs,
windowDurationMs: 30 * 24 * 60 * 60 * 1000, // Approximate 30 days
});
}
}
} catch (e: any) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
}
return usage;
}
// ── Main export ────────────────────────────────────────────────────────────
/**
@@ -501,12 +779,17 @@ export async function fetchAllProviderUsage(_authStorage?: AuthStorageLike): Pro
fetchClaudeUsage(),
fetchCodexUsage(),
fetchGeminiUsage(),
fetchMinimaxUsage(),
fetchZaiUsage(),
]);
const providers: ProviderUsage[] = [];
for (const r of results) {
if (r.status === "fulfilled") {
providers.push(r.value);
// Apply pace calculation to all windows
const provider = r.value;
provider.windows = provider.windows.map(applyPaceToWindow);
providers.push(provider);
}
}