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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ services:

**周末/体验套餐自动领取 (claim)** —— 默认开启。代理每 5 分钟探测一次官方的限量套餐活动页,上新瞬间自动帮你抢(`claim.enabled: false` 可关闭)。手动抢:`bun run src/index.ts claim`。

**额度显示 (quota)** —— 登录后面板会自动查一次当前套餐的各模型额度(剩余 / 总额、到期时间),之后按 <kbd>r</kbd> 手动刷新。也可以命令行直接查:`bun run src/index.ts quota`(对应 HTTP 接口 `GET /quota`)。注意上游计费网关对频繁查询有限速,所以面板不做定时轮询。
**额度显示 (quota)** —— 登录后面板会自动查一次额度,之后按 <kbd>r</kbd> 手动刷新。数据来自上游两个额度平面:体验/积分制套餐的积分桶(`billing/balance`,剩余 / 总额、到期时间),以及个人编码套餐的用量窗口(`/api/monitor/usage/quota/limit`,与官方用量面板同源,5 小时 / 周窗口的剩余 / 总量与重置时间)。命令行直接查:`bun run src/index.ts quota`(对应 HTTP 接口 `GET /quota`)。注意上游网关对频繁查询有限速,所以面板不做定时轮询。

</details>

Expand Down
2 changes: 1 addition & 1 deletion README_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ The plan type (`plan`: `coding-plan` personal / `start-plan` trial) can be toggl

**Weekend/trial plan auto-claiming (claim)** — enabled by default. The proxy probes the official limited-plan campaign page every 5 minutes and grabs new drops for you the instant they appear (`claim.enabled: false` to disable). Manual run: `bun run src/index.ts claim`.

**Quota display (quota)** — after login the panel fetches the plan's per-model balances (remaining / total units, expiry) once automatically; refresh manually with <kbd>r</kbd>. CLI: `bun run src/index.ts quota` (HTTP: `GET /quota`). The upstream billing gateway rate-limits frequent queries, so the panel does not poll on a timer.
**Quota display (quota)** — after login the panel fetches quota once automatically; refresh manually with <kbd>r</kbd>. Data comes from two upstream planes: trial/credits-plan buckets (`billing/balance`, remaining / total units, expiry) and individual coding-plan usage windows (`/api/monitor/usage/quota/limit`, same endpoint the official usage panel reads — 5-hour / weekly window remaining / total and reset time). CLI: `bun run src/index.ts quota` (HTTP: `GET /quota`). The upstream gateways rate-limit frequent queries, so the panel does not poll on a timer.

</details>

Expand Down
21 changes: 19 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,8 +403,8 @@ async function claimCommand(args: string[]): Promise<void> {
}

