merge: sync upstream v0.5.81 into MIBP fork

# Conflicts:
#	.gitignore
#	Dockerfile
#	open-sse/handlers/chatCore.js
#	open-sse/providers/registry/cline.js
#	open-sse/providers/registry/index.js
#	open-sse/services/usage.js
#	open-sse/utils/streamHandler.js
#	package.json
#	src/app/(dashboard)/dashboard/profile/page.js
#	src/app/(dashboard)/dashboard/providers/[id]/page.js
This commit is contained in:
MUH. IQRAM BAHRING committed 2026-09-19 11:35:34 +08:00
commit 0ac771bad8
208 files changed
+13062 -977

No files matched your search

+14
View File
@@ -45,6 +45,20 @@ export function checkFallbackError(status, errorText, backoffLevel = 0) {
}
}
// Request-scoped client errors that matched no rule above: a 400 caused by the
// request itself (context overflow, malformed body, unsupported parameter) says
// nothing about the credential, so cooling the account down only removes a
// healthy connection from rotation. With a single connection it is worse: every
// later request in the window fails with a copy of this very error
// ("all 1 accounts locked for <model> | lastError=[400]: ..."), which hides the
// real cause from the caller and makes unrelated sessions look like they hit the
// same limit. Hand the upstream error back for this request instead.
// Account-scoped statuses keep their rules above (401/402/403/404/429), and the
// text rules still win for rate-limit / quota / capacity wording.
if (status >= 400 && status < 500 && status !== 401 && status !== 402 && status !== 403 && status !== 429) {
return { shouldFallback: false, cooldownMs: 0 };
}
// Default: transient cooldown for any unmatched error
return { shouldFallback: true, cooldownMs: TRANSIENT_COOLDOWN_MS };
}
+47 -15
View File
@@ -13,12 +13,10 @@ function buildModelListHeaders(token, isApiKey) {
}
/**
* Fetch ClinePass live model catalog from Cline's /models endpoint.
*
* @param {object} credentials - Connection credentials ({ accessToken, apiKey })
* @returns {Promise<{ models: { id: string, name: string }[] } | null>}
* Internal: fetch the raw model list from Cline's /models endpoint.
* Returns the parsed array or null on any failure.
*/
export async function resolveClinepassModels(credentials) {
async function fetchClineRawModels(credentials) {
const isApiKey = Boolean(credentials?.apiKey);
const token = isApiKey ? credentials.apiKey : credentials?.accessToken;
if (!token) return null;
@@ -39,19 +37,53 @@ export async function resolveClinepassModels(credentials) {
const json = await response.json();
const rawList = Array.isArray(json) ? json : json?.data;
if (!Array.isArray(rawList)) return null;
const models = rawList
.filter((m) => typeof m?.id === "string" && m.id.startsWith("cline-pass/"))
.map((m) => ({
id: m.id,
name: m.name || m.id,
}));
return models.length ? { models } : null;
return Array.isArray(rawList) ? rawList : null;
} catch {
return null;
} finally {
clearTimeout(timer);
}
}
/**
* Fetch ClinePass live model catalog from Cline's /models endpoint.
* Returns only models with the cline-pass/ prefix.
*
* @param {object} credentials - Connection credentials ({ accessToken, apiKey })
* @returns {Promise<{ models: { id: string, name: string }[] } | null>}
*/
export async function resolveClinepassModels(credentials) {
const rawList = await fetchClineRawModels(credentials);
if (!rawList) return null;
const models = rawList
.filter((m) => typeof m?.id === "string" && m.id.startsWith("cline-pass/"))
.map((m) => ({
id: m.id,
name: m.name || m.id,
}));
return models.length ? { models } : null;
}
/**
* Fetch Cline live model catalog from Cline's /models endpoint.
* Unlike resolveClinepassModels, this returns ALL models (including
* free-tier models like z-ai/glm-5.3-flash) without the cline-pass/ prefix filter.
*
* @param {object} credentials - Connection credentials ({ accessToken, apiKey })
* @returns {Promise<{ models: { id: string, name: string }[] } | null>}
*/
export async function resolveClineModels(credentials) {
const rawList = await fetchClineRawModels(credentials);
if (!rawList) return null;
const models = rawList
.filter((m) => typeof m?.id === "string" && m.id.trim() !== "")
.map((m) => ({
id: m.id,
name: m.name || m.id,
}));
return models.length ? { models } : null;
}
+2
View File
@@ -124,6 +124,8 @@ export async function getModelInfoCore(modelStr, aliasesOrGetter) {
// Config-driven prefix → provider inference (first match wins, fallback "openai").
const MODEL_PREFIX_PROVIDERS = [
// Codex CLI sends this bare virtual model for auto-review — keep it on OAuth Codex (#1398).
[/^codex-auto-review$/, "codex"],
[/^claude-/, "anthropic"],
[/^gemini-/, "gemini"],
[/^gpt-/, "openai"],
+8 -5
View File
@@ -203,7 +203,8 @@ async function onboardUser(accessToken, tierID, externalSignal, endpoints, provi
const reqBody = { tierId: tierID, metadata: LOAD_CODE_ASSIST_METADATA };
const headers = provider === "antigravity" ? ANTIGRAVITY_LOAD_CODE_ASSIST_HEADERS : LOAD_CODE_ASSIST_HEADERS;
const MAX_ATTEMPTS = 5;
const MAX_ATTEMPTS = Number(process.env.ONBOARD_MAX_ATTEMPTS) || 2;
const BASE_RETRY_DELAY_MS = Number(process.env.ONBOARD_RETRY_DELAY_MS) || 12_000;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
// Bail out immediately if the connection was removed
@@ -241,9 +242,10 @@ async function onboardUser(accessToken, tierID, externalSignal, endpoints, provi
throw new Error("onboardUser done but no project_id in response");
}
// Server not done yet – wait and retry
// Server not done yet – wait and retry with jitter
const jitter = Math.floor(Math.random() * 5000);
console.log(`[ProjectId] Onboard attempt ${attempt}/${MAX_ATTEMPTS}: not done yet, waiting...`);
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise(resolve => setTimeout(resolve, BASE_RETRY_DELAY_MS + jitter));
} catch (error) {
clearTimeout(timeoutId);
@@ -256,9 +258,10 @@ async function onboardUser(accessToken, tierID, externalSignal, endpoints, provi
console.warn(`[ProjectId] onboardUser failed after ${MAX_ATTEMPTS} attempts: ${error.message}`);
return null;
}
// Continue to next attempt instead of throwing (which would skip remaining retries)
// Wait with jitter before retrying
const jitter = Math.floor(Math.random() * 5000);
console.warn(`[ProjectId] onboardUser attempt ${attempt} failed: ${error.message}, retrying...`);
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise(resolve => setTimeout(resolve, BASE_RETRY_DELAY_MS + jitter));
} finally {
clearTimeout(timeoutId);
externalSignal?.removeEventListener("abort", forwardAbort);
+24
View File
@@ -343,6 +343,30 @@ export async function resolveQoderModels(credentials, options = {}) {
}
}
/**
* Every model key the chat endpoint accepts for this credential: the IDE-visible
* models first, then catalog entries flagged `enable:false` (hidden in the IDE
* picker, e.g. by an account policy, but still served by agent_chat_generation —
* see fetchQoderCatalogRaw). /v1/models uses this so the advertised list matches
* what the router will actually route instead of collapsing to one or two keys.
*/
export function routableQoderModels(catalog) {
if (!catalog) return [];
const out = [];
const seen = new Set();
for (const m of catalog.models || []) {
if (!m?.id || seen.has(m.id)) continue;
seen.add(m.id);
out.push({ id: m.id, name: m.name || m.id, hidden: false });
}
for (const [key, cfg] of catalog.rawConfigs || []) {
if (!key || seen.has(key)) continue;
seen.add(key);
out.push({ id: key, name: cfg?.display_name || key, hidden: true });
}
return out;
}
export function invalidateQoderCatalog(credentials) {
if (!credentials) return;
catalogCache.delete(cacheKey(credentials));
+199
View File
@@ -0,0 +1,199 @@
import { makeKv } from "../../src/lib/db/helpers/kvStore.js";
const MAX_SIGNATURES = 2000;
const MAX_PERSISTED_SIGNATURES = 10_000;
const MEMORY_TTL_MS = 1000 * 60 * 60; // 1 hour
const PERSISTED_TTL_MS = 1000 * 60 * 60 * 24 * 7; // 7 days
const SCOPE = "gemini_thought_signatures";
const signatureKv = makeKv(SCOPE);
const memorySignatures = new Map();
let pruneCounter = 0;
/**
* Model family that produced / will consume a signature. Antigravity serves Gemini and Claude
* models behind the same API, and each backend only accepts its own signatures: a Claude
* signature replayed to Gemini fails with 400 "Corrupted thought signature." (and vice versa).
*/
export function signatureFamily(model) {
const m = typeof model === "string" ? model.toLowerCase() : "";
if (!m) return null;
if (m.includes("claude")) return "claude";
if (m.includes("gemini")) return "gemini";
return m;
}
// Entries stored before families were recorded (no `family`) stay usable for any model.
function isCompatible(entry, family) {
return !entry.family || !family || entry.family === family;
}
function pruneMemoryExpired() {
const now = Date.now();
for (const [key, value] of memorySignatures.entries()) {
if (value.expiresAt <= now) {
memorySignatures.delete(key);
}
}
while (memorySignatures.size > MAX_SIGNATURES) {
const oldestKey = memorySignatures.keys().next().value;
if (!oldestKey) break;
memorySignatures.delete(oldestKey);
}
}
async function maybePrunePersisted() {
pruneCounter++;
if (pruneCounter % 100 !== 0) return;
try {
const all = await signatureKv.getAll();
const keys = Object.keys(all);
const now = Date.now();
const expiredKeys = [];
const valid = [];
for (const k of keys) {
const entry = all[k];
if (!entry || typeof entry.signature !== "string" || (entry.expiresAt && entry.expiresAt <= now)) {
expiredKeys.push(k);
} else {
valid.push({ key: k, createdAt: entry.createdAt || 0 });
}
}
for (const k of expiredKeys) {
await signatureKv.remove(k).catch(() => {});
}
if (valid.length > MAX_PERSISTED_SIGNATURES) {
valid.sort((a, b) => b.createdAt - a.createdAt);
const toRemove = valid.slice(MAX_PERSISTED_SIGNATURES);
for (const item of toRemove) {
await signatureKv.remove(item.key).catch(() => {});
}
}
} catch {
// Fail-open
}
}
/**
* Store a thought signature for a tool_call_id with optional sessionId namespace (RAM + SQLite async).
* `model` is the model that produced the signature; lookups for another model family skip it.
*/
export function storeGeminiThoughtSignature(toolCallId, signature, sessionId = null, model = null) {
if (typeof toolCallId !== "string" || !toolCallId) return;
if (typeof signature !== "string" || !signature) return;
const now = Date.now();
const family = signatureFamily(model);
pruneMemoryExpired();
const keys = [];
if (sessionId && typeof sessionId === "string") {
keys.push(`${sessionId}:${toolCallId}`);
}
keys.push(toolCallId);
for (const k of keys) {
memorySignatures.set(k, {
signature,
family,
expiresAt: now + MEMORY_TTL_MS,
});
// Async persist to SQLite kv table without blocking
signatureKv.set(k, {
signature,
family,
createdAt: now,
expiresAt: now + PERSISTED_TTL_MS,
}).catch(() => {});
}
maybePrunePersisted().catch(() => {});
}
/**
* Retrieve a thought signature by tool_call_id (RAM first, then SQLite fallback).
* `model` is the target model; signatures produced by another model family are ignored.
*/
export async function getGeminiThoughtSignature(toolCallId, sessionId = null, model = null) {
if (typeof toolCallId !== "string" || !toolCallId) return null;
const family = signatureFamily(model);
pruneMemoryExpired();
if (sessionId && typeof sessionId === "string") {
const sessionKey = `${sessionId}:${toolCallId}`;
const sessionEntry = memorySignatures.get(sessionKey);
if (sessionEntry && sessionEntry.expiresAt > Date.now() && isCompatible(sessionEntry, family)) {
return sessionEntry.signature;
}
}
const entry = memorySignatures.get(toolCallId);
if (entry && entry.expiresAt > Date.now() && isCompatible(entry, family)) {
return entry.signature;
}
try {
if (sessionId && typeof sessionId === "string") {
const sessionKey = `${sessionId}:${toolCallId}`;
const sessionRow = await signatureKv.get(sessionKey);
if (sessionRow && typeof sessionRow.signature === "string" && (!sessionRow.expiresAt || sessionRow.expiresAt > Date.now()) && isCompatible(sessionRow, family)) {
memorySignatures.set(sessionKey, {
signature: sessionRow.signature,
family: sessionRow.family || null,
expiresAt: Date.now() + MEMORY_TTL_MS,
});
return sessionRow.signature;
}
}
const row = await signatureKv.get(toolCallId);
if (row && typeof row.signature === "string") {
if (row.expiresAt && row.expiresAt <= Date.now()) {
signatureKv.remove(toolCallId).catch(() => {});
return null;
}
if (!isCompatible(row, family)) return null;
memorySignatures.set(toolCallId, {
signature: row.signature,
family: row.family || null,
expiresAt: Date.now() + MEMORY_TTL_MS,
});
return row.signature;
}
} catch {
// Fail-open
}
return null;
}
/**
* Synchronous get from RAM cache only (for sync translators).
* `model` is the target model; signatures produced by another model family are ignored.
*/
export function getGeminiThoughtSignatureSync(toolCallId, sessionId = null, model = null) {
if (typeof toolCallId !== "string" || !toolCallId) return null;
const family = signatureFamily(model);
pruneMemoryExpired();
if (sessionId && typeof sessionId === "string") {
const sessionKey = `${sessionId}:${toolCallId}`;
const sessionEntry = memorySignatures.get(sessionKey);
if (sessionEntry && sessionEntry.expiresAt > Date.now() && isCompatible(sessionEntry, family)) {
return sessionEntry.signature;
}
}
const entry = memorySignatures.get(toolCallId);
if (entry && entry.expiresAt > Date.now() && isCompatible(entry, family)) {
return entry.signature;
}
return null;
}
+2
View File
@@ -148,6 +148,8 @@ const REFRESH_HANDLERS = {
"codebuddy-intl": (c, log) => refreshCodebuddyIntlToken(c.refreshToken, log),
trae: (c, log) => refreshTraeToken(c.refreshToken, c, log),
cline: (c, log) => refreshClineToken(c.refreshToken, log),
// ClinePass shares Cline's WorkOS auth endpoints, so the same refresh works.
clinepass: (c, log) => refreshClineToken(c.refreshToken, log),
zed: () => refreshZedToken(),
windsurf: (c, log) => refreshWindsurfToken(c, log),
// Kimi Code OAuth (merged into id `kimi`); legacy id still routes here
+6
View File
@@ -15,10 +15,13 @@ import { getGrokCliUsage } from "./usage/grok-cli.js";
import { getKimiUsage } from "./usage/kimi.js";
import { getDeepseekUsage } from "./usage/deepseek.js";
import { getFreebuffUsage } from "./usage/freebuff.js";
import { getOpenCodeGoUsage } from "./usage/opencode-go.js";
import { getGroqUsage } from "./usage/groq.js";
import { getZedUsage } from "./usage/zed.js";
import { getXiaomiMimoUsage } from "./usage/xiaomi-mimo.js";
import { resolveQoderCredentials } from "./qoderModels.js";
import { getGlmUsage } from "./usage/glm.js";
import { getCommandCodeUsage } from "./usage/commandcode.js";
import {
getIflowUsage,
getOllamaUsage,
@@ -56,10 +59,13 @@ 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),
freebuff: (c) => getFreebuffUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
groq: (c) => getGroqUsage(c.apiKey, c.proxyOptions),
zed: (c) => getZedUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
"xiaomi-mimo": (c) => getXiaomiMimoUsage(c.accessToken, c.providerSpecificData, c.proxyOptions),
commandcode: (c) => getCommandCodeUsage(c.apiKey, c.proxyOptions),
};
export async function getUsageForProvider(connection, proxyOptions = null, options = {}) {
@@ -0,0 +1,150 @@
/**
* Antigravity weekly quota — best-effort retrieval from retrieveUserQuotaSummary.
* Failure never breaks existing per-model quota display.
*/
import { U, parseResetTime, fetchWithTimeout } from "./shared.js";
import { ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_IDE_VERSION } from "../../providers/shared.js";
// — Weekly quota summary config ——————————————————————————————
const WEEKLY_CONFIG = {
...U("antigravity"),
userAgent: ANTIGRAVITY_IDE_USER_AGENT,
};
// — Cache: TTL + in-flight dedup per project ———————————————
const WEEKLY_CACHE_TTL_MS = 180_000; // 3 minutes
const weeklyCache = new Map(); // cacheKey -> { result, expiresAt } | { promise }
function cacheKey(accessToken, projectId) {
return `${accessToken}::${projectId || ""}`;
}
// Exported for tests only
export function _clearWeeklyCache() {
weeklyCache.clear();
}
// — Group-name to stable key mapping ——————————————————————
const GROUP_MATCHERS = [
{ pattern: /gemini/i, key: "gemini_weekly", displayName: "Gemini (Weekly)" },
{ pattern: /claude|gpt/i, key: "claude_gpt_weekly", displayName: "Claude & GPT (Weekly)" },
];
/**
* Parse a retrieveUserQuotaSummary response into normalized weekly quotas.
* Pure function — safe to unit-test without network.
*
* @param {Object|null} data Raw JSON response
* @returns {Object} e.g. { gemini_weekly: { used, total, ... }, claude_gpt_weekly: { ... } }
*/
export function parseWeeklyQuotaSummary(data) {
if (!data || typeof data !== "object") return {};
// Groups may live at data.groups or data.quotaSummary.groups
const groups = Array.isArray(data.groups)
? data.groups
: Array.isArray(data.quotaSummary?.groups)
? data.quotaSummary.groups
: null;
if (!groups) return {};
const result = {};
for (const group of groups) {
if (!group || typeof group !== "object") continue;
const displayName = group.displayName || "";
const buckets = Array.isArray(group.buckets) ? group.buckets : [];
for (const bucket of buckets) {
if (!bucket || typeof bucket !== "object") continue;
// Identify weekly buckets by checking bucketId + displayName for "weekly"
const bucketText = `${bucket.bucketId || ""} ${bucket.displayName || ""}`.toLowerCase();
if (!bucketText.includes("weekly")) continue;
// Skip disabled buckets
if (bucket.disabled === true) continue;
const remainingFraction = Number(bucket.remainingFraction);
if (!Number.isFinite(remainingFraction)) continue;
// Match group to a known family
for (const matcher of GROUP_MATCHERS) {
if (matcher.pattern.test(displayName)) {
const total = 1000;
const remaining = Math.round(total * remainingFraction);
const used = Math.max(0, total - remaining);
result[matcher.key] = {
used,
total,
resetAt: parseResetTime(bucket.resetTime),
remainingPercentage: remainingFraction * 100,
unlimited: false,
displayName: matcher.displayName,
};
break; // first matching bucket per family wins
}
}
}
}
return result;
}
/**
* Fetch weekly quota summary — cached, deduped, never throws.
*/
export async function fetchAntigravityWeeklyQuota(accessToken, projectId, proxyOptions = null) {
const key = cacheKey(accessToken, projectId);
// Serve in-flight or cached
const hit = weeklyCache.get(key);
if (hit?.promise) return hit.promise;
if (hit && hit.expiresAt > Date.now()) return hit.result;
const promise = (async () => {
try {
const url = WEEKLY_CONFIG.quotaSummaryApiUrl;
if (!url) return {};
const response = await fetchWithTimeout(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"User-Agent": WEEKLY_CONFIG.userAgent,
"Content-Type": "application/json",
"X-Client-Name": "antigravity",
"X-Client-Version": ANTIGRAVITY_IDE_VERSION,
},
body: JSON.stringify({
...(projectId ? { project: projectId } : {}),
}),
}, 10000, proxyOptions);
if (!response.ok) return {};
const data = await response.json();
return parseWeeklyQuotaSummary(data);
} catch {
return {};
}
})();
weeklyCache.set(key, { promise });
try {
const result = await promise;
if (result && Object.keys(result).length > 0) {
weeklyCache.set(key, { result, expiresAt: Date.now() + WEEKLY_CACHE_TTL_MS });
} else {
weeklyCache.delete(key);
}
return result;
} catch {
weeklyCache.delete(key);
return {};
}
}
+16
View File
@@ -110,6 +110,22 @@ async function fetchClaudeUsageRaw(accessToken, proxyOptions = null) {
}
}
// Model-scoped weekly limits (e.g. Fable) arrive in limits[], not as
// seven_day_* keys: { kind: "weekly_scoped", percent, resets_at,
// scope: { model: { display_name: "Fable" } } }. No limits entry means
// the account has no such window — omit the row, never fabricate one.
if (Array.isArray(data.limits)) {
for (const limit of data.limits) {
if (limit?.kind !== "weekly_scoped") continue;
const modelName = String(limit?.scope?.model?.display_name || "").trim().toLowerCase();
if (!modelName || typeof limit.percent !== "number") continue;
quotas[`weekly ${modelName} (7d)`] = createQuotaObject({
utilization: Math.max(0, Math.min(100, limit.percent)),
resets_at: limit.resets_at,
});
}
}
return {
plan: "Claude Code",
extraUsage: data.extra_usage ?? null,
+8 -1
View File
@@ -21,6 +21,13 @@ function toIsoDate(value) {
return Number.isFinite(time) ? date.toISOString() : null;
}
function errorMessage(value, fallback) {
if (!value) return fallback;
if (typeof value === "string") return value;
if (typeof value.message === "string") return value.message;
return JSON.stringify(value);
}
function getCodexAccountId(providerSpecificData) {
return providerSpecificData?.workspaceId || providerSpecificData?.accountId || providerSpecificData?.chatgptAccountId || null;
}
@@ -162,7 +169,7 @@ export async function getCodexRateLimitResetCredits(accessToken, proxyOptions =
}
if (!response.ok) {
const message = data?.message || data?.error || data?.detail || `Codex reset credits API unavailable (${response.status}).`;
const message = errorMessage(data?.message || data?.error || data?.detail, `Codex reset credits API unavailable (${response.status}).`);
throw new Error(message);
}
+134
View File
@@ -0,0 +1,134 @@
/**
* Command Code usage — billing credits + 5h/weekly rate windows.
* Mirrors ~/cc-usage.mjs: whoami → credits + subscriptions.
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { parseResetTime, toFiniteNumber } from "./shared.js";
const BASE = (process.env.COMMAND_CODE_API_BASE_URL || "https://api.commandcode.ai").replace(/\/$/, "");
const PLAN_NAMES = {
"individual-go": "Go",
"individual-goat": "GOAT",
"individual-pro": "Pro",
"individual-pro-v1": "Pro",
"individual-provider": "Provider",
"individual-max": "Max",
"individual-ultra": "Ultra",
"teams-pro": "Teams Pro",
};
const PLAN_CAPS = {
"individual-go": 10,
"individual-goat": 70,
"individual-pro": 30,
"individual-pro-v1": 80,
"individual-provider": 15,
"individual-max": 150,
"individual-ultra": 300,
"teams-pro": 40,
};
function qs(route, params) {
const s = new URLSearchParams(
Object.entries(params || {}).filter(([, v]) => v != null),
).toString();
return s ? `${route}?${s}` : route;
}
function windowQuota(win) {
if (!win || typeof win !== "object") return null;
const used = toFiniteNumber(win.used, 0);
const total = toFiniteNumber(win.cap, 0);
if (total <= 0 && used <= 0) return null;
return {
used,
total,
remaining: Math.max(0, total - used),
unlimited: false,
resetAt: parseResetTime(win.resetAt),
};
}
/**
* @param {string|null|undefined} apiKey
* @param {object|null} proxyOptions
*/
export async function getCommandCodeUsage(apiKey, proxyOptions = null) {
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
return { message: "Command Code API key not available. Add a key to view usage." };
}
const headers = {
Authorization: `Bearer ${apiKey.trim()}`,
Accept: "application/json",
};
const get = async (route) => {
const response = await proxyAwareFetch(
BASE + route,
{ method: "GET", headers },
proxyOptions,
);
return response;
};
try {
const whoamiRes = await get(qs("/alpha/whoami", { limits: "1" }));
if (whoamiRes.status === 401 || whoamiRes.status === 403) {
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
}
if (!whoamiRes.ok) {
return { plan: "Command Code", message: `Command Code usage API error (${whoamiRes.status})` };
}
const whoami = await whoamiRes.json().catch(() => ({}));
const orgId = whoami?.org?.id ?? null;
const [creditsRes, subsRes] = await Promise.all([
get(qs("/alpha/billing/credits", { orgId })),
get(qs("/alpha/billing/subscriptions", { orgId })),
]);
if (creditsRes.status === 401 || creditsRes.status === 403 || subsRes.status === 401 || subsRes.status === 403) {
return { plan: "Command Code", message: "Command Code authentication failed. Check the API key." };
}
if (!creditsRes.ok) {
return { plan: "Command Code", message: `Command Code credits API error (${creditsRes.status})` };
}
if (!subsRes.ok) {
return { plan: "Command Code", message: `Command Code subscriptions API error (${subsRes.status})` };
}
const creditsBody = await creditsRes.json().catch(() => ({}));
const subsBody = await subsRes.json().catch(() => ({}));
const planId = subsBody?.data?.planId ?? null;
const plan = (planId && PLAN_NAMES[planId]) || planId || "Command Code";
const cap = planId ? (PLAN_CAPS[planId] || 0) : 0;
const c = creditsBody?.credits || {};
const remaining =
toFiniteNumber(c.monthlyCredits, 0) +
toFiniteNumber(c.purchasedCredits, 0) +
toFiniteNumber(c.freeCredits, 0);
const used = cap > 0 ? Math.max(0, cap - remaining) : 0;
const total = cap > 0 ? cap : remaining;
const quotas = {};
quotas.Credits = {
used,
total,
remaining,
unlimited: cap <= 0,
resetAt: parseResetTime(subsBody?.data?.currentPeriodEnd),
};
const fiveHour = windowQuota(creditsBody?.windowLimits?.fiveHour);
if (fiveHour) quotas["Session (5h)"] = fiveHour;
const weekly = windowQuota(creditsBody?.windowLimits?.weekly);
if (weekly) quotas.Weekly = weekly;
return { plan, quotas };
} catch (error) {
return { message: `Command Code error: ${error.message}` };
}
}
+4 -3
View File
@@ -91,14 +91,15 @@ export async function getDeepseekUsage(apiKey = null, proxyOptions = null) {
const quotas = {};
for (const b of balances) {
const total = Math.max(0, b.totalBalance);
// Credit pot: show full remaining against current balance; never set absolute
// `remaining` — QuotaTable treats it as a 0–100 percentage.
// Credit balance: show as "Credit: $X.XX USD" not a usage quota
quotas[`Balance (${b.currency})`] = {
used: 0,
total,
remainingPercentage: total > 0 ? 100 : 0,
resetAt: null,
unlimited: total > 0,
unlimited: false,
isCreditBalance: true,
currency: b.currency,
};
}
+60 -2
View File
@@ -5,6 +5,7 @@
import { CLIENT_METADATA } from "../../config/appConstants.js";
import { ANTIGRAVITY_IDE_USER_AGENT, ANTIGRAVITY_IDE_VERSION, ANTIGRAVITY_OAUTH_CLIENT } from "../../providers/shared.js";
import { U, parseResetTime, normalizeCloudCodeProjectId, fetchWithTimeout } from "./shared.js";
import { fetchAntigravityWeeklyQuota } from "./antigravity-weekly.js";
// Antigravity API config (from Quotio) — urls from registry, oauth client + dynamic UA kept here
const ANTIGRAVITY_CONFIG = {
@@ -157,8 +158,15 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
const data = await response.json();
const quotas = {};
// Parse model quotas (inspired by vscode-antigravity-cockpit)
if (data.models) {
// Detect tier: free-tier accounts only have weekly quotas (no separate 5h window).
// On free-tier, fetchAvailableModels returns misleading per-model quota info
// (missing remainingFraction defaults to 0, or reflects the weekly limit not a 5h window).
const paidTierId = subscriptionInfo?.paidTier?.id;
const isFreeTier = !paidTierId || paidTierId === "free-tier";
// Parse model quotas only for paid-tier accounts.
// Free-tier accounts skip this — their only meaningful quota is the weekly limit.
if (!isFreeTier && data.models) {
// Filter only recommended/important models (must match PROVIDER_MODELS ag ids)
const importantModels = [
'gemini-3.8-flash-high',
@@ -212,6 +220,56 @@ export async function getAntigravityUsage(accessToken, providerSpecificData, pro
}
}
// Best-effort weekly quota overlay — never blocks or breaks per-model results
try {
const weeklyQuotas = await fetchAntigravityWeeklyQuota(
accessToken,
projectId,
proxyOptions
);
// Reconcile weekly quota against model family status:
// If every model in a family is locked/exhausted (remainingPercentage === 0)
// until a future reset time, the weekly limit cannot be 100% available.
// On Google's Free Starter tier, retrieveUserQuotaSummary buggily reports
// remainingFraction: 1 even after the starter quota is depleted and all models 429.
const entries = Object.entries(quotas);
const geminiModels = entries.filter(([k]) => k.startsWith("gemini-") && !k.includes("image"));
const claudeModels = entries.filter(([k]) => k.startsWith("claude-"));
if (weeklyQuotas.gemini_weekly && geminiModels.length > 0) {
const allGeminiExhausted = geminiModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
if (allGeminiExhausted && weeklyQuotas.gemini_weekly.remainingPercentage > 0) {
const maxResetAt = geminiModels.reduce((max, [, q]) =>
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
);
weeklyQuotas.gemini_weekly.used = weeklyQuotas.gemini_weekly.total;
weeklyQuotas.gemini_weekly.remainingPercentage = 0;
if (maxResetAt) {
weeklyQuotas.gemini_weekly.resetAt = maxResetAt;
}
}
}
if (weeklyQuotas.claude_gpt_weekly && claudeModels.length > 0) {
const allClaudeExhausted = claudeModels.every(([, q]) => (q.remainingPercentage ?? 0) === 0);
if (allClaudeExhausted && weeklyQuotas.claude_gpt_weekly.remainingPercentage > 0) {
const maxResetAt = claudeModels.reduce((max, [, q]) =>
!max || (q.resetAt && new Date(q.resetAt) > new Date(max)) ? q.resetAt : max, null
);
weeklyQuotas.claude_gpt_weekly.used = weeklyQuotas.claude_gpt_weekly.total;
weeklyQuotas.claude_gpt_weekly.remainingPercentage = 0;
if (maxResetAt) {
weeklyQuotas.claude_gpt_weekly.resetAt = maxResetAt;
}
}
}
Object.assign(quotas, weeklyQuotas);
} catch {
// Silently ignore — weekly is best-effort
}
return {
plan: subscriptionInfo?.currentTier?.name || "Unknown",
quotas,
+107
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}` };
}
}
+125
View File
@@ -0,0 +1,125 @@
/**
* Xiaomi MiMo usage — weekly quota from the Xiaomi account session.
*
* Primary path: GET {mimo-server}/api/user/usage authorized by the account-session
* cookie (see shared/mimoAccount.js). Response: { code: 0, data: { percent (remaining
* %), resetDate, resetAt } }.
*
* Fallback: the sk- API key cannot read the quota, so when no account session is
* available we surface a graceful message instead of failing.
*/
import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { getMimoAccountUsage } from "../../shared/mimoAccount.js";
const USAGE_URL = "https://aistudio.xiaomimimo.com/open-apis/v1/user/usage";
/**
* @param {string|null|undefined} accessToken - sk- API key
* @param {object|null} providerSpecificData - may contain mimoPassToken, uid, etc.
* @param {object|null} proxyOptions
*/
export async function getXiaomiMimoUsage(accessToken = null, providerSpecificData = null, proxyOptions = null) {
// Preferred path: the weekly quota comes from the account service session
// (mimo-server /api/user/usage), which the sk- key cannot reach. The session is
// derived from MiMo Desktop's persisted passToken via the SSO/sts handshake.
const account = await getMimoAccountUsage(providerSpecificData, proxyOptions);
if (typeof account.percent === "number" && Number.isFinite(account.percent)) {
const remaining = Math.max(0, Math.min(100, Math.round(account.percent)));
const used = 100 - remaining;
let resetAt = null;
if (typeof account.resetAt === "number" && account.resetAt > 0) {
resetAt = new Date(account.resetAt * 1000).toISOString();
} else if (typeof account.resetDate === "string") {
const parsed = new Date(`${account.resetDate}T00:00:00Z`);
if (!Number.isNaN(parsed.getTime())) resetAt = parsed.toISOString();
}
return {
plan: "Xiaomi MiMo Desktop",
quotas: {
Weekly: { used, total: 100, remainingPercentage: remaining, resetAt, unlimited: false },
},
};
}
// Fallback: no account session available (Desktop never logged in, or its cookie
// store is locked). The sk- key cannot read the quota, so surface a clear message.
const key = accessToken || providerSpecificData?.apiKey;
if (!key || typeof key !== "string" || !key.trim()) {
return { message: "Xiaomi MiMo Desktop not connected. Add credentials to view usage." };
}
try {
const response = await proxyAwareFetch(
USAGE_URL,
{
method: "GET",
headers: {
Authorization: `Bearer ${key.trim()}`,
"X-Mimo-Source": "mimocode-cli",
Accept: "application/json",
},
signal: AbortSignal.timeout(10000),
},
proxyOptions,
);
if (response.status === 401) {
return {
plan: "Xiaomi MiMo Desktop",
message: "Weekly quota requires Xiaomi account session. API key alone is insufficient.",
};
}
if (!response.ok) {
return {
plan: "Xiaomi MiMo Desktop",
message: `Usage API error (${response.status})`,
};
}
const data = await response.json().catch(() => null);
if (!data || data.code !== 0 || !data.data) {
return {
plan: "Xiaomi MiMo Desktop",
message: "Usage endpoint returned unexpected response.",
};
}
const { percent, resetDate } = data.data;
if (typeof percent !== "number" || !Number.isFinite(percent)) {
return {
plan: "Xiaomi MiMo Desktop",
message: "Usage data missing percent field.",
};
}
// percent = remaining percentage (e.g. 94 means 94% remaining)
const remaining = Math.max(0, Math.min(100, Math.round(percent)));
const used = 100 - remaining;
// Parse resetDate — expected format "2026-09-16"
let resetAt = null;
if (resetDate && typeof resetDate === "string") {
const parsed = new Date(`${resetDate}T00:00:00Z`);
if (!Number.isNaN(parsed.getTime())) {
resetAt = parsed.toISOString();
}
}
return {
plan: "Xiaomi MiMo Desktop",
quotas: {
Weekly: {
used,
total: 100,
remainingPercentage: remaining,
resetAt,
unlimited: false,
},
},
};
} catch (error) {
return { message: `Xiaomi MiMo Desktop usage error: ${error.message}` };
}
}