mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 05:42:19 +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>
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
/**
|
|
* usage/openrouter.ts — OpenRouter usage-dashboard builder (#6842)
|
|
*
|
|
* Extracted as a leaf module (not inlined in usage.ts) so the god-file stays
|
|
* flat: this owns turning an OpenrouterQuota into the `UsageQuota` shape the
|
|
* Dashboard → Usage page renders, mirroring getDeepseekUsage's pattern.
|
|
*/
|
|
|
|
import { fetchOpenrouterQuota, type OpenrouterQuota } from "../openrouterQuotaFetcher.ts";
|
|
import { getFreeWindowStatus, resolveAccountKey } from "../openrouterFreeWindow.ts";
|
|
import { type UsageQuota } from "./quota.ts";
|
|
|
|
function buildCreditsQuota(quota: OpenrouterQuota): UsageQuota | null {
|
|
if (quota.limit === null && quota.creditBalance === null) return null;
|
|
return {
|
|
used: quota.limit !== null ? quota.limit - (quota.limitRemaining ?? quota.limit) : 0,
|
|
total: quota.limit ?? 0,
|
|
remaining: quota.creditBalance ?? undefined,
|
|
remainingPercentage: quota.limit !== null ? Math.round((1 - quota.percentUsed) * 100) : 100,
|
|
resetAt: quota.resetAt ?? null,
|
|
unlimited: quota.limit === null,
|
|
currency: "USD",
|
|
};
|
|
}
|
|
|
|
function buildFreeWindowQuota(connectionId: string, connection?: Record<string, unknown>) {
|
|
const accountKey = resolveAccountKey(connectionId, connection);
|
|
const status = getFreeWindowStatus(accountKey);
|
|
const dailyQuota: UsageQuota = {
|
|
used: status.dailyUsed,
|
|
total: status.dailyLimit,
|
|
remaining: status.dailyRemaining,
|
|
remainingPercentage: Math.round((status.dailyRemaining / status.dailyLimit) * 100),
|
|
resetAt: status.dailyResetAt,
|
|
unlimited: false,
|
|
displayName: "Free-tier requests (daily)",
|
|
};
|
|
const rpmQuota: UsageQuota = {
|
|
used: status.rpmUsed,
|
|
total: status.rpmLimit,
|
|
remaining: status.rpmRemaining,
|
|
remainingPercentage: Math.round((status.rpmRemaining / status.rpmLimit) * 100),
|
|
resetAt: null,
|
|
unlimited: false,
|
|
displayName: "Free-tier requests (per minute)",
|
|
};
|
|
return { dailyQuota, rpmQuota };
|
|
}
|
|
|
|
/**
|
|
* OpenRouter Usage — merges the /key + /credits polling fetcher with the
|
|
* locally-tracked `:free`-variant request window into one usage payload.
|
|
*/
|
|
export async function getOpenrouterUsage(
|
|
connectionId: string,
|
|
apiKey: string,
|
|
providerSpecificData?: Record<string, unknown> | null
|
|
) {
|
|
if (!apiKey) {
|
|
return { message: "OpenRouter API key not available. Add a key to view usage." };
|
|
}
|
|
|
|
const connection = { apiKey, providerSpecificData: providerSpecificData ?? {} };
|
|
const quota = (await fetchOpenrouterQuota(connectionId, connection)) as OpenrouterQuota | null;
|
|
|
|
const quotas: Record<string, UsageQuota> = {};
|
|
const { dailyQuota, rpmQuota } = buildFreeWindowQuota(connectionId, connection);
|
|
quotas.free_daily = dailyQuota;
|
|
quotas.free_rpm = rpmQuota;
|
|
|
|
if (!quota) {
|
|
return {
|
|
plan: "OpenRouter (credits endpoint unreachable)",
|
|
quotas,
|
|
message: "OpenRouter connected. /key and /credits both unreachable — no balance data.",
|
|
};
|
|
}
|
|
|
|
const creditsQuota = buildCreditsQuota(quota);
|
|
if (creditsQuota) quotas.credits = creditsQuota;
|
|
|
|
return {
|
|
plan: quota.isFreeTier ? "OpenRouter (Free Tier)" : "OpenRouter",
|
|
quotas,
|
|
isFreeTier: quota.isFreeTier,
|
|
usageDaily: quota.usageDaily,
|
|
usageWeekly: quota.usageWeekly,
|
|
usageMonthly: quota.usageMonthly,
|
|
};
|
|
}
|