fix(opencode-go): add provider limits quota fetcher (#2861)

Integrated into release/v3.8.6
This commit is contained in:
Rajvardhan Patil
2026-05-29 13:58:50 +05:30
committed by GitHub
parent a8b4dc3e8c
commit a7e71ae494
7 changed files with 352 additions and 6 deletions

View File

@@ -88,6 +88,16 @@ const NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
const OPENCODE_GO_QUOTA_URL =
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit";
const OPENCODE_GO_QUOTA_TOTALS = {
session: 12,
weekly: 30,
mcp_monthly: 60,
} as const;
const OPENCODE_GO_QUOTA_ORDER = ["session", "weekly", "mcp_monthly"] as const;
type OpenCodeGoQuotaName = (typeof OPENCODE_GO_QUOTA_ORDER)[number];
// Cursor dashboard usage API config
// The endpoint that powers https://cursor.com/dashboard/spending. Validates the WorkOS
// session via the WorkosCursorSessionToken cookie (format: `${userId}::${jwt}`) and
@@ -198,6 +208,76 @@ function getGlmQuotaDisplayName(quotaName: string): string {
return quotaName;
}
function getOpenCodeGoTokenQuotaName(
limit: JsonRecord,
existingQuotas: Record<string, UsageQuota>
): "session" | "weekly" {
const unit = toNumber(limit.unit, 0);
const number = toNumber(limit.number, 0);
if (unit === 3 && number === 5) return "session";
if (unit === 6 && number === 1) return "weekly";
if ((unit === 4 && number === 7) || (unit === 3 && number >= 24 * 7)) return "weekly";
return existingQuotas.session ? "weekly" : "session";
}
function getOpenCodeGoQuotaDisplayName(quotaName: OpenCodeGoQuotaName): string {
if (quotaName === "session") return "5-hour rolling";
if (quotaName === "weekly") return "Weekly";
return "Monthly";
}
function normalizeOpenCodeGoQuotaToken(apiKey: string): string {
return apiKey.trim().replace(/^Bearer\s+/i, "");
}
function buildOpenCodeGoDollarQuota(
quotaName: OpenCodeGoQuotaName,
percentage: unknown,
resetAt: string | null,
usedOverride?: unknown,
details?: UsageQuota["details"]
): UsageQuota {
const total = OPENCODE_GO_QUOTA_TOTALS[quotaName];
const percentUsed = toPercentage(percentage);
const rawUsed = toNumber(usedOverride, Number.NaN);
const used = roundCurrency(
Number.isFinite(rawUsed) ? Math.max(0, Math.min(total, rawUsed)) : (total * percentUsed) / 100
);
const remaining = roundCurrency(Math.max(0, total - used));
const remainingPercentage =
total > 0
? clampPercentage(Math.round((remaining / total) * 100))
: clampPercentage(100 - percentUsed);
return {
used,
total,
remaining,
remainingPercentage,
resetAt,
unlimited: false,
displayName: getOpenCodeGoQuotaDisplayName(quotaName),
currency: "USD",
details,
};
}
function orderOpenCodeGoQuotas(quotas: Record<string, UsageQuota>): Record<string, UsageQuota> {
const ordered: Record<string, UsageQuota> = {};
for (const key of OPENCODE_GO_QUOTA_ORDER) {
if (quotas[key]) ordered[key] = quotas[key];
}
for (const [key, quota] of Object.entries(quotas)) {
if (!ordered[key]) ordered[key] = quota;
}
return ordered;
}
function getFieldValue(source: unknown, snakeKey: string, camelKey: string): unknown {
const obj = toRecord(source);
return obj[snakeKey] ?? obj[camelKey] ?? null;
@@ -207,6 +287,10 @@ function clampPercentage(value: number): number {
return Math.max(0, Math.min(100, value));
}
function roundCurrency(value: number): number {
return Math.round(value * 100) / 100;
}
function toDisplayLabel(value: string): string {
return value
.replace(/^copilot[_\s-]*/i, "")
@@ -788,6 +872,98 @@ async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string,
return { plan, quotas: orderGlmQuotas(quotas) };
}
async function getOpenCodeGoUsage(apiKey: string) {
const token = normalizeOpenCodeGoQuotaToken(apiKey);
if (!token) {
return { message: "API key not available. Add an OpenCode Go API key to view usage." };
}
const res = await fetch(OPENCODE_GO_QUOTA_URL, {
headers: {
Authorization: token,
"Accept-Language": "en-US,en",
"Content-Type": "application/json",
Accept: "application/json",
},
});
if (!res.ok) {
if (res.status === 401 || res.status === 403) throw new Error("Invalid OpenCode Go API key");
throw new Error(`OpenCode Go quota API error (${res.status})`);
}
const json = await res.json();
const code = toNumber(json.code, 200);
if (code === 401 || code === 403 || json.success === false) {
throw new Error("Invalid OpenCode Go API key");
}
const data = toRecord(json.data);
const limits: unknown[] = Array.isArray(data.limits) ? data.limits : [];
const quotas: Record<string, UsageQuota> = {};
for (const limit of limits) {
const src = toRecord(limit);
const type = String(src.type || "").toUpperCase();
const resetAt = parseResetTime(src.nextResetTime);
if (type === "TOKENS_LIMIT" || type === "TOKEN_LIMIT") {
const quotaName = getOpenCodeGoTokenQuotaName(src, quotas);
quotas[quotaName] = buildOpenCodeGoDollarQuota(
quotaName,
src.percentage,
resetAt,
undefined,
Array.isArray(src.models)
? (src.models as unknown[]).map((model) => {
const modelInfo = toRecord(model);
return {
name: String(modelInfo.model || modelInfo.modelCode || "usage"),
used: toNumber(modelInfo.percentage, 0),
};
})
: undefined
);
continue;
}
if (type === "TIME_LIMIT" || type === "TIME_USAGE_LIMIT") {
quotas.mcp_monthly = buildOpenCodeGoDollarQuota(
"mcp_monthly",
src.percentage,
resetAt,
src.currentValue,
Array.isArray(src.usageDetails)
? src.usageDetails.map((item) => {
const detail = toRecord(item);
return {
name: String(detail.modelCode || detail.name || "usage"),
used: toNumber(detail.usage, 0),
};
})
: undefined
);
}
}
const levelRaw =
typeof data.planName === "string"
? data.planName
: typeof data.level === "string"
? data.level
: "";
const planLabel = toTitleCase(levelRaw.replace(/\s*plan$/i, ""));
const plan = planLabel
? /^opencode\s+go\b/i.test(planLabel)
? planLabel
: `OpenCode Go ${planLabel}`
: null;
return { plan, quotas: orderOpenCodeGoQuotas(quotas) };
}
/**
* Bailian (Alibaba Coding Plan) Usage
* Fetches triple-window quota (5h, weekly, monthly) and returns worst-case.
@@ -1184,6 +1360,7 @@ export const USAGE_FETCHER_PROVIDERS = [
"glm-cn",
"zai",
"glmt",
"opencode-go",
"minimax",
"minimax-cn",
"crof",
@@ -1238,6 +1415,8 @@ export async function getUsageForProvider(
...(providerSpecificData || {}),
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
});
case "opencode-go":
return await getOpenCodeGoUsage(apiKey || "");
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey || "", provider);

View File

@@ -41,6 +41,7 @@ const PROVIDER_LABEL: Record<string, string> = {
glm: "GLM (Z.AI)",
zai: "Z.AI",
glmt: "GLM Thinking",
"opencode-go": "OpenCode Go",
"kimi-coding": "Kimi Coding",
minimax: "MiniMax",
"minimax-cn": "MiniMax CN",
@@ -60,10 +61,11 @@ const PROVIDER_ORDER: Record<string, number> = {
glm: 7,
zai: 8,
glmt: 9,
"kimi-coding": 10,
minimax: 11,
"minimax-cn": 12,
nanogpt: 13,
"opencode-go": 10,
"kimi-coding": 11,
minimax: 12,
"minimax-cn": 13,
nanogpt: 14,
};
const TIER_FILTERS = [

View File

@@ -246,6 +246,7 @@ export function parseQuotaData(provider, data) {
case "glm":
case "glm-cn":
case "glmt":
case "opencode-go":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(
@@ -403,7 +404,12 @@ export function parseQuotaData(provider, data) {
});
}
if (providerId === "glm" || providerId === "glm-cn" || providerId === "glmt") {
if (
providerId === "glm" ||
providerId === "glm-cn" ||
providerId === "glmt" ||
providerId === "opencode-go"
) {
normalizedQuotas.sort((a, b) => {
const orderA = GLM_QUOTA_ORDER[a.name] ?? 99;
const orderB = GLM_QUOTA_ORDER[b.name] ?? 99;

View File

@@ -53,6 +53,7 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"glm-cn",
"zai",
"glmt",
"opencode-go",
"minimax",
"minimax-cn",
"crof",

View File

@@ -2890,6 +2890,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"glm-cn",
"zai",
"glmt",
"opencode-go",
"minimax",
"minimax-cn",
"crof",

View File

@@ -76,12 +76,13 @@ test("convertUsageToQuotaInfo clamps remainingPercentage outside 0-100", () => {
assert.equal(result!.windows!.b.percentUsed, 1);
});
test("registerGenericQuotaFetchers registers Claude and GLM via the generic adapter", () => {
test("registerGenericQuotaFetchers registers Claude, GLM, and OpenCode Go via the generic adapter", () => {
registerGenericQuotaFetchers();
// Claude has no bespoke fetcher → should be registered.
assert.ok(getQuotaFetcher("claude"), "claude should be registered");
assert.ok(getQuotaFetcher("glm"), "glm should be registered");
assert.ok(getQuotaFetcher("zai"), "zai should be registered");
assert.ok(getQuotaFetcher("opencode-go"), "opencode-go should be registered");
// Codex has its own dedicated fetcher (registered by codexQuotaFetcher.ts,
// not by the generic registrar) — the generic registrar skips it. We can't
// assert "codex" here without first calling registerCodexQuotaFetcher,

View File

@@ -0,0 +1,156 @@
import test from "node:test";
import assert from "node:assert/strict";
const usage = await import("../../open-sse/services/usage.ts");
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
test("USAGE_SUPPORTED_PROVIDERS includes opencode-go", () => {
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as string[]).includes("opencode-go"),
"opencode-go must be in the usage-supported providers allowlist"
);
});
test("getUsageForProvider returns helpful message when opencode-go has no apiKey", async () => {
let called = false;
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
called = true;
return new Response("unexpected", { status: 500 });
};
try {
const result = (await usage.getUsageForProvider({
id: "opencode-go-no-key",
provider: "opencode-go",
apiKey: "",
})) as { message?: string };
assert.equal(called, false, "quota fetch must not run without an API key");
assert.match(result.message ?? "", /OpenCode Go/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider exposes OpenCode Go 5h, weekly, and monthly quotas", async () => {
const originalFetch = globalThis.fetch;
const reset5h = Date.now() + 2 * 60 * 60 * 1000;
const resetWeekly = Date.now() + 4 * 24 * 60 * 60 * 1000;
const resetMonthly = Date.now() + 20 * 24 * 60 * 60 * 1000;
let requestUrl = "";
let requestHeaders: Headers | null = null;
globalThis.fetch = async (input, init) => {
requestUrl = String(input);
requestHeaders = new Headers(init?.headers as HeadersInit | undefined);
return new Response(
JSON.stringify({
code: 200,
success: true,
data: {
level: "pro",
limits: [
{
type: "TOKENS_LIMIT",
unit: 3,
number: 5,
percentage: 25,
nextResetTime: reset5h,
},
{
type: "TOKENS_LIMIT",
unit: 6,
number: 1,
percentage: 50,
nextResetTime: resetWeekly,
},
{
type: "TIME_LIMIT",
percentage: 10,
currentValue: 6,
usage: 60,
nextResetTime: resetMonthly,
usageDetails: [{ modelCode: "search-prime", usage: 3 }],
},
],
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
};
try {
const result = (await usage.getUsageForProvider({
id: "opencode-go-usage",
provider: "opencode-go",
apiKey: "Bearer opencode-go-key",
})) as {
plan?: string | null;
quotas?: Record<
string,
{
used: number;
total: number;
remaining: number;
remainingPercentage: number;
resetAt: string | null;
displayName?: string;
currency?: string;
details?: Array<{ name: string; used: number }>;
}
>;
};
assert.equal(requestUrl, "https://api.z.ai/api/monitor/usage/quota/limit");
assert.equal(requestHeaders?.get("Authorization"), "opencode-go-key");
assert.equal(requestHeaders?.get("Content-Type"), "application/json");
assert.equal(result.plan, "OpenCode Go Pro");
assert.deepEqual(Object.keys(result.quotas ?? {}), ["session", "weekly", "mcp_monthly"]);
assert.equal(result.quotas!.session.displayName, "5-hour rolling");
assert.equal(result.quotas!.session.currency, "USD");
assert.equal(result.quotas!.session.used, 3);
assert.equal(result.quotas!.session.total, 12);
assert.equal(result.quotas!.session.remaining, 9);
assert.equal(result.quotas!.session.remainingPercentage, 75);
assert.equal(result.quotas!.session.resetAt, new Date(reset5h).toISOString());
assert.equal(result.quotas!.weekly.displayName, "Weekly");
assert.equal(result.quotas!.weekly.used, 15);
assert.equal(result.quotas!.weekly.total, 30);
assert.equal(result.quotas!.weekly.remaining, 15);
assert.equal(result.quotas!.weekly.remainingPercentage, 50);
assert.equal(result.quotas!.weekly.resetAt, new Date(resetWeekly).toISOString());
assert.equal(result.quotas!.mcp_monthly.displayName, "Monthly");
assert.equal(result.quotas!.mcp_monthly.used, 6);
assert.equal(result.quotas!.mcp_monthly.total, 60);
assert.equal(result.quotas!.mcp_monthly.remaining, 54);
assert.equal(result.quotas!.mcp_monthly.remainingPercentage, 90);
assert.equal(result.quotas!.mcp_monthly.resetAt, new Date(resetMonthly).toISOString());
assert.deepEqual(result.quotas!.mcp_monthly.details, [{ name: "search-prime", used: 3 }]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider rejects invalid OpenCode Go API keys", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("nope", { status: 401 });
try {
await assert.rejects(
() =>
usage.getUsageForProvider({
id: "opencode-go-401",
provider: "opencode-go",
apiKey: "bad-key",
}),
/Invalid OpenCode Go API key/
);
} finally {
globalThis.fetch = originalFetch;
}
});