/**
* `quota` subcommand — print the live plan balance snapshot (per-model
* remaining/total units + expiry) from the billing control plane. Reuses
* `quota` subcommand — print the live quota snapshot: per-model credit buckets
* (billing plane) and coding-plan usage windows (monitor plane). Reuses
* collectQuotaSnapshot (same path GET /quota serves).
*/
async function quotaCommand(): Promise<void> {
Expand All @@ -425,6 +425,23 @@ async function quotaCommand(): Promise<void> {
const exp = b.expiresAt ? ` · expires ${fmtQuotaExpiry(b.expiresAt)}` : "";
console.log(` ${b.showName || "(unnamed)"}: ${b.remainingUnits.toLocaleString("en-US")} / ${b.totalUnits.toLocaleString("en-US")} units${exp}`);
}
if (snap.codingPlan) {
const { level, limits } = snap.codingPlan;
if (limits.length === 0) {
console.log("No coding-plan usage windows reported by the monitor endpoint.");
}
for (const l of limits) {
const tier = level ? ` (${level})` : "";
// Mirror of the official panel: remaining only — upstream `number` is
// not a comparable total (live TIME_LIMIT row: remaining=3894, number=1).
const amount =
l.remaining !== undefined
? `${l.remaining.toLocaleString("en-US")}${l.unit ? ` ${l.unit}` : ""} remaining`
: "no usage numbers reported";
const reset = l.nextResetTime !== undefined ? ` · resets ${fmtQuotaExpiry(l.nextResetTime)}` : "";
console.log(` coding-plan${tier}: [${l.type}] ${amount}${reset}`);
}
}
for (const plan of snap.claimablePlans) {
const grants = plan.entitlements
.map((e) => `${e.showName || plan.name}: ${(e.grantUnits ?? 0).toLocaleString("en-US")} ${e.unitType}`)
Expand Down
141 changes: 130 additions & 11 deletions src/server/routes-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,30 @@ interface BillingCall {
headers: Record<string, string>;
}

/** Mock fetch that records billing calls and answers both endpoints. */
function makeBillingFetch(opts: { code?: number; body?: unknown } = {}): { fetchImpl: typeof fetch; calls: BillingCall[] } {
/** Mock fetch that records billing/monitor calls and answers all three endpoints. */
function makeBillingFetch(opts: {
code?: number;
body?: unknown;
/** Monitor-plane (coding) envelope data; defaults to an empty limits list. */
codingBody?: unknown;
codingCode?: number;
/** Simulate monitor-endpoint network/HTTP failure. */
codingFail?: boolean;
} = {}): { fetchImpl: typeof fetch; calls: BillingCall[] } {
const calls: BillingCall[] = [];
const record = (u: string, init?: RequestInit): void => {
calls.push({ url: u, headers: { ...((init?.headers as Record<string, string>) ?? {}) } });
};
const fetchImpl = (async (url: string | URL | Request, init?: RequestInit): Promise<Response> => {
const u = typeof url === "string" ? url : url.toString();
if (u.includes("/api/monitor/usage/quota/limit")) {
record(u, init);
if (opts.codingFail) return new Response("upstream exploded", { status: 500 });
const code = opts.codingCode ?? 0;
return new Response(JSON.stringify({ code, msg: "ok", data: opts.codingBody ?? { level: "max", limits: [] } }), { status: 200 });
}
if (u.includes("/api/v1/zcode-plan/billing/")) {
calls.push({ url: u, headers: { ...((init?.headers as Record<string, string>) ?? {}) } });
record(u, init);
const code = opts.code ?? 0;
return new Response(JSON.stringify({ code, msg: "ok", data: opts.body ?? { server_time: 1720000000, balances: [], plans: [] } }), { status: 200 });
}
Expand All @@ -81,6 +98,11 @@ function makeBillingFetch(opts: { code?: number; body?: unknown } = {}): { fetch
return { fetchImpl, calls };
}

/** Calls to one plane, in request order. */
function planeCalls(calls: BillingCall[], plane: "billing" | "monitor"): BillingCall[] {
return calls.filter((c) => c.url.includes(plane === "billing" ? "/api/v1/zcode-plan/billing/" : "/api/monitor/usage/quota/limit"));
}

/** Set/restore identity env overrides around a test body. */
async function withEnv(overrides: Record<string, string | undefined>, fn: () => Promise<void>): Promise<void> {
const saved: Record<string, string | undefined> = {};
Expand All @@ -100,29 +122,38 @@ async function withEnv(overrides: Record<string, string | undefined>, fn: () =>
}

describe("collectQuotaSnapshot fingerprint", () => {
it("no overrides → real platform/arch, never undefined-undefined", async () => {
it("no overrides → real platform/arch on billing calls, raw key auth on coding call", async () => {
await withEnv({ ZCODE_IDENTITY_PLATFORM: undefined, ZCODE_IDENTITY_ARCH: undefined }, async () => {
const { fetchImpl, calls } = makeBillingFetch();
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
expect(calls.length).toBe(2);
const billing = planeCalls(calls, "billing");
const monitor = planeCalls(calls, "monitor");
expect(billing.length).toBe(2);
expect(monitor.length).toBe(1);
const expected = `${process.platform}-${os.arch()}`;
expect(snap.errors).toEqual([]);
for (const c of calls) {
for (const c of billing) {
const url = new URL(c.url);
expect(url.searchParams.get("platform")).toBe(expected);
expect(url.searchParams.get("app_version")).toBe("test-1.0.0");
expect(c.headers["X-Platform"]).toBe(expected);
}
expect(calls[0].url).toContain("/billing/balance?");
expect(calls[1].url).toContain("/billing/preview?");
expect(billing[0].url).toContain("/billing/balance?");
expect(billing[1].url).toContain("/billing/preview?");
// Coding plane mirror: bundle `wK` origin + `md` single raw-key header.
expect(monitor[0].url).toBe("https://api.z.ai/api/monitor/usage/quota/limit");
expect(monitor[0].headers["authorization"]).toBe("key-x.secret-y");
expect(monitor[0].headers["accept"]).toBe("application/json");
expect(monitor[0].headers["X-Platform"]).toBeUndefined();
expect(snap.codingPlan).toEqual({ level: "max", limits: [] });
});
});

it("valid overrides → both billing calls use the overridden fingerprint", async () => {
it("valid overrides → billing calls use the overridden fingerprint (coding plane unaffected)", async () => {
await withEnv({ ZCODE_IDENTITY_PLATFORM: "linux", ZCODE_IDENTITY_ARCH: "x64" }, async () => {
const { fetchImpl, calls } = makeBillingFetch();
await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
for (const c of calls) {
for (const c of planeCalls(calls, "billing")) {
const url = new URL(c.url);
expect(url.searchParams.get("platform")).toBe("linux-x64");
expect(c.headers["X-Platform"]).toBe("linux-x64");
Expand All @@ -135,7 +166,7 @@ describe("collectQuotaSnapshot fingerprint", () => {
const { fetchImpl, calls } = makeBillingFetch();
await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
const expected = `${process.platform}-${os.arch()}`;
for (const c of calls) {
for (const c of planeCalls(calls, "billing")) {
expect(new URL(c.url).searchParams.get("platform")).toBe(expected);
}
});
Expand Down Expand Up @@ -200,3 +231,91 @@ describe("collectQuotaSnapshot response mapping", () => {
expect(snap.balances[1].expiresAt).toBeUndefined();
});
});

describe("collectQuotaSnapshot coding plane", () => {
const bigmodelCred: Credential = { apiKey: "bm-key-123", provider: "bigmodel", jwt: makeJwt() };
const loadBigmodel = async (): Promise<Credential> => bigmodelCred;
const jwtLessCred: Credential = { apiKey: "key-x.secret-y", provider: "zai" };
const loadJwtLess = async (): Promise<Credential> => jwtLessCred;

it("maps level + limits (numeric coercion, snake alias, rows without numbers dropped)", async () => {
const { fetchImpl } = makeBillingFetch({
codingBody: {
level: "max",
limits: [
{ type: "TIME_LIMIT", number: 120, usage: 84, remaining: 36, percentage: 70, next_reset_time: "1720000000", unit: "prompt" },
{ type: "no-numbers" }, // no numeric fields → display noise, dropped
{ number: 5 }, // no type → dropped (bundle `E_` keeps typed rows only)
{ type: "WEEK_LIMIT", remaining: 500 },
],
},
});
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
expect(snap.codingPlan).toEqual({
level: "max",
limits: [
{ type: "TIME_LIMIT", unit: "prompt", total: 120, used: 84, remaining: 36, percentage: 70, nextResetTime: 1720000000 },
{ type: "WEEK_LIMIT", remaining: 500 },
],
});
});

it("code 200 envelope counts as success (bundle `$_` semantics)", async () => {
const { fetchImpl } = makeBillingFetch({ codingCode: 200, codingBody: { level: "pro", limits: [] } });
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
expect(snap.errors).toEqual([]);
expect(snap.codingPlan).toEqual({ level: "pro", limits: [] });
});

it("bigmodel credential → open.bigmodel.cn monitor origin with the raw key", async () => {
const { fetchImpl, calls } = makeBillingFetch();
await collectQuotaSnapshot(makeConfig({ provider: "bigmodel" }), fetchImpl, loadBigmodel);
const monitor = planeCalls(calls, "monitor");
expect(monitor[0].url).toBe("https://open.bigmodel.cn/api/monitor/usage/quota/limit");
expect(monitor[0].headers["authorization"]).toBe("bm-key-123");
});

it("monitor endpoint failure degrades to codingPlan null + errors entry, credits data intact", async () => {
const { fetchImpl } = makeBillingFetch({
body: { server_time: 1, balances: [{ show_name: "Free", total_units: 10, remaining_units: 5 }] },
codingFail: true,
});
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
expect(snap.codingPlan).toBeNull();
expect(snap.errors.length).toBe(1);
expect(snap.errors[0]).toContain("coding: 500");
expect(snap.balances).toHaveLength(1);
});

it("nonzero monitor envelope code surfaces in errors, snapshot still resolves", async () => {
const { fetchImpl } = makeBillingFetch({ codingCode: 3012 });
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadFake);
expect(snap.codingPlan).toBeNull();
expect(snap.errors.length).toBe(1);
expect(snap.errors[0]).toContain("coding: 3012");
});

it("jwt-less credential still serves the coding plane (billing skipped with a note)", async () => {
const { fetchImpl, calls } = makeBillingFetch({
codingBody: { level: null, limits: [{ type: "TIME_LIMIT", remaining: 36, number: 120 }] },
});
const snap = await collectQuotaSnapshot(makeConfig(), fetchImpl, loadJwtLess);
expect(planeCalls(calls, "billing").length).toBe(0);
expect(planeCalls(calls, "monitor").length).toBe(1);
expect(snap.jwt).toBeNull();
expect(snap.balances).toEqual([]);
expect(snap.errors.length).toBe(1);
expect(snap.errors[0]).toContain("no plan JWT");
expect(snap.codingPlan).toEqual({ level: null, limits: [{ type: "TIME_LIMIT", remaining: 36, total: 120 }] });
});

it("unusable coding origin → coding plane silently skipped (fail-open)", async () => {
const cfg = makeConfig();
cfg.providers.zai.openaiBase = "not a url";
const { fetchImpl, calls } = makeBillingFetch();
const snap = await collectQuotaSnapshot(cfg, fetchImpl, loadFake);
expect(planeCalls(calls, "monitor").length).toBe(0);
expect(snap.codingPlan).toBeNull();
expect(snap.errors).toEqual([]);
});
});
Loading
Loading