Files
OmniRoute/tests/unit/usage-devin-cli.test.ts
Mr White 5a0a131bc7 feat(usage): devin-cli agentic quota + openrouter credits in Provider Limits (#12256)
* 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>
2026-09-02 00:02:15 -03:00

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));
});
});