Files
OmniRoute/tests/unit/usage-fetcher-registration-coverage.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

134 lines
5.5 KiB
TypeScript

/**
* `usage/fetcherProviders.ts` says of itself that it exists "so the registration
* list can't drift from the dispatcher's switch statement", and asks whoever adds
* a case to remember to add it here too. Nothing enforced that, and it drifted:
* #8006 added `adobe-firefly`/`firefly` to the switch and to
* `USAGE_SUPPORTED_PROVIDERS` but not to this list, so three consumers
* (`genericQuotaFetcher.ts`, `freeAccessQuota.ts`, the provider-plugin manifest)
* were told those providers had no usage fetcher when in fact they do.
*
* This test is the enforcement the comment asked for.
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { USAGE_FETCHER_PROVIDERS } from "../../open-sse/services/usage/fetcherProviders.ts";
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts";
const here = path.dirname(fileURLToPath(import.meta.url));
/**
* The provider ids `getUsageForProvider` actually dispatches on. Read from the
* source rather than by calling the function: importing the dispatcher pulls in
* its whole fetcher graph (DB, sockets, child_process), which is precisely the
* weight `fetcherProviders.ts` was extracted to avoid.
*/
function dispatchedProviderIds(): string[] {
const source = fs.readFileSync(path.resolve(here, "../../open-sse/services/usage.ts"), "utf8");
// Anchor on the dispatcher itself, not on the first occurrence of the word
// "switch" -- that one is in a doc comment twenty lines above it. Then stop at
// the closing brace of that switch, so a second switch added later in the file
// cannot contribute cases to an invariant that is only about this one.
const start = source.indexOf("switch (provider) {");
assert.notEqual(start, -1, "could not find `switch (provider) {` in services/usage.ts");
assert.equal(
source.indexOf("switch (provider) {", start + 1),
-1,
"more than one `switch (provider)` — this reader would merge them"
);
let depth = 0;
let end = start;
for (let i = source.indexOf("{", start); i < source.length; i += 1) {
if (source[i] === "{") depth += 1;
else if (source[i] === "}") {
depth -= 1;
if (depth === 0) {
end = i;
break;
}
}
}
assert.ok(end > start, "unbalanced braces in the dispatcher switch");
const body = source.slice(start, end);
const ids = [...body.matchAll(/^\s*case ["']([^"']+)["']:/gm)].map((match) => match[1]);
assert.ok(ids.length > 0, "parsed no cases out of the dispatcher — the reader is broken");
return [...new Set(ids)];
}
test("every provider the dispatcher handles is declared as having a fetcher", () => {
const undeclared = dispatchedProviderIds().filter(
(id) => !(USAGE_FETCHER_PROVIDERS as readonly string[]).includes(id)
);
assert.deepEqual(
undeclared,
[],
"these providers have a usage fetcher the dispatcher will happily call, but nothing " +
"declares it — so genericQuotaFetcher, freeAccessQuota and the plugin manifest all " +
"report them as having no usage support: " +
undeclared.join(", ")
);
});
test("nothing is declared that the dispatcher would not handle", () => {
const dispatched = dispatchedProviderIds();
const phantom = (USAGE_FETCHER_PROVIDERS as readonly string[]).filter(
(id) => !dispatched.includes(id)
);
assert.deepEqual(
phantom,
[],
`declared but unreachable, so a quota fetch would silently return nothing: ${phantom.join(", ")}`
);
});
/**
* The fetcher list and the "usage supported" connection list are not the same
* set, and should not be forced to converge — but every difference needs a
* reason on record, or the next drift hides among the ones we accepted.
*/
const ACCEPTED_DIVERGENCE: Record<string, string> = {
// Aggregators: a usage fetcher exists, but a connection to them is not itself
// presented as a usage-reporting account in the UI.
opencode: "aggregator — fetcher exists, not surfaced as a usage-reporting connection",
"opencode-zen": "aggregator — same as opencode",
xai: "reached through xai-oauth for connection purposes",
// Declared supported, no fetcher: a real gap, left alone here on purpose so
// this PR stays about the two providers whose fetcher already exists.
"xiaomi-mimo-token-plan": "declared supported with no fetcher — open question, not fixed here",
};
test("every difference between the two lists is one we have written down", () => {
const fetcher = new Set<string>(USAGE_FETCHER_PROVIDERS as readonly string[]);
const supported = new Set<string>(USAGE_SUPPORTED_PROVIDERS as readonly string[]);
const unexplained = [
...[...fetcher].filter((id) => !supported.has(id)),
...[...supported].filter((id) => !fetcher.has(id)),
].filter((id) => !(id in ACCEPTED_DIVERGENCE));
assert.deepEqual(
unexplained,
[],
"the two lists differ here for no recorded reason — either wire it up, or add it to " +
`ACCEPTED_DIVERGENCE with why: ${unexplained.join(", ")}`
);
});
test("the recorded divergences are still real, so the list does not rot", () => {
const fetcher = new Set<string>(USAGE_FETCHER_PROVIDERS as readonly string[]);
const supported = new Set<string>(USAGE_SUPPORTED_PROVIDERS as readonly string[]);
const stale = Object.keys(ACCEPTED_DIVERGENCE).filter(
(id) => fetcher.has(id) === supported.has(id)
);
assert.deepEqual(
stale,
[],
`these no longer differ; drop them from ACCEPTED_DIVERGENCE: ${stale.join(", ")}`
);
});