mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
* feat(usage): devin-cli agentic quota + openrouter credits in Provider Limits Two provider families with live quota APIs were missing from the Provider Limits dashboard because their list entries were absent: - devin-cli: new usage leaf querying the Codeium seat-management Connect API (exa.seat_management_pb.SeatManagementService/GetUserStatus, protobuf over POST with the raw `Basic <token>-<token>` auth header the CLI itself uses). Surfaces the plan name plus daily/weekly agentic quota percentages with reset timestamps from the GetUserStatus plan_status payload, via a minimal hand-rolled protobuf encoder/reader (no proto dependency warranted for two fixed messages). - openrouter: the /key + /credits quota fetcher (#6842) was already wired into the dispatcher but gated out of the bulk sync — add it to USAGE_SUPPORTED_PROVIDERS and PROVIDER_LIMITS_APIKEY_PROVIDERS so key limits and account credits actually surface. * fix(build): externalize tiktoken so tiktoken_bg.wasm resolves at runtime The vendored ChatGPT Web connector v4.0.7 (#12181) imports tiktoken (get_encoding) at module level. tiktoken's node build reads tiktoken_bg.wasm via a __dirname-relative fs.readFileSync during import; when Next bundles the package the wasm asset is not traced into the server chunk, and page-data collection for every route reaching the tokenizer (e.g. /api/providers/[id]/chatgpt-web-codex-doctor) aborts with "Missing tiktoken_bg.wasm" — breaking the whole standalone build. Externalize it like the other runtime-resolved native/wasm packages (sql.js, sqlite-vec, better-sqlite3): the require stays at runtime, where node_modules/tiktoken/tiktoken_bg.wasm resolves normally. * fix(openrouter): /credits balance survives a /key failure OpenRouter is credit-based, not subscription-based: the authoritative remaining-credits signal is GET /api/v1/credits (total_credits - total_usage, the documented "get remaining credits" endpoint), while the /key limit fields are optional per-key caps that most accounts never set. fetchOpenrouterQuota previously treated /key as mandatory — any /key failure (429 rate limit, transient error, unexpected shape) discarded the whole payload and the Usage dashboard showed "OpenRouter (usage endpoint unreachable)" even though /credits was reachable. Now: - /key unavailable + /credits OK → credits-only quota (creditBalance = total_credits - total_usage) instead of null - /key 401/403 alone no longer means an invalid token; only a double auth-rejection (both endpoints) does - null is returned only when both endpoints fail, and the dashboard label reflects that ("credits endpoint unreachable") * fix(openrouter): render AI Credits as a USD credit count in Provider Limits The Provider Limits card's dollar renderer only activates on isCredits/creditCount rows (QuotaCardExpanded), but openrouter went through parseGeneric — which drops `currency` and never sets those flags — so the credits balance rendered as a meaningless "100% left" (the unlimited-credits row is always 100%) instead of the actual credit count. Route openrouter's `credits` quota through buildCreditsQuota() like the DeepSeek/AgentRouter credits rows: label "AI Credits", dollar-formatted balance. Free-tier request windows keep the generic percentage treatment. * fix(usage): document DEVIN_SEAT_API_URL and split quota parsers Keep fetchOpenrouterQuota and decodeProtoFields under the complexity ratchets, and add the seat-management URL to the env/docs contract. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * test(usage): drop duplicated GLM quota-ordering test in provider-limits-ui * test(usage): drop stale openrouter ACCEPTED_DIVERGENCE OpenRouter is now in both USAGE_FETCHER_PROVIDERS and USAGE_SUPPORTED_PROVIDERS, so the recorded aggregator divergence is no longer real. Add the changelog fragment. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
/**
|
|
* tests/unit/usage-devin-cli.test.ts
|
|
*
|
|
* Devin CLI (devin-cli) exposes no REST usage endpoint — the official CLI reads
|
|
* account quota from the Codeium seat-management Connect API
|
|
* (exa.seat_management_pb.SeatManagementService/GetUserStatus, protobuf over
|
|
* POST with a raw `Basic <token>-<token>` auth header). These tests cover the
|
|
* minimal protobuf wire helpers (encoder round-trip + malformed-input
|
|
* rejection), the GetUserStatus response parser against a wire-format fixture
|
|
* matching the live API shape, and the dispatcher wiring.
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { decodeProtoFields, parseDevinUserStatus } =
|
|
await import("../../open-sse/services/usage/devinCli.ts");
|
|
|
|
function encodeVarint(value: number): number[] {
|
|
const bytes: number[] = [];
|
|
let v = value;
|
|
while (v > 0x7f) {
|
|
bytes.push((v & 0x7f) | 0x80);
|
|
v = Math.floor(v / 128);
|
|
}
|
|
bytes.push(v);
|
|
return bytes;
|
|
}
|
|
|
|
function lenField(field: number, payload: number[]): number[] {
|
|
return [...encodeVarint((field << 3) | 2), ...encodeVarint(payload.length), ...payload];
|
|
}
|
|
|
|
function varintField(field: number, value: number): number[] {
|
|
return [...encodeVarint((field << 3) | 0), ...encodeVarint(value)];
|
|
}
|
|
|
|
function strField(field: number, text: string): number[] {
|
|
const bytes = Array.from(new TextEncoder().encode(text));
|
|
return [...encodeVarint((field << 3) | 2), ...encodeVarint(bytes.length), ...bytes];
|
|
}
|
|
|
|
describe("devin-cli protobuf wire helpers", () => {
|
|
it("decodes varint and length-delimited fields", () => {
|
|
const wire = new Uint8Array([...varintField(14, 90), ...lenField(2, [0x50, 0x72, 0x6f])]);
|
|
const fields = decodeProtoFields(wire);
|
|
assert.ok(fields);
|
|
assert.equal(fields.length, 2);
|
|
assert.equal(fields[0].field, 14);
|
|
assert.equal(fields[0].varint, 90);
|
|
assert.equal(fields[1].field, 2);
|
|
assert.ok(fields[1].bytes);
|
|
assert.equal(new TextDecoder().decode(fields[1].bytes!), "Pro");
|
|
});
|
|
|
|
it("returns null on malformed input (truncated length-delimited field)", () => {
|
|
// Field 1, wire 2, length 200 — but only 3 bytes follow.
|
|
const malformed = new Uint8Array([0x0a, 0xc8, 0x01, 0x01, 0x02, 0x03]);
|
|
assert.equal(decodeProtoFields(malformed), null);
|
|
});
|
|
});
|
|
|
|
describe("parseDevinUserStatus", () => {
|
|
it("parses a GetUserStatus fixture into the quota snapshot", () => {
|
|
const dailyResetUnix = 1788249600;
|
|
const weeklyResetAtUnix = 1788681600;
|
|
const planInfo = [...strField(2, "Pro")];
|
|
const planStatus = [
|
|
...lenField(1, planInfo),
|
|
...varintField(14, 90),
|
|
...varintField(15, 95),
|
|
...varintField(17, dailyResetUnix),
|
|
...varintField(18, weeklyResetAtUnix),
|
|
];
|
|
const userStatus = [...lenField(13, planStatus)];
|
|
const response = new Uint8Array([...lenField(1, userStatus)]);
|
|
|
|
const snapshot = parseDevinUserStatus(response);
|
|
assert.ok(snapshot);
|
|
assert.equal(snapshot.plan, "Pro");
|
|
assert.equal(snapshot.dailyRemainingPercent, 90);
|
|
assert.equal(snapshot.weeklyRemainingPercent, 95);
|
|
assert.equal(snapshot.dailyResetAtUnix, dailyResetUnix);
|
|
assert.equal(snapshot.weeklyResetAtUnix, weeklyResetAtUnix);
|
|
});
|
|
|
|
it("returns null when plan_status is absent", () => {
|
|
const userStatus = [...strField(2, "no-plan-status-here")];
|
|
const response = new Uint8Array([...lenField(1, userStatus)]);
|
|
assert.equal(parseDevinUserStatus(response), null);
|
|
});
|
|
});
|
|
|
|
describe("devin-cli dispatcher wiring", () => {
|
|
it("is registered in USAGE_FETCHER_PROVIDERS alongside openrouter", async () => {
|
|
const { USAGE_FETCHER_PROVIDERS } = await import("../../open-sse/services/usage.ts");
|
|
assert.ok(USAGE_FETCHER_PROVIDERS.includes("devin-cli"));
|
|
assert.ok(USAGE_FETCHER_PROVIDERS.includes("openrouter"));
|
|
});
|
|
|
|
it("getDevinCliUsage returns a graceful message without a token", async () => {
|
|
const { getDevinCliUsage } = await import("../../open-sse/services/usage/devinCli.ts");
|
|
const result = (await getDevinCliUsage("")) as { message?: string; quotas?: unknown };
|
|
assert.ok(result.message);
|
|
assert.equal(result.quotas, undefined);
|
|
});
|
|
|
|
it("dispatcher routes devin-cli to the seat-management fetcher", async () => {
|
|
const { getUsageForProvider } = await import("../../open-sse/services/usage.ts");
|
|
const result = (await getUsageForProvider({
|
|
id: "conn-d",
|
|
provider: "devin-cli",
|
|
accessToken: undefined,
|
|
apiKey: undefined,
|
|
})) as { message?: string; quotas?: unknown };
|
|
assert.ok(result.message && !("quotas" in result));
|
|
});
|
|
});
|