chore(quota): drop the Kimi Code quota reader (Kimi already left the catalog)
Redone on v2.8.22 from 3179d25 (feat/grok-usage, never merged).
This commit is contained in:
@@ -6,9 +6,7 @@
|
||||
* with its own token only while it is valid, never refreshed, and never shown
|
||||
* with the usage of a token that belongs to a different account; each
|
||||
* tool's plan name; the Cursor CLI's login asked of cursor.com only while
|
||||
* valid; Antigravity's `agy --print /usage` pools, never on an old agy; and
|
||||
* the Kimi Code login, sent only to its own host while valid, never
|
||||
* refreshed.
|
||||
* valid; and Antigravity's `agy --print /usage` pools, never on an old agy.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -22,12 +20,6 @@ process.env.XDG_CONFIG_HOME = require("node:fs").mkdtempSync(
|
||||
);
|
||||
// Nor run the machine's real agy: only the Antigravity test's fake one runs.
|
||||
process.env.AGENTHUB_AGY_USAGE = "0";
|
||||
// Nor its real Kimi login.
|
||||
for (const k of ["KIMI_CODE_HOME"]) {
|
||||
process.env[k] = require("node:fs").mkdtempSync(
|
||||
require("node:path").join(require("node:os").tmpdir(), "quota-home-")
|
||||
);
|
||||
}
|
||||
|
||||
test("windowLabel names the plan windows", () => {
|
||||
assert.strictEqual(windowLabel(300), "5h");
|
||||
@@ -415,58 +407,3 @@ function stubFetch(answers) {
|
||||
};
|
||||
return { asked, restore: () => (global.fetch = prev) };
|
||||
}
|
||||
|
||||
test("Kimi Code: the CLI's own token asks api.kimi.com/coding for 5h, week and month", async () => {
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { quotas, __resetCache } = require("../lib/quota");
|
||||
const dir = path.join(process.env.KIMI_CODE_HOME, "credentials");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, "kimi-code.json");
|
||||
const f = stubFetch({
|
||||
"https://api.kimi.com/coding/v1/usages": [
|
||||
200,
|
||||
{
|
||||
usages: {
|
||||
limit_5h: { used_ratio: 0.3, reset_time: "2099-01-01T05:00:00Z" },
|
||||
limit_7d: { used_ratio: "0.2", reset_time: "2099-01-07T00:00:00Z" },
|
||||
limit_month_total: { used_ratio: 0.4, reset_time: "2099-02-01T00:00:00Z" },
|
||||
limit_month_code: { used_ratio: 0.25, reset_time: "2099-02-01T00:00:00Z" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const kimi = async () => (await quotas()).tools.find((t) => t.id === "kimi");
|
||||
try {
|
||||
__resetCache();
|
||||
assert.strictEqual(await kimi(), undefined);
|
||||
const exp = Math.floor(Date.now() / 1000) + 600;
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
JSON.stringify({ access_token: "tok-k", refresh_token: "r", expires_at: exp })
|
||||
);
|
||||
__resetCache();
|
||||
const row = await kimi();
|
||||
assert.strictEqual(row.expiresAt, new Date(exp * 1000).toISOString());
|
||||
assert.deepStrictEqual(
|
||||
row.windows.map((w) => [w.label, Math.round(w.usedPercent)]),
|
||||
[
|
||||
["5h", 30],
|
||||
["wk", 20],
|
||||
["mo", 40],
|
||||
]
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
f.asked.filter((a) => a.includes("kimi")),
|
||||
["https://api.kimi.com/coding/v1/usages Bearer tok-k"]
|
||||
);
|
||||
fs.writeFileSync(file, JSON.stringify({ access_token: "tok-k", expires_at: 1 }));
|
||||
__resetCache();
|
||||
assert.strictEqual((await kimi()).error, "ETOKENEXPIRED");
|
||||
assert.strictEqual(f.asked.filter((a) => a.includes("kimi")).length, 1);
|
||||
} finally {
|
||||
f.restore();
|
||||
fs.rmSync(file, { force: true });
|
||||
__resetCache();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file server-s3-quota.test.js
|
||||
* @description S3 plan, P4: plan quota per member home. `quotas({home})`
|
||||
* reads Claude, Codex, Cursor and Kimi Code from that
|
||||
* reads Claude, Codex and Cursor from that
|
||||
* Linux account's home only — never the host's logins, never agy — and only
|
||||
* files the member owns (review fix F3: a FIFO or another account's file is
|
||||
* not read). `GET /api/pricing/quota` gives a developer or an analyst their
|
||||
@@ -32,7 +32,6 @@ process.env.CLAUDE_HOME = path.join(HOST, ".claude");
|
||||
process.env.CODEX_HOME = path.join(HOST, ".codex");
|
||||
process.env.XDG_CONFIG_HOME = path.join(HOST, ".config");
|
||||
process.env.AGENTHUB_CURSOR_DIR = path.join(HOST, ".cursor");
|
||||
process.env.KIMI_CODE_HOME = path.join(HOST, ".kimi-code");
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
const auth = require("../lib/auth");
|
||||
@@ -73,10 +72,6 @@ function seedHome(home, who, { codexPercent }) {
|
||||
write(path.join(home, ".cursor", "cli-config.json"), {
|
||||
authInfo: { email: `${who}@cursor`, userId: "u1" },
|
||||
});
|
||||
write(path.join(home, ".kimi-code", "credentials", "kimi-code.json"), {
|
||||
access_token: `${who}-kimi`,
|
||||
expires_at: Math.floor(LATER / 1000),
|
||||
});
|
||||
}
|
||||
|
||||
let asked = [];
|
||||
@@ -136,12 +131,12 @@ describe("quotas({home}) reads a member's own logins", () => {
|
||||
__resetCache();
|
||||
asked = [];
|
||||
const { tools } = await quotas({ home: MIA });
|
||||
assert.deepEqual(tools.map((t) => t.id).sort(), ["claude", "codex", "cursor", "kimi"]);
|
||||
assert.deepEqual(tools.map((t) => t.id).sort(), ["claude", "codex", "cursor"]);
|
||||
const codex = tools.find((t) => t.id === "codex");
|
||||
assert.equal(codex.account, "mia@codex");
|
||||
assert.equal(codex.windows[0].usedPercent, 21);
|
||||
assert.equal(tools.find((t) => t.id === "cursor").account, "mia@cursor");
|
||||
assert.ok(asked.length >= 4, asked.join(","));
|
||||
assert.ok(asked.length >= 3, asked.join(","));
|
||||
assert.ok(
|
||||
asked.every((a) => a.includes("mia-")),
|
||||
`only mia's tokens leave: ${asked.join(",")}`
|
||||
@@ -161,9 +156,9 @@ describe("quotas({home}) reads a member's own logins", () => {
|
||||
});
|
||||
|
||||
it("reads no file the member does not own, and never blocks on a FIFO", async () => {
|
||||
fs.mkdirSync(path.join(BOB, ".kimi-code", "credentials"), { recursive: true });
|
||||
fs.mkdirSync(path.join(BOB, ".config", "cursor"), { recursive: true });
|
||||
require("node:child_process").execFileSync("mkfifo", [
|
||||
path.join(BOB, ".kimi-code", "credentials", "kimi-code.json"),
|
||||
path.join(BOB, ".config", "cursor", "auth.json"),
|
||||
]);
|
||||
__resetCache();
|
||||
assert.deepEqual((await quotas({ home: BOB })).tools, []);
|
||||
|
||||
+4
-57
@@ -34,12 +34,6 @@
|
||||
* names no account and no plan. Cached like Claude; a failed run shows
|
||||
* the last figure with its `asOf`. Running agy may let it update itself;
|
||||
* `AGENTHUB_AGY_USAGE=0` never runs it.
|
||||
* - **Kimi Code**: `$KIMI_CODE_HOME/credentials/kimi-code.json` (default
|
||||
* `~/.kimi-code`), sent only to `api.kimi.com`'s `GET /coding/v1/usages`:
|
||||
* `5h`, `wk`, `mo` from `limit_5h`/`limit_7d`/`limit_month_total`.
|
||||
* Kimi's token is used only while valid and never refreshed (Kimi rotates
|
||||
* its refresh token: a refresh here would sign the CLI out). This reader is
|
||||
* Orca's (github.com/stablyai/orca, MIT), trimmed.
|
||||
*
|
||||
* Each tool also carries its signed-in `account` (an email), its `plan` (the
|
||||
* subscription's name as the tool reports it: Claude's `subscriptionType`,
|
||||
@@ -50,8 +44,8 @@
|
||||
*
|
||||
* Server mode with S3 (`quotas({home})`): a member's plan is read from THEIR
|
||||
* Linux account's home only — `.claude`, `.codex`, `.config/cursor` +
|
||||
* `.cursor`, `.kimi-code`, never the host's env overrides — through `run-as`'s owner-checked reads (F3). No
|
||||
* saved Claude profiles (the Profiles modal is the host's) and no agy: its
|
||||
* `.cursor`, never the host's env overrides — through `run-as`'s
|
||||
* owner-checked reads (F3). No saved Claude profiles (the Profiles modal is the host's) and no agy: its
|
||||
* `/usage` needs the self-updating binary, so it stays host-only.
|
||||
*
|
||||
* Only percentages, window lengths, reset/expiry times, the plan's name and
|
||||
@@ -79,7 +73,6 @@ const TAIL_BYTES = 512 * 1024;
|
||||
const CODEX_FILES = 5;
|
||||
const CURSOR_USAGE_URL = "https://cursor.com/api/usage-summary";
|
||||
const TIMEOUT_MS = 10_000;
|
||||
const KIMI_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
|
||||
const AGY_TIMEOUT_MS = 60_000;
|
||||
const AGY_MIN_VERSION = [1, 1, 11];
|
||||
const AGY_WINDOW = { "5h": "5h", weekly: "wk" };
|
||||
@@ -350,7 +343,6 @@ function toolDirs(home) {
|
||||
codex: home ? path.join(home, ".codex") : getCodexHome(),
|
||||
cursorConfig: path.join(env("XDG_CONFIG_HOME") || path.join(h, ".config"), "cursor"),
|
||||
cursor: home ? path.join(home, ".cursor") : getCursorDir(),
|
||||
kimi: env("KIMI_CODE_HOME") || path.join(h, ".kimi-code"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -464,50 +456,6 @@ async function antigravityQuota() {
|
||||
}
|
||||
}
|
||||
|
||||
const isoOf = (v) => {
|
||||
const ms = Date.parse(v);
|
||||
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
||||
};
|
||||
|
||||
/** A Bearer GET to a tool's own host: its windows, or the row's error. */
|
||||
async function bearerWindows(who, url, token, headers, toWindows) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: "application/json", Authorization: `Bearer ${token}`, ...headers },
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const expired = res.status === 401 || res.status === 403;
|
||||
return { ...who, windows: [], error: expired ? "ETOKENEXPIRED" : "EUPSTREAM" };
|
||||
}
|
||||
return { ...who, ...toWindows(await res.json()) };
|
||||
} catch {
|
||||
return { ...who, windows: [], error: "EUPSTREAM" };
|
||||
}
|
||||
}
|
||||
|
||||
/** Kimi Code's OAuth token and its managed-plan usage. */
|
||||
async function kimiQuota(dirs = toolDirs()) {
|
||||
const cred = readJson(path.join(dirs.kimi, "credentials", "kimi-code.json"));
|
||||
if (typeof cred?.access_token !== "string" || !cred.access_token) return null;
|
||||
const exp = Number(cred.expires_at) * 1000;
|
||||
const who = { id: "kimi", active: true, expiresAt: exp ? iso(exp) : null };
|
||||
if (exp && !fresh(exp)) return { ...who, windows: [], error: "ETOKENEXPIRED" };
|
||||
return bearerWindows(who, KIMI_USAGE_URL, cred.access_token, {}, (body) => ({
|
||||
windows: [
|
||||
["5h", body?.usages?.limit_5h],
|
||||
["wk", body?.usages?.limit_7d],
|
||||
["mo", body?.usages?.limit_month_total],
|
||||
]
|
||||
.filter(([, u]) => u && Number.isFinite(Number(u.used_ratio)))
|
||||
.map(([label, u]) => ({
|
||||
label,
|
||||
usedPercent: Number(u.used_ratio) * 100,
|
||||
resetsAt: isoOf(u.reset_time),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Every tool's quota row. `home`: a member's Linux home (S3) — that account's
|
||||
* logins only, and no agy; omitted, the host's.
|
||||
@@ -515,13 +463,12 @@ async function kimiQuota(dirs = toolDirs()) {
|
||||
async function quotas({ force = false, home = null } = {}) {
|
||||
const dirs = toolDirs(home);
|
||||
const key = (k) => (home ? `${home}|${k}` : k);
|
||||
const [claude, cursor, antigravity, kimi] = await Promise.all([
|
||||
const [claude, cursor, antigravity] = await Promise.all([
|
||||
claudeQuotas(force, home),
|
||||
cached(key("cursor"), force, () => cursorQuota(dirs)),
|
||||
home ? null : cached("antigravity", force, antigravityQuota),
|
||||
cached(key("kimi"), force, () => kimiQuota(dirs)),
|
||||
]);
|
||||
const tools = [...claude, codexQuota(dirs.codex), antigravity, cursor, kimi].filter(Boolean);
|
||||
const tools = [...claude, codexQuota(dirs.codex), antigravity, cursor].filter(Boolean);
|
||||
return { tools, checkedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user