fix(usage): track OpenCode Go quota (#3791)

OpenCode Go API-key connections now appear in the Quota Tracker and report rolling, weekly, and monthly subscription usage.
This commit is contained in:
JOJO
2026-09-05 21:07:58 +07:00
committed by decolua
parent 81f4f93082
commit 0da803eef4
5 changed files with 246 additions and 1 deletions

View File

@@ -21,6 +21,9 @@ export default {
transport: {
baseUrl: "https://opencode.ai/zen/go/v1/chat/completions",
headers: {},
usage: {
url: "https://opencode.ai/zen/go/v1/usage",
},
},
// Multi-endpoint: pick the transport matching the client sourceFormat to skip
// translation. Guarded per-model by `supportedFormats` (see chatCore) because
@@ -48,4 +51,8 @@ export default {
{ id: "qwen3.7-plus", name: "Qwen 3.7 Plus", supportedFormats: ["openai", "claude"] },
{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus", supportedFormats: ["openai", "claude"] },
],
features: {
usage: true,
usageApikey: true,
},
};

View File

@@ -14,6 +14,7 @@ import { getCodeBuddyCnUsage, getCodeBuddyIntlUsage } from "./usage/codebuddy-cn
import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getKimiUsage } from "./usage/kimi.js";
import { getDeepseekUsage } from "./usage/deepseek.js";
import { getOpenCodeGoUsage } from "./usage/opencode-go.js";
import { getGroqUsage } from "./usage/groq.js";
import { getZedUsage } from "./usage/zed.js";
import { resolveQoderCredentials } from "./qoderModels.js";
@@ -55,6 +56,7 @@ const USAGE_HANDLERS = {
"codebuddy-intl": (c) => getCodeBuddyIntlUsage(c.accessToken, c.apiKey, c.providerSpecificData, c.proxyOptions),
"grok-cli": (c) => getGrokCliUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
kimi: (c) => getKimiUsage(c.accessToken, c.apiKey, c.proxyOptions, c.providerSpecificData),
"opencode-go": (c) => getOpenCodeGoUsage(c.apiKey, c.proxyOptions),
deepseek: (c) => getDeepseekUsage(c.apiKey, c.proxyOptions),
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),

View File

@@ -0,0 +1,107 @@
/**
* OpenCode Go usage — GET https://opencode.ai/zen/go/v1/usage
* Auth: Bearer <apiKey>
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { parseResetTime, toFiniteNumber, U } from "./shared.js";
const USAGE_URL = U("opencode-go").url;
const QUOTA_NAMES = {
rolling: "Rolling",
weekly: "Weekly",
monthly: "Monthly",
};
function parsePercent(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return null;
}
export async function getOpenCodeGoUsage(apiKey = null, proxyOptions = null) {
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
return {
message: "OpenCode Go API key not available. Add a key to view usage.",
};
}
try {
const response = await proxyAwareFetch(
USAGE_URL,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey.trim()}`,
Accept: "application/json",
},
},
proxyOptions,
);
if (response.status === 401) {
return {
plan: "OpenCode Go",
message: "OpenCode Go authentication failed. Check the API key.",
};
}
if (response.status === 403) {
const error = await response.json().catch(() => null);
const subscriptionRequired = error?.error?.type === "EntitlementError";
return {
plan: "OpenCode Go",
message: subscriptionRequired
? "OpenCode Go subscription required for this API key."
: "OpenCode Go access forbidden for this API key.",
};
}
if (!response.ok) {
return {
plan: "OpenCode Go",
message: `OpenCode Go usage API error (${response.status}).`,
};
}
const data = await response.json().catch(() => null);
if (!data?.usage || typeof data.usage !== "object") {
return {
plan: "OpenCode Go",
message: "OpenCode Go usage response did not contain quota data.",
};
}
const quotas = {};
for (const [period, name] of Object.entries(QUOTA_NAMES)) {
const quota = data.usage[period];
if (!quota || typeof quota !== "object") continue;
const percent = parsePercent(quota.percent);
if (percent === null) continue;
const used = Math.max(0, Math.min(100, toFiniteNumber(percent, 0)));
quotas[name] = {
used,
total: 100,
remaining: 100 - used,
remainingPercentage: 100 - used,
resetAt: parseResetTime(quota.resetsAt),
unlimited: false,
};
}
if (Object.keys(quotas).length === 0) {
return {
plan: "OpenCode Go",
message: "OpenCode Go usage response did not contain valid quota data.",
};
}
return { plan: "OpenCode Go", quotas };
} catch (error) {
return { message: `OpenCode Go error: ${error.message}` };
}
}