Support quota scraping for OpenCode Go and Ollama Cloud (#4642)

Integrated into release/v3.8.34 (Ollama Cloud + OpenCode Go dashboard quota scraping; rebuilt onto tip, gates green: typecheck/public-creds/file-size/lint/docs-sync + 31 tests)
This commit is contained in:
Jan Leon
2026-06-22 23:40:10 +02:00
committed by GitHub
parent 56eb7f0206
commit 4e9d4f4eb7
31 changed files with 1955 additions and 688 deletions

View File

@@ -384,6 +384,22 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
# OpenCode Go dashboard quota scraping. Prefer configuring these per connection
# in Dashboard → Providers → OpenCode Go. Env vars are useful for headless
# deployments or shared server defaults. The cookie is sensitive.
#OPENCODE_GO_WORKSPACE_ID=wrk_...
#OMNIROUTE_OPENCODE_GO_WORKSPACE_ID=wrk_...
#OPENCODE_GO_AUTH_COOKIE=auth=...
#OMNIROUTE_OPENCODE_GO_AUTH_COOKIE=auth=...
# Ollama Cloud quota scraping. Prefer configuring this per connection in
# Dashboard → Providers → Ollama Cloud. The cookie is sensitive.
#OLLAMA_USAGE_COOKIE=__Secure-session=...
#OLLAMA_CLOUD_USAGE_COOKIE=__Secure-session=...
#OMNIROUTE_OLLAMA_USAGE_COOKIE=__Secure-session=...
# ═══════════════════════════════════════════════════════════════════════════════
# 8. OUTBOUND PROXY (Upstream Provider Calls)

View File

@@ -265,6 +265,15 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
| `OMNIROUTE_GEMINI_CLI_USAGE_URL` | `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | `open-sse/services/usage.ts` | Gemini CLI quota lookup endpoint. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_QUOTA_URL` | `https://opencode.ai/zen/go/v1/quota` | `open-sse/services/opencodeQuotaFetcher.ts` | OpenCode (zen/go) quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_GO_QUOTA_URL` | `https://api.z.ai/api/monitor/usage/quota/limit` | `open-sse/services/usage.ts` | OpenCode Go quota lookup endpoint used by the Usage page. Override for relays / test fixtures. |
| `OMNIROUTE_OPENCODE_GO_DASHBOARD_URL` | `https://opencode.ai/workspace` | `open-sse/services/usage.ts` | OpenCode Go dashboard base URL used for quota scraping when a workspace ID and auth cookie are configured. Override for relays / test fixtures. |
| `OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go workspace ID used for dashboard quota scraping. Prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_OPENCODE_GO_WORKSPACE_ID` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go workspace ID env var used before the shorter alias. Prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OPENCODE_GO_AUTH_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | OpenCode Go `auth` cookie used for dashboard quota scraping. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_OPENCODE_GO_AUTH_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Alternate OpenCode Go `auth` cookie env var used before the shorter alias. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_OLLAMA_CLOUD_USAGE_URL` | `https://ollama.com/settings` | `open-sse/services/usage.ts` | Ollama Cloud settings URL used for quota scraping. Override for relays / test fixtures. |
| `OLLAMA_USAGE_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Ollama Cloud `__Secure-session` cookie used for settings-page quota scraping. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OLLAMA_CLOUD_USAGE_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Alternate Ollama Cloud `__Secure-session` cookie env var. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_OLLAMA_USAGE_COOKIE` | _(unset)_ | `open-sse/services/usage.ts` | Alternate Ollama Cloud `__Secure-session` cookie env var used before the shorter aliases. Sensitive; prefer the per-connection Dashboard field when multiple accounts are configured. |
| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) usage limits endpoint. Override for relays / test fixtures. |
> [!IMPORTANT]

View File

@@ -0,0 +1,558 @@
import { sanitizeErrorMessage } from "../utils/error.ts";
type JsonRecord = Record<string, unknown>;
type UsageQuota = {
used: number;
total: number;
remaining?: number;
remainingPercentage?: number;
resetAt: string | null;
unlimited: boolean;
displayName?: string;
details?: Array<{ name: string; used: number }>;
currency?: string;
};
const OPENCODE_GO_QUOTA_URL =
process.env.OMNIROUTE_OPENCODE_GO_QUOTA_URL ?? "https://api.z.ai/api/monitor/usage/quota/limit";
const OPENCODE_GO_DASHBOARD_BASE_URL =
process.env.OMNIROUTE_OPENCODE_GO_DASHBOARD_URL ?? "https://opencode.ai/workspace";
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;
const OPENCODE_GO_SCRAPED_NUMBER = String.raw`(-?\d+(?:\.\d+)?)`;
const OLLAMA_CLOUD_USAGE_URL =
process.env.OMNIROUTE_OLLAMA_CLOUD_USAGE_URL ?? "https://ollama.com/settings";
const OLLAMA_CLOUD_SESSION_COOKIE = "__Secure-session";
type OpenCodeGoQuotaName = (typeof OPENCODE_GO_QUOTA_ORDER)[number];
type DashboardWindow = { usagePercent: number; resetAt: string | null };
type OpenCodeGoDashboardUsage = Partial<Record<OpenCodeGoQuotaName, DashboardWindow>>;
type OpenCodeGoDashboardConfig =
| { state: "configured"; workspaceId: string; authCookie: string }
| { state: "incomplete"; missing: string }
| { state: "none" };
type OllamaCloudUsage = {
session?: DashboardWindow;
weekly?: DashboardWindow;
planTier?: string | null;
};
type OllamaCloudConfig =
| { state: "configured"; cookie: string }
| { state: "invalid"; error: string }
| { state: "none" };
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNumber(value: unknown, fallback = 0): number {
const parsed =
typeof value === "number"
? value
: typeof value === "string" && value.trim().length > 0
? Number(value)
: Number.NaN;
return Number.isFinite(parsed) ? parsed : fallback;
}
function toPercentage(value: unknown): number {
return Math.max(0, Math.min(100, toNumber(value, 0)));
}
function toTitleCase(value: string): string {
return value
.trim()
.split(/[\s_-]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
.join(" ");
}
function roundCurrency(value: number): number {
return Math.round(value * 100) / 100;
}
function safeToIsoString(time: number): string | null {
if (!Number.isFinite(time) || time < 0 || time > 8.64e15) return null;
try {
return new Date(time).toISOString();
} catch {
return null;
}
}
function parseResetTime(resetValue: unknown): string | null {
const numeric = toNumber(resetValue, Number.NaN);
if (Number.isFinite(numeric) && numeric > 0) return safeToIsoString(numeric);
if (typeof resetValue !== "string" || !resetValue.trim()) return null;
const parsed = Date.parse(resetValue);
return Number.isFinite(parsed) ? safeToIsoString(parsed) : null;
}
function getProviderSpecificString(data: JsonRecord | undefined, keys: string[]): string {
const obj = toRecord(data);
for (const key of keys) {
const value = obj[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
function resolveOpenCodeGoDashboardConfig(
providerSpecificData?: JsonRecord
): OpenCodeGoDashboardConfig {
const workspaceId =
process.env.OMNIROUTE_OPENCODE_GO_WORKSPACE_ID?.trim() ||
process.env.OPENCODE_GO_WORKSPACE_ID?.trim() ||
getProviderSpecificString(providerSpecificData, [
"openCodeGoWorkspaceId",
"opencodeGoWorkspaceId",
"workspaceId",
]);
const authCookie =
process.env.OMNIROUTE_OPENCODE_GO_AUTH_COOKIE?.trim() ||
process.env.OPENCODE_GO_AUTH_COOKIE?.trim() ||
getProviderSpecificString(providerSpecificData, [
"openCodeGoAuthCookie",
"opencodeGoAuthCookie",
"authCookie",
]);
if (!workspaceId && !authCookie) return { state: "none" };
if (workspaceId && authCookie) return { state: "configured", workspaceId, authCookie };
return {
state: "incomplete",
missing: workspaceId ? "OPENCODE_GO_AUTH_COOKIE" : "OPENCODE_GO_WORKSPACE_ID",
};
}
function normalizeOpenCodeGoAuthCookie(value: string): string {
return value
.trim()
.replace(/^auth=/i, "")
.trim();
}
function buildBearerAuthorization(value: string): string {
const token = value
.trim()
.replace(/^Bearer\s+/i, "")
.trim();
return token ? `Bearer ${token}` : "";
}
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 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));
return {
used,
total,
remaining,
remainingPercentage:
total > 0 ? Math.max(0, Math.min(100, Math.round((remaining / total) * 100))) : 100,
resetAt,
unlimited: false,
displayName:
quotaName === "session" ? "5-hour rolling" : quotaName === "weekly" ? "Weekly" : "Monthly",
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 parseOpenCodeGoSsrWindow(html: string, field: string): DashboardWindow | null {
for (const candidate of [
{
usageIndex: 1,
resetIndex: 2,
pattern: String.raw`${field}:\$R\[\d+\]=\{[^}]*usagePercent:${OPENCODE_GO_SCRAPED_NUMBER}[^}]*resetInSec:${OPENCODE_GO_SCRAPED_NUMBER}[^}]*\}`,
},
{
usageIndex: 2,
resetIndex: 1,
pattern: String.raw`${field}:\$R\[\d+\]=\{[^}]*resetInSec:${OPENCODE_GO_SCRAPED_NUMBER}[^}]*usagePercent:${OPENCODE_GO_SCRAPED_NUMBER}[^}]*\}`,
},
]) {
const match = new RegExp(candidate.pattern).exec(html);
if (!match) continue;
const usagePercent = toNumber(match[candidate.usageIndex], Number.NaN);
const resetInSec = toNumber(match[candidate.resetIndex], Number.NaN);
if (Number.isFinite(usagePercent) && Number.isFinite(resetInSec)) {
return {
usagePercent,
resetAt: safeToIsoString(Date.now() + Math.max(0, resetInSec) * 1000),
};
}
}
return null;
}
function parseOpenCodeGoHumanReset(value: string): number | null {
const text = value.toLowerCase().replace(/\s+/g, " ").trim();
if (["reset-now", "reset now", "now", "resets now"].includes(text)) return 0;
const days = text.match(/(\d+(?:\.\d+)?)\s*days?/);
const hours = text.match(/(\d+(?:\.\d+)?)\s*hours?/);
const minutes = text.match(/(\d+(?:\.\d+)?)\s*minutes?/);
const seconds = text.match(/(\d+(?:\.\d+)?)\s*seconds?/);
if (!days && !hours && !minutes && !seconds) return null;
return (
toNumber(days?.[1], 0) * 86_400 +
toNumber(hours?.[1], 0) * 3_600 +
toNumber(minutes?.[1], 0) * 60 +
toNumber(seconds?.[1], 0)
);
}
function parseOpenCodeGoDashboardHtml(html: string): OpenCodeGoDashboardUsage | null {
const usage: OpenCodeGoDashboardUsage = {
session: parseOpenCodeGoSsrWindow(html, "rollingUsage") ?? undefined,
weekly: parseOpenCodeGoSsrWindow(html, "weeklyUsage") ?? undefined,
mcp_monthly: parseOpenCodeGoSsrWindow(html, "monthlyUsage") ?? undefined,
};
if (usage.session || usage.weekly || usage.mcp_monthly) return usage;
for (const content of html.split(/data-slot="usage-item"/).slice(1)) {
const label = content
.match(/data-slot="usage-label">([^<]+)</)?.[1]
?.trim()
.toLowerCase();
const usagePercent = toNumber(
content.match(/data-slot="usage-value">[^0-9]*(\d+(?:\.\d+)?)/)?.[1],
Number.NaN
);
const resetMatch = content.match(/data-slot="(reset-time|reset-now)">([\s\S]*?)<\/span>/);
if (!label || !Number.isFinite(usagePercent) || !resetMatch) continue;
const resetInSec =
resetMatch[1] === "reset-now"
? 0
: parseOpenCodeGoHumanReset(
resetMatch[2].replace(/<!--\$-->|<!--\/-->/g, "").replace(/Resets?\s*in\s*/i, "")
);
if (resetInSec === null || !Number.isFinite(resetInSec)) continue;
const window = {
usagePercent,
resetAt: safeToIsoString(Date.now() + Math.max(0, resetInSec) * 1000),
};
if (label.includes("rolling")) usage.session = window;
else if (label.includes("weekly")) usage.weekly = window;
else if (label.includes("monthly")) usage.mcp_monthly = window;
}
return usage.session || usage.weekly || usage.mcp_monthly ? usage : null;
}
async function fetchOpenCodeGoDashboardUsage(
config: Extract<OpenCodeGoDashboardConfig, { state: "configured" }>
) {
const url = `${OPENCODE_GO_DASHBOARD_BASE_URL.replace(/\/+$/, "")}/${encodeURIComponent(
config.workspaceId
)}/go`;
const response = await fetch(url, {
headers: {
Accept: "text/html",
Cookie: `auth=${normalizeOpenCodeGoAuthCookie(config.authCookie)}`,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0",
},
signal: AbortSignal.timeout(10_000),
});
if (!response.ok)
return { usage: null, message: `OpenCode Go dashboard error (${response.status}).` };
const usage = parseOpenCodeGoDashboardHtml(await response.text());
return {
usage,
message: usage ? undefined : "OpenCode Go dashboard response did not contain quota windows.",
};
}
export async function getOpenCodeGoUsage(apiKey: string, providerSpecificData?: JsonRecord) {
const dashboardConfig = resolveOpenCodeGoDashboardConfig(providerSpecificData);
if (dashboardConfig.state === "incomplete") {
return {
message: `OpenCode Go dashboard quota config is incomplete. Missing ${dashboardConfig.missing}.`,
};
}
if (dashboardConfig.state === "configured") {
try {
const dashboard = await fetchOpenCodeGoDashboardUsage(dashboardConfig);
if (!dashboard.usage) {
return { message: dashboard.message || "OpenCode Go dashboard quota data unavailable." };
}
const quotas: Record<string, UsageQuota> = {};
for (const quotaName of OPENCODE_GO_QUOTA_ORDER) {
const usage = dashboard.usage[quotaName];
if (usage) {
quotas[quotaName] = buildOpenCodeGoDollarQuota(
quotaName,
usage.usagePercent,
usage.resetAt
);
}
}
return { plan: "OpenCode Go", quotas: orderOpenCodeGoQuotas(quotas) };
} catch (error) {
return { message: `OpenCode Go dashboard quota error: ${sanitizeErrorMessage(error)}` };
}
}
const token = apiKey.trim().replace(/^Bearer\s+/i, "");
if (!token) {
return {
message:
"OpenCode Go quota requires OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE. " +
"The API key can be used for chat/models, but OpenCode Go does not expose quota via API key.",
};
}
try {
const res = await fetch(OPENCODE_GO_QUOTA_URL, {
headers: {
Authorization: buildBearerAuthorization(token),
"Accept-Language": "en-US,en",
"Content-Type": "application/json",
Accept: "application/json",
},
});
if (!res.ok) {
if (res.status === 401 || res.status === 403) {
return {
message:
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
};
}
return {
message:
`OpenCode Go quota API error (${res.status}). ` +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
};
}
let json: unknown;
try {
json = await res.json();
} catch {
return { message: "OpenCode Go quota response parsing failed." };
}
const root = toRecord(json);
if (
toNumber(root.code, 200) === 401 ||
toNumber(root.code, 200) === 403 ||
root.success === false
) {
return {
message:
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping.",
};
}
const data = toRecord(root.data);
const quotas: Record<string, UsageQuota> = {};
for (const limit of Array.isArray(data.limits) ? data.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.map((model) => {
const modelInfo = toRecord(model);
return {
name: String(modelInfo.model || modelInfo.modelCode || "usage"),
used: toNumber(modelInfo.percentage, 0),
};
})
: undefined
);
} else 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, ""));
return {
plan: planLabel
? /^opencode\s+go\b/i.test(planLabel)
? planLabel
: `OpenCode Go ${planLabel}`
: null,
quotas: orderOpenCodeGoQuotas(quotas),
};
} catch (error) {
return { message: `OpenCode Go quota API error: ${sanitizeErrorMessage(error)}` };
}
}
function resolveOllamaCloudConfig(providerSpecificData?: JsonRecord): OllamaCloudConfig {
const cookie =
process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE?.trim() ||
process.env.OLLAMA_USAGE_COOKIE?.trim() ||
process.env.OLLAMA_CLOUD_USAGE_COOKIE?.trim() ||
getProviderSpecificString(providerSpecificData, [
"ollamaUsageCookie",
"ollamaCloudUsageCookie",
"ollamaCloudCookie",
"usageCookie",
"cookie",
]);
if (!cookie) return { state: "none" };
if (cookie.includes("\r") || cookie.includes("\n")) {
return { state: "invalid", error: "Ollama Cloud cookie contains invalid CRLF characters." };
}
return { state: "configured", cookie };
}
function normalizeOllamaCloudCookie(value: string): string {
const trimmed = value.trim();
return trimmed.toLowerCase().startsWith(`${OLLAMA_CLOUD_SESSION_COOKIE.toLowerCase()}=`)
? trimmed.slice(OLLAMA_CLOUD_SESSION_COOKIE.length + 1).trim()
: trimmed;
}
function extractOllamaUsagePercent(trackHtml: string): number | null {
const tagHeader = trackHtml.match(/^[^>]*/)?.[0] ?? "";
const ariaMatch = tagHeader.match(/(\d+(?:\.\d+)?)%\s*used/);
if (ariaMatch) {
const pct = toNumber(ariaMatch[1], Number.NaN);
if (Number.isFinite(pct) && pct >= 0 && pct <= 100) return pct;
}
const style = tagHeader.match(/style="([^"]*)"/)?.[1] ?? "";
const pct = toNumber(style.match(/(?:^|;)\s*width\s*:\s*([0-9.]+)%/)?.[1], Number.NaN);
return Number.isFinite(pct) && pct >= 0 && pct <= 100 ? pct : null;
}
function parseOllamaCloudSettingsHtml(html: string): OllamaCloudUsage | null {
const parts = html.split(/\bdata-usage-track\b/);
if (parts.length < 2) return null;
const extractTime = (text: string): string | null => {
const match = text.match(/class="[^"]*local-time[^"]*"[^>]*data-time="([^"]*)"/);
return match?.[1] || null;
};
const sessionPercent = extractOllamaUsagePercent(parts[1]);
const weeklyPercent = parts[2] ? extractOllamaUsagePercent(parts[2]) : null;
if (sessionPercent === null && weeklyPercent === null) return null;
return {
...(sessionPercent !== null
? { session: { usagePercent: sessionPercent, resetAt: extractTime(parts[1]) } }
: {}),
...(weeklyPercent !== null
? { weekly: { usagePercent: weeklyPercent, resetAt: extractTime(parts[2]) } }
: {}),
planTier: html.match(/class="[^"]*capitalize[^"]*"[^>]*>([^<]*)</)?.[1]?.trim() || null,
};
}
async function fetchOllamaCloudUsageFromSettings(
config: Extract<OllamaCloudConfig, { state: "configured" }>
) {
const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
redirect: "manual",
headers: {
Accept: "text/html",
Cookie: `${OLLAMA_CLOUD_SESSION_COOKIE}=${normalizeOllamaCloudCookie(config.cookie)}`,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Gecko/20100101 Firefox/148.0",
},
signal: AbortSignal.timeout(10_000),
});
if (response.status >= 300 && response.status < 400) {
return { usage: null, message: "Ollama Cloud authentication expired. Refresh the cookie." };
}
if (!response.ok)
return { usage: null, message: `Ollama Cloud settings error (${response.status}).` };
const usage = parseOllamaCloudSettingsHtml(await response.text());
return {
usage,
message: usage ? undefined : "Ollama Cloud settings page did not contain usage quota tracks.",
};
}
export async function getOllamaCloudUsage(providerSpecificData?: JsonRecord) {
const config = resolveOllamaCloudConfig(providerSpecificData);
if (config.state === "none") {
return {
message:
"Ollama Cloud quota requires OLLAMA_USAGE_COOKIE. Copy the __Secure-session cookie from ollama.com/settings.",
};
}
if (config.state === "invalid") return { message: config.error };
try {
const result = await fetchOllamaCloudUsageFromSettings(config);
if (!result.usage) return { message: result.message || "Ollama Cloud quota data unavailable." };
const quotas: Record<string, UsageQuota> = {};
for (const key of ["session", "weekly"] as const) {
const quota = result.usage[key];
if (!quota) continue;
const pct = toPercentage(quota.usagePercent);
quotas[key] = {
used: pct,
total: 100,
remaining: Math.max(0, 100 - pct),
remainingPercentage: Math.max(0, 100 - pct),
resetAt: quota.resetAt,
unlimited: false,
displayName: key === "session" ? "Session" : "Weekly",
};
}
return {
plan: result.usage.planTier ? `Ollama Cloud ${result.usage.planTier}` : "Ollama Cloud",
quotas,
};
} catch (error) {
return { message: `Ollama Cloud quota error: ${sanitizeErrorMessage(error)}` };
}
}

View File

@@ -20,6 +20,7 @@ import { getDbInstance } from "@/lib/db/core";
import { fetchBailianQuota, type BailianTripleWindowQuota } from "./bailianQuotaFetcher.ts";
import { fetchDeepseekQuota, type DeepseekQuota } from "./deepseekQuotaFetcher.ts";
import { fetchOpencodeQuota, type OpencodeTripleWindowQuota } from "./opencodeQuotaFetcher.ts";
import { getOllamaCloudUsage, getOpenCodeGoUsage } from "./opencodeOllamaUsage.ts";
import {
applyAntigravityClientProfileHeaders,
getAntigravityBootstrapHeaders,
@@ -91,19 +92,6 @@ const NANOGPT_CONFIG = {
usageUrl: "https://nano-gpt.com/api/subscription/v1/usage",
};
const OPENCODE_GO_QUOTA_URL =
// Note: api.z.ai rejects opencode-go keys with {"code":401}. This default is a
// known broken placeholder (see issues #10448, #16017). The env-var override lets
// operators point at a working endpoint once OpenCode ships one.
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
@@ -214,77 +202,6 @@ function getGlmQuotaDisplayName(quotaName: string): string {
if (quotaName === "weekly") return "Weekly Quota";
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;
@@ -966,121 +883,6 @@ 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) {
return {
message:
"OpenCode Go does not expose a public quota API. Chat requests still work. " +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
};
}
return {
message:
`OpenCode Go quota API error (${res.status}). ` +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
};
}
let json: unknown;
try {
json = await res.json();
} catch {
return { message: "OpenCode Go quota response parsing failed." };
}
const code = toNumber((json as Record<string, unknown>).code, 200);
if (code === 401 || code === 403 || (json as Record<string, unknown>).success === false) {
return {
message:
"OpenCode Go does not expose a public quota API. Chat requests still work. " +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status.",
};
}
const data = toRecord((json as Record<string, unknown>).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.
@@ -1589,7 +1391,9 @@ export async function getUsageForProvider(
...(provider === "glm-cn" ? { apiRegion: "china" } : {}),
});
case "opencode-go":
return await getOpenCodeGoUsage(apiKey || "");
return await getOpenCodeGoUsage(apiKey || "", providerSpecificData);
case "ollama-cloud":
return await getOllamaCloudUsage(providerSpecificData);
case "minimax":
case "minimax-cn":
return await getMiniMaxUsage(apiKey || "", provider);
@@ -3118,7 +2922,10 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec
// "auth expired, chat may still work" message instead of a generic upstream-error blob
// so the quota card matches what users with legacy social-auth accounts already see.
// Inspired by https://github.com/decolua/9router/pull/620.
if ((response.status === 401 || response.status === 403) && isSocialAuthKiroAccount(providerSpecificData)) {
if (
(response.status === 401 || response.status === 403) &&
isSocialAuthKiroAccount(providerSpecificData)
) {
return {
message: "Kiro quota API authentication expired. Chat may still work.",
quotas: {},

View File

@@ -81,15 +81,15 @@ const ENV_KEY_RE = /(clientId|clientSecret|apiKey)Env\s*:/;
//
// 6A.8: Expanded scope to open-sse/** + src/lib/oauth/**. Newly discovered FPs:
//
// open-sse/services/usage.ts L582: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
// open-sse/services/usage.ts L499: `getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn")`
// The CRED_KEY_RE matches `apiKey:` in the TypeScript function-parameter type annotation.
// "minimax" and "minimax-cn" are provider-name strings in the type annotation, NOT credentials.
// This is a false positive (the gate was designed for object-literal assignments, not fn params).
// TODO(6A.8): Consider tightening CRED_KEY_RE to exclude function-signature contexts — but
// that adds complexity; the FP rate is low (1 file). Frozen by file:line:value key.
export const KNOWN_LITERAL_CREDS = new Set([
"open-sse/services/usage.ts:582:minimax", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 543→547 by #3838/#4293, then 547→582 by the v3.8.33 usage.ts growth)
"open-sse/services/usage.ts:582:minimax-cn", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 543→547 by #3838/#4293, then 547→582 by the v3.8.33 usage.ts growth)
"open-sse/services/usage.ts:499:minimax", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 582→499 by the OpenCode/Ollama usage extraction)
"open-sse/services/usage.ts:499:minimax-cn", // TODO(6A.8): pre-existing FP — TS fn-param type, not a credential (moved 582→499 by the OpenCode/Ollama usage extraction)
]);
/**

View File

@@ -111,6 +111,7 @@ export function formatUsdCost(value: number, locale: string): string {
*/
export function maskKey(fullKey: string | null | undefined): string {
if (!fullKey) return "";
if (fullKey.includes("****")) return fullKey;
return fullKey.length > 8 ? `${fullKey.slice(0, 8)}...` : fullKey;
}

View File

@@ -12,8 +12,6 @@ import {
getProviderBaseUrlHint,
getProviderBaseUrlPlaceholder,
isGlmProvider,
parseRoutingTagsInput,
parseExcludedModelsInput,
getWebSessionCredentialLabel,
getWebSessionCredentialHint,
getWebSessionCredentialCheckLabel,
@@ -28,7 +26,8 @@ import { getWebSessionCredentialRequirement } from "../../webSessionCredentials"
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { assignCcCompatibleRequestDefaults } from "./ccCompatibleRequestDefaults";
import { buildAddProviderSpecificData } from "./connectionProviderSpecificData";
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
export interface AddApiKeyModalProps {
isOpen: boolean;
@@ -112,6 +111,7 @@ export default function AddApiKeyModal({
customUserAgent: "",
accountId: "",
consoleApiKey: "",
...EMPTY_QUOTA_SCRAPING_FIELDS,
ccCompatibleContext1m: false,
ccCompatibleRedactThinking: false,
passthroughModels: false,
@@ -283,47 +283,27 @@ export default function AddApiKeyModal({
}
}
const providerSpecificData: Record<string, unknown> = {};
if (formData.customUserAgent.trim()) {
providerSpecificData.customUserAgent = formData.customUserAgent.trim();
}
openRouterPreset.applyTo(providerSpecificData);
if (formData.routingTags.trim()) {
providerSpecificData.tags = parseRoutingTagsInput(formData.routingTags);
}
if (formData.excludedModels.trim()) {
providerSpecificData.excludedModels = parseExcludedModelsInput(formData.excludedModels);
}
if (formData.passthroughModels) {
providerSpecificData.passthroughModels = true;
}
if (showFreeModelsToggle && formData.importFreeModelsOnly) {
providerSpecificData.importFreeModelsOnly = true;
}
if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) {
providerSpecificData.consoleApiKey = formData.consoleApiKey.trim();
}
if (isGooglePse && formData.cx.trim()) {
providerSpecificData.cx = formData.cx.trim();
}
if (usesBaseUrl) {
providerSpecificData.baseUrl = validatedBaseUrl;
} else if (showsRegion) {
providerSpecificData.region = formData.region.trim() || defaultRegion;
} else if (isGlm) {
providerSpecificData.apiRegion = formData.apiRegion;
} else if (isCloudflare && formData.accountId.trim()) {
providerSpecificData.accountId = formData.accountId.trim();
}
if (isCcCompatible) assignCcCompatibleRequestDefaults(providerSpecificData, formData);
const providerSpecificData = buildAddProviderSpecificData({
provider,
formData,
openRouterPreset,
showFreeModelsToggle,
isGooglePse,
usesBaseUrl,
validatedBaseUrl,
showsRegion,
defaultRegion,
isGlm,
isCloudflare,
isCcCompatible,
});
const payload = {
name: formData.name,
apiKey: credentialInput.trim() || undefined,
priority: formData.priority,
testStatus: "active",
providerSpecificData:
Object.keys(providerSpecificData).length > 0 ? providerSpecificData : undefined,
providerSpecificData,
};
const error = await onSave(payload);
@@ -710,6 +690,12 @@ export default function AddApiKeyModal({
</div>
)}
{freeModelsToggle}
<QuotaScrapingFields
provider={provider}
values={formData}
onChange={(patch) => setFormData({ ...formData, ...patch })}
t={t}
/>
{isCompatible && !isCcCompatible && (
<p className="text-xs text-text-muted">
{isAnthropic

View File

@@ -49,7 +49,8 @@ import { getWebSessionCredentialRequirement } from "../../webSessionCredentials"
import { useOpenRouterPresetControl } from "../OpenRouterPresetInput";
import WebSessionCredentialGuide from "../WebSessionCredentialGuide";
import CcCompatibleRequestDefaultsFields from "./CcCompatibleRequestDefaultsFields";
import { mergeCcCompatibleRequestDefaults } from "./ccCompatibleRequestDefaults";
import { assignEditApiKeyProviderSpecificData } from "./connectionProviderSpecificData";
import QuotaScrapingFields, { EMPTY_QUOTA_SCRAPING_FIELDS } from "./QuotaScrapingFields";
export interface EditConnectionModalConnection {
id?: string;
@@ -115,6 +116,7 @@ export default function EditConnectionModal({
codexServiceTier: "default" as CodexServiceTier,
codexOpenaiStoreEnabled: false,
consoleApiKey: "",
...EMPTY_QUOTA_SCRAPING_FIELDS,
ccCompatibleContext1m: false,
ccCompatibleRedactThinking: false,
cloudCodeProjectId: "",
@@ -218,6 +220,10 @@ export default function EditConnectionModal({
const existingOpenRouterPreset = stringField(connection.providerSpecificData?.preset);
const existingCx = stringField(connection.providerSpecificData?.cx);
const existingAccountId = stringField(connection.providerSpecificData?.accountId);
const existingOpenCodeGoWorkspaceId =
stringField(connection.providerSpecificData?.opencodeGoWorkspaceId) ||
stringField(connection.providerSpecificData?.openCodeGoWorkspaceId) ||
stringField(connection.providerSpecificData?.workspaceId);
const codexRequestDefaults = getCodexRequestDefaults(connection.providerSpecificData);
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
connection.providerSpecificData
@@ -269,6 +275,9 @@ export default function EditConnectionModal({
codexServiceTier: codexRequestDefaults.serviceTier ?? "default",
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
consoleApiKey: existingConsoleApiKey,
opencodeGoWorkspaceId: existingOpenCodeGoWorkspaceId,
opencodeGoAuthCookie: "",
ollamaCloudUsageCookie: "",
ccCompatibleContext1m: ccRequestDefaults.context1m,
ccCompatibleRedactThinking: ccRequestDefaults.redactThinking,
cloudCodeProjectId:
@@ -482,45 +491,24 @@ export default function EditConnectionModal({
if (!isOAuth) {
updates.providerSpecificData = {
...(connection.providerSpecificData || {}),
extraApiKeys: extraApiKeys.filter((k) => k.trim().length > 0),
tag: formData.tag.trim() || undefined,
tags: parseRoutingTagsInput(formData.routingTags),
excludedModels: parseExcludedModelsInput(formData.excludedModels),
customUserAgent: formData.customUserAgent.trim(),
...openRouterPreset.getPatch(),
...(formData.passthroughModels ? { passthroughModels: true } : {}),
};
if (provider === "bailian-coding-plan") {
if (formData.consoleApiKey.trim()) {
updates.providerSpecificData.consoleApiKey = formData.consoleApiKey.trim();
} else {
updates.providerSpecificData.consoleApiKey = undefined;
}
}
if (formData.validationModelId) {
updates.providerSpecificData.validationModelId = formData.validationModelId;
}
if (isGooglePse) {
updates.providerSpecificData.cx = formData.cx.trim() || undefined;
}
if (usesBaseUrl) {
updates.providerSpecificData.baseUrl = validatedBaseUrl;
} else if (showsRegion) {
updates.providerSpecificData.region = formData.region.trim() || defaultRegion;
} else if (isGlm) {
updates.providerSpecificData.apiRegion = formData.apiRegion;
} else if (isCloudflare && formData.accountId.trim()) {
updates.providerSpecificData.accountId = formData.accountId.trim();
}
if (supportsGoogleProjectId) {
updates.providerSpecificData.projectId = trimmedCloudCodeProjectId || null;
}
if (isCcCompatible) {
updates.providerSpecificData.requestDefaults = mergeCcCompatibleRequestDefaults(
updates.providerSpecificData.requestDefaults,
formData
);
}
assignEditApiKeyProviderSpecificData({
provider,
formData,
target: updates.providerSpecificData,
extraApiKeys,
openRouterPreset,
usesBaseUrl,
validatedBaseUrl,
showsRegion,
defaultRegion,
isGlm,
isCloudflare,
supportsGoogleProjectId,
trimmedCloudCodeProjectId,
isGooglePse,
isCcCompatible,
});
} else {
updates.providerSpecificData = {
...(connection.providerSpecificData || {}),
@@ -695,6 +683,13 @@ export default function EditConnectionModal({
description={t("disableCoolingDescription")}
/>
</div>
<QuotaScrapingFields
provider={provider}
values={formData}
onChange={(patch) => setFormData({ ...formData, ...patch })}
t={t}
editMode
/>
{supportsGoogleProjectId && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
{isAntigravity && (

View File

@@ -0,0 +1,113 @@
"use client";
import { Input } from "@/shared/components";
import { providerText, type ProviderMessageTranslator } from "../../providerPageHelpers";
export type QuotaScrapingFieldValues = {
opencodeGoWorkspaceId: string;
opencodeGoAuthCookie: string;
ollamaCloudUsageCookie: string;
};
export const EMPTY_QUOTA_SCRAPING_FIELDS: QuotaScrapingFieldValues = {
opencodeGoWorkspaceId: "",
opencodeGoAuthCookie: "",
ollamaCloudUsageCookie: "",
};
export function assignQuotaScrapingProviderData(
provider: string | undefined,
values: QuotaScrapingFieldValues,
target: Record<string, unknown>
) {
if (provider === "opencode-go") {
target.opencodeGoWorkspaceId = values.opencodeGoWorkspaceId.trim() || undefined;
if (values.opencodeGoAuthCookie.trim()) {
target.opencodeGoAuthCookie = values.opencodeGoAuthCookie.trim();
}
} else if (provider === "ollama-cloud" && values.ollamaCloudUsageCookie.trim()) {
target.ollamaCloudUsageCookie = values.ollamaCloudUsageCookie.trim();
}
}
type QuotaScrapingFieldsProps = {
provider?: string;
values: QuotaScrapingFieldValues;
onChange: (patch: Partial<QuotaScrapingFieldValues>) => void;
t: ProviderMessageTranslator;
editMode?: boolean;
};
export default function QuotaScrapingFields({
provider,
values,
onChange,
t,
editMode = false,
}: QuotaScrapingFieldsProps) {
if (provider === "opencode-go") {
return (
<div className="flex flex-col gap-3 rounded-lg border border-border/50 bg-surface/20 p-4">
<Input
label={providerText(t, "opencodeGoWorkspaceIdLabel", "OpenCode Go workspace ID")}
name="opencodeGoWorkspaceId"
value={values.opencodeGoWorkspaceId}
onChange={(e) => onChange({ opencodeGoWorkspaceId: e.target.value })}
placeholder="workspace_..."
hint={providerText(
t,
"opencodeGoWorkspaceIdHint",
"Required for quota scraping. Copy it from the OpenCode Go workspace URL."
)}
autoComplete="off"
spellCheck={false}
/>
<Input
label={providerText(t, "opencodeGoAuthCookieLabel", "OpenCode Go auth cookie")}
name="opencodeGoAuthCookie"
type="password"
value={values.opencodeGoAuthCookie}
onChange={(e) => onChange({ opencodeGoAuthCookie: e.target.value })}
placeholder="auth=..."
hint={providerText(
t,
"opencodeGoAuthCookieHint",
editMode
? "Leave blank to keep the stored cookie. Paste auth=... or only the cookie value to replace it."
: "Paste the auth cookie value from opencode.ai. The auth= prefix is accepted."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
</div>
);
}
if (provider === "ollama-cloud") {
return (
<div className="flex flex-col gap-3 rounded-lg border border-border/50 bg-surface/20 p-4">
<Input
label={providerText(t, "ollamaCloudUsageCookieLabel", "Ollama Cloud usage cookie")}
name="ollamaCloudUsageCookie"
type="password"
value={values.ollamaCloudUsageCookie}
onChange={(e) => onChange({ ollamaCloudUsageCookie: e.target.value })}
placeholder="__Secure-session=..."
hint={providerText(
t,
"ollamaCloudUsageCookieHint",
editMode
? "Leave blank to keep the stored cookie. Paste the __Secure-session cookie value from ollama.com/settings to replace it."
: "Required for quota scraping. Paste the __Secure-session cookie value from ollama.com/settings."
)}
autoComplete="off"
spellCheck={false}
autoCapitalize="off"
/>
</div>
);
}
return null;
}

View File

@@ -0,0 +1,125 @@
import { parseExcludedModelsInput, parseRoutingTagsInput } from "../../providerPageHelpers";
import {
assignCcCompatibleRequestDefaults,
mergeCcCompatibleRequestDefaults,
} from "./ccCompatibleRequestDefaults";
import {
assignQuotaScrapingProviderData,
type QuotaScrapingFieldValues,
} from "./QuotaScrapingFields";
type FormData = QuotaScrapingFieldValues & {
accountId: string;
apiRegion: string;
ccCompatibleContext1m: boolean;
ccCompatibleRedactThinking: boolean;
consoleApiKey: string;
customUserAgent: string;
cx: string;
excludedModels: string;
importFreeModelsOnly: boolean;
passthroughModels: boolean;
region: string;
routingTags: string;
tag?: string;
validationModelId?: string;
};
type ProviderSpecificData = Record<string, unknown>;
export function buildAddProviderSpecificData(options: {
provider?: string;
formData: FormData;
openRouterPreset: { applyTo: (target: ProviderSpecificData) => void };
showFreeModelsToggle: boolean;
isGooglePse: boolean;
usesBaseUrl: boolean;
validatedBaseUrl: string | null;
showsRegion: boolean;
defaultRegion: string;
isGlm: boolean;
isCloudflare: boolean;
isCcCompatible?: boolean;
}) {
const {
provider,
formData,
openRouterPreset,
showFreeModelsToggle,
isGooglePse,
usesBaseUrl,
validatedBaseUrl,
showsRegion,
defaultRegion,
isGlm,
isCloudflare,
isCcCompatible,
} = options;
const data: ProviderSpecificData = {};
if (formData.customUserAgent.trim()) data.customUserAgent = formData.customUserAgent.trim();
openRouterPreset.applyTo(data);
if (formData.routingTags.trim()) data.tags = parseRoutingTagsInput(formData.routingTags);
if (formData.excludedModels.trim()) {
data.excludedModels = parseExcludedModelsInput(formData.excludedModels);
}
if (formData.passthroughModels) data.passthroughModels = true;
if (showFreeModelsToggle && formData.importFreeModelsOnly) data.importFreeModelsOnly = true;
if (provider === "bailian-coding-plan" && formData.consoleApiKey.trim()) {
data.consoleApiKey = formData.consoleApiKey.trim();
}
assignQuotaScrapingProviderData(provider, formData, data);
if (isGooglePse && formData.cx.trim()) data.cx = formData.cx.trim();
if (usesBaseUrl) data.baseUrl = validatedBaseUrl;
else if (showsRegion) data.region = formData.region.trim() || defaultRegion;
else if (isGlm) data.apiRegion = formData.apiRegion;
else if (isCloudflare && formData.accountId.trim()) data.accountId = formData.accountId.trim();
if (isCcCompatible) assignCcCompatibleRequestDefaults(data, formData);
return Object.keys(data).length > 0 ? data : undefined;
}
export function assignEditApiKeyProviderSpecificData(options: {
provider: string;
formData: FormData;
target: ProviderSpecificData;
extraApiKeys: string[];
openRouterPreset: { getPatch: () => ProviderSpecificData };
usesBaseUrl: boolean;
validatedBaseUrl: string | null;
showsRegion: boolean;
defaultRegion: string;
isGlm: boolean;
isCloudflare: boolean;
supportsGoogleProjectId: boolean;
trimmedCloudCodeProjectId: string;
isGooglePse: boolean;
isCcCompatible: boolean;
}) {
const o = options;
Object.assign(o.target, {
extraApiKeys: o.extraApiKeys.filter((key) => key.trim().length > 0),
tag: o.formData.tag.trim() || undefined,
tags: parseRoutingTagsInput(o.formData.routingTags),
excludedModels: parseExcludedModelsInput(o.formData.excludedModels),
customUserAgent: o.formData.customUserAgent.trim(),
...o.openRouterPreset.getPatch(),
...(o.formData.passthroughModels ? { passthroughModels: true } : {}),
});
if (o.provider === "bailian-coding-plan") {
o.target.consoleApiKey = o.formData.consoleApiKey.trim() || undefined;
}
assignQuotaScrapingProviderData(o.provider, o.formData, o.target);
if (o.formData.validationModelId) o.target.validationModelId = o.formData.validationModelId;
if (o.isGooglePse) o.target.cx = o.formData.cx.trim() || undefined;
if (o.usesBaseUrl) o.target.baseUrl = o.validatedBaseUrl;
else if (o.showsRegion) o.target.region = o.formData.region.trim() || o.defaultRegion;
else if (o.isGlm) o.target.apiRegion = o.formData.apiRegion;
else if (o.isCloudflare && o.formData.accountId.trim()) {
o.target.accountId = o.formData.accountId.trim();
}
if (o.supportsGoogleProjectId) o.target.projectId = o.trimmedCloudCodeProjectId || null;
if (o.isCcCompatible) {
o.target.requestDefaults = mergeCcCompatibleRequestDefaults(
o.target.requestDefaults,
o.formData
);
}
}

View File

@@ -91,6 +91,7 @@ export default function QuotaCard({
quotas={quotas}
loading={loading}
error={error}
message={quota?.message ?? null}
refreshedAt={displayRefreshedAt}
hasStaleData={hasStaleData}
onRefresh={onRefresh}

View File

@@ -0,0 +1,6 @@
export function formatAutoRefreshCountdown(ms: number): string {
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}

View File

@@ -21,6 +21,8 @@ import useEmailPrivacyStore from "@/store/emailPrivacyStore";
import { useNotificationStore } from "@/store/notificationStore";
import QuotaCutoffModal from "./QuotaCutoffModal";
import QuotaCardGrid from "./QuotaCardGrid";
import { useVisibleQuotaData } from "./useVisibleQuotaData";
import { formatAutoRefreshCountdown } from "./formatters";
import { translateUsageOrFallback, type UsageTranslationValues } from "./i18nFallback";
import { compareTr } from "@/shared/utils/turkishText";
@@ -33,7 +35,6 @@ const MIN_FETCH_INTERVAL_MS = 30000;
const QUOTA_BAR_GREEN_THRESHOLD = 50;
const QUOTA_BAR_YELLOW_THRESHOLD = 20;
// Display label per known provider; the icon is resolved by ProviderIcon.
const PROVIDER_LABEL: Record<string, string> = {
antigravity: "Antigravity",
"gemini-cli": "Gemini CLI",
@@ -46,6 +47,7 @@ const PROVIDER_LABEL: Record<string, string> = {
zai: "Z.AI",
glmt: "GLM Thinking",
"opencode-go": "OpenCode Go",
"ollama-cloud": "Ollama Cloud",
"kimi-coding": "Kimi Coding",
minimax: "MiniMax",
"minimax-cn": "MiniMax CN",
@@ -53,8 +55,7 @@ const PROVIDER_LABEL: Record<string, string> = {
deepseek: "DeepSeek",
};
// Group ordering — single source of truth for "where does Codex sit
// relative to Antigravity on the page".
// Group ordering — single source of truth for provider placement.
const PROVIDER_ORDER: Record<string, number> = {
antigravity: 1,
"gemini-cli": 2,
@@ -66,10 +67,11 @@ const PROVIDER_ORDER: Record<string, number> = {
zai: 8,
glmt: 9,
"opencode-go": 10,
"kimi-coding": 11,
minimax: 12,
"minimax-cn": 13,
nanogpt: 14,
"ollama-cloud": 11,
"kimi-coding": 12,
minimax: 13,
"minimax-cn": 14,
nanogpt: 15,
};
const TIER_FILTERS = [
@@ -213,13 +215,6 @@ function aggregateWorst(statuses: StatusKey[]): "critical" | "alert" | "ok" | "e
return worst;
}
function formatAutoRefreshCountdown(ms: number): string {
const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
interface ProviderLimitsProps {
showFilters?: boolean;
autoRefreshInterval?: number;
@@ -564,6 +559,7 @@ export default function ProviderLimits({
(a, b) => (PROVIDER_ORDER[a.provider] || 99) - (PROVIDER_ORDER[b.provider] || 99)
);
}, [filteredConnections]);
const visibleQuotaData = useVisibleQuotaData(sortedConnections, quotaData);
const resolvedPlanByConnection = useMemo(() => {
const out: Record<string, string | null> = {};
@@ -613,10 +609,10 @@ export default function ProviderLimits({
const statusByConnection = useMemo(() => {
const out: Record<string, StatusKey> = {};
for (const conn of sortedConnections) {
out[conn.id] = getWorstStatus(quotaData[conn.id]?.quotas);
out[conn.id] = getWorstStatus(visibleQuotaData[conn.id]?.quotas);
}
return out;
}, [sortedConnections, quotaData]);
}, [sortedConnections, visibleQuotaData]);
const purchaseTypeCounts = useMemo(() => {
const counts: Record<PurchaseTypeKey, number> = {
@@ -697,8 +693,8 @@ export default function ProviderLimits({
const sa = statusRank[statusByConnection[a.id] || "empty"];
const sb = statusRank[statusByConnection[b.id] || "empty"];
if (sa !== sb) return sa - sb;
const ra = getSoonestResetMs(quotaData[a.id]?.quotas);
const rb = getSoonestResetMs(quotaData[b.id]?.quotas);
const ra = getSoonestResetMs(visibleQuotaData[a.id]?.quotas);
const rb = getSoonestResetMs(visibleQuotaData[b.id]?.quotas);
return ra - rb;
});
}, [
@@ -711,7 +707,7 @@ export default function ProviderLimits({
statusByConnection,
envFilter,
providerFilter,
quotaData,
visibleQuotaData,
]);
// Distinct provider keys present in the current connection set (after the
@@ -1055,7 +1051,7 @@ export default function ProviderLimits({
<QuotaCardGrid
connections={visibleConnections}
quotaData={quotaData}
quotaData={visibleQuotaData}
loading={loading}
errors={errors}
lastRefreshedAt={lastRefreshedAt}
@@ -1064,7 +1060,7 @@ export default function ProviderLimits({
renderInlineQuotaSummary={(quota) => renderInlineQuotaSummary(quota.quotas)}
onRefresh={refreshProvider}
onOpenCutoff={(conn) => {
const windows = (quotaData[conn.id]?.quotas || []).filter(
const windows = (visibleQuotaData[conn.id]?.quotas || []).filter(
(q: any) => q && typeof q.name === "string" && !q.isCredits
);
setCutoffModalWindows(windows);

View File

@@ -25,6 +25,7 @@ interface Props {
quotas: any[];
loading: boolean;
error: string | null;
message?: string | null;
refreshedAt?: string;
hasStaleData: boolean;
onRefresh: () => void;
@@ -109,6 +110,7 @@ export default function QuotaCardExpanded({
quotas,
loading,
error,
message,
refreshedAt,
hasStaleData,
onRefresh,
@@ -143,6 +145,10 @@ export default function QuotaCardExpanded({
<span className="material-symbols-outlined text-[13px]">error</span>
<span>{error}</span>
</div>
) : quotas.length === 0 && message ? (
<div className="text-[11px] text-text-muted italic" title={message}>
{message}
</div>
) : quotas.length === 0 ? (
<div className="text-[11px] text-text-muted italic">{t("noQuotaData")}</div>
) : (

View File

@@ -0,0 +1,189 @@
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
import { safePercentage } from "@/shared/utils/formatting";
const GLM_QUOTA_ORDER: Record<string, number> = { session: 0, weekly: 1, mcp_monthly: 2 };
function quotaEntries(data: any): Array<[string, any]> {
return data?.quotas && typeof data.quotas === "object" ? Object.entries(data.quotas) : [];
}
function isUnlimitedEmpty(quota: any): boolean {
return Boolean(quota?.unlimited && (!quota?.total || quota.total <= 0));
}
function isPastResetWindow(resetAt: any): boolean {
if (!resetAt) return false;
const resetTime =
typeof resetAt === "number" ? resetAt : typeof resetAt === "string" ? Date.parse(resetAt) : NaN;
return Number.isFinite(resetTime) && Date.now() >= resetTime;
}
function getResetAdjustedQuota(quota: any) {
const usedRaw = Number(quota?.used || 0);
const totalRaw = Number(quota?.total || 0);
const total = Number.isFinite(totalRaw) ? totalRaw : 0;
const remainingRaw = safePercentage(quota?.remainingPercentage);
const hasPendingUsage = usedRaw > 0 || (remainingRaw !== undefined && remainingRaw < 100);
const staleAfterReset = isPastResetWindow(quota?.resetAt || null) && hasPendingUsage;
return {
staleAfterReset,
total,
used: staleAfterReset ? 0 : usedRaw,
remainingPercentage: staleAfterReset && total > 0 ? 100 : remainingRaw,
};
}
function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
const adjusted = getResetAdjustedQuota(quota);
return {
name,
used: Number.isFinite(adjusted.used) ? adjusted.used : 0,
total: adjusted.total,
resetAt: quota?.resetAt || null,
staleAfterReset: adjusted.staleAfterReset,
...(adjusted.remainingPercentage !== undefined
? { remainingPercentage: adjusted.remainingPercentage }
: {}),
...extras,
};
}
function parseGeneric(data: any) {
return quotaEntries(data).map(([name, quota]) => normalizeQuotaEntry(name, quota));
}
function parseGithub(data: any) {
return quotaEntries(data)
.filter(([, quota]) => !isUnlimitedEmpty(quota))
.map(([name, quota]) => normalizeQuotaEntry(name, quota));
}
function parseGlmFamily(data: any) {
return quotaEntries(data).map(([name, quota]) =>
normalizeQuotaEntry(name, quota, {
displayName: quota?.displayName,
details: Array.isArray(quota?.details) ? quota.details : undefined,
isPercentageOnly:
Number(quota?.total || 0) === 100 && quota?.remainingPercentage !== undefined,
})
);
}
function buildCreditsQuota(
name: string,
remaining: number,
remainingPercentage: number,
extra = {}
) {
return {
name,
used: 0,
total: 0,
remaining,
resetAt: null,
unlimited: false,
isCredits: true,
remainingPercentage,
creditCount: remaining,
...extra,
};
}
function parseAntigravityQuota(modelKey: string, quota: any) {
if (modelKey === "credits") {
const remaining = Number(quota?.remaining ?? 0);
return buildCreditsQuota("credits", remaining, remaining > 50 ? 100 : remaining > 10 ? 60 : 20);
}
if (modelKey === "models" || isUnlimitedEmpty(quota)) return null;
return normalizeQuotaEntry(modelKey, quota, {
modelKey,
isPercentageOnly: quota?.fractionReported === true,
...(quota?.quotaSource ? { quotaSource: quota.quotaSource } : {}),
...(quota?.fractionReported !== undefined ? { fractionReported: quota.fractionReported } : {}),
});
}
function parseAntigravity(data: any) {
return quotaEntries(data)
.map(([modelKey, quota]) => parseAntigravityQuota(modelKey, quota))
.filter(Boolean);
}
function parseCodex(data: any) {
return quotaEntries(data).map(([quotaType, quota]) =>
normalizeQuotaEntry(quotaType, quota, {
displayName: quota?.displayName,
isPercentageOnly: true,
})
);
}
function parseClaude(data: any) {
if (data?.message)
return [{ name: "error", used: 0, total: 0, resetAt: null, message: data.message }];
return quotaEntries(data).map(([name, quota]) =>
normalizeQuotaEntry(name, quota, { isPercentageOnly: true })
);
}
function parseGeminiCli(data: any) {
return quotaEntries(data).map(([modelKey, quota]) =>
normalizeQuotaEntry(modelKey, quota, { modelKey })
);
}
function parseDeepseekQuota(quotaKey: string, quota: any) {
const match = quotaKey.match(/^credits(?:_([a-z]{3}))?$/);
if (!match) return normalizeQuotaEntry(quotaKey, quota);
const remaining = Number(quota?.remaining ?? 0);
const currency = quota?.currency ?? (match[1] ? match[1].toUpperCase() : "USD");
return buildCreditsQuota(currency, remaining, remaining > 20 ? 100 : remaining > 5 ? 60 : 20, {
currency,
});
}
function parseDeepseek(data: any) {
return quotaEntries(data).map(([quotaKey, quota]) => parseDeepseekQuota(quotaKey, quota));
}
function parseProviderQuotas(providerId: string, data: any) {
if (providerId === "github") return parseGithub(data);
if (["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return parseGlmFamily(data);
if (providerId === "antigravity" || providerId === "agy") return parseAntigravity(data);
if (providerId === "codex") return parseCodex(data);
if (providerId === "claude") return parseClaude(data);
if (providerId === "gemini-cli") return parseGeminiCli(data);
if (providerId === "deepseek") return parseDeepseek(data);
return parseGeneric(data);
}
function sortProviderModelOrder(provider: string, quotas: any[]) {
const modelOrder = getModelsByProviderId(provider);
if (modelOrder.length === 0) return;
const orderMap = new Map(modelOrder.map((m, i) => [m.id, i]));
quotas.sort(
(a, b) =>
(orderMap.get(a.modelKey || a.name) ?? 999) - (orderMap.get(b.modelKey || b.name) ?? 999)
);
}
function sortGlmOrder(providerId: string, quotas: any[]) {
if (!["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return;
quotas.sort((a, b) => (GLM_QUOTA_ORDER[a.name] ?? 99) - (GLM_QUOTA_ORDER[b.name] ?? 99));
}
export function parseQuotaData(provider: string | undefined, data: any) {
if (!data || typeof data !== "object") return [];
const providerId = String(provider || "").toLowerCase();
try {
const normalizedQuotas = parseProviderQuotas(providerId, data);
sortProviderModelOrder(provider, normalizedQuotas);
sortGlmOrder(providerId, normalizedQuotas);
return normalizedQuotas;
} catch (error) {
console.error(`Error parsing quota data for ${provider}:`, error);
return [];
}
}

View File

@@ -0,0 +1,66 @@
import { useEffect, useMemo, useState } from "react";
import { collectHiddenQuotaModelIds, filterHiddenModelQuotas } from "./utils";
function getProviderKey(connections: any[]): string {
const providers = new Set<string>();
for (const conn of connections) {
if (typeof conn?.provider === "string" && conn.provider) providers.add(conn.provider);
}
return Array.from(providers).sort().join("|");
}
export function useVisibleQuotaData(
connections: any[],
quotaData: Record<string, any>
): Record<string, any> {
const [hiddenModelsByProvider, setHiddenModelsByProvider] = useState<Record<string, string[]>>(
{}
);
const providerKey = useMemo(() => getProviderKey(connections), [connections]);
useEffect(() => {
if (!providerKey) return;
let alive = true;
const providers = providerKey.split("|").filter(Boolean);
Promise.all(
providers.map(async (provider) => {
try {
const response = await fetch(
`/api/provider-models?provider=${encodeURIComponent(provider)}`
);
if (!response.ok) return [provider, []] as const;
const data = await response.json();
return [provider, collectHiddenQuotaModelIds(provider, data)] as const;
} catch {
return [provider, []] as const;
}
})
).then((entries) => {
if (alive) setHiddenModelsByProvider(Object.fromEntries(entries));
});
return () => {
alive = false;
};
}, [providerKey]);
return useMemo(() => {
const next: Record<string, any> = {};
for (const conn of connections) {
const data = quotaData[conn.id];
if (!data) continue;
next[conn.id] = {
...data,
quotas: filterHiddenModelQuotas(
conn.provider,
data.quotas,
hiddenModelsByProvider[conn.provider]
),
};
}
return next;
}, [connections, hiddenModelsByProvider, quotaData]);
}

View File

@@ -1,5 +1,4 @@
import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts";
import { safePercentage } from "@/shared/utils/formatting";
export { parseQuotaData } from "./quotaParsing";
const PROVIDER_PLAN_FALLBACKS = new Set([
"claude code",
@@ -36,12 +35,6 @@ const QUOTA_LABEL_MAP: Record<string, string> = {
time_limit: "Time Limit",
};
const GLM_QUOTA_ORDER: Record<string, number> = {
session: 0,
weekly: 1,
mcp_monthly: 2,
};
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
@@ -179,267 +172,6 @@ export function calculatePercentage(used, total) {
return Math.round(((total - used) / total) * 100);
}
function isPastResetWindow(resetAt) {
if (!resetAt) return false;
const resetTime =
typeof resetAt === "number" ? resetAt : typeof resetAt === "string" ? Date.parse(resetAt) : NaN;
if (!Number.isFinite(resetTime)) return false;
return Date.now() >= resetTime;
}
function normalizeQuotaEntry(name: string, quota: any = {}, extras: any = {}) {
const usedRaw = Number(quota?.used || 0);
const totalRaw = Number(quota?.total || 0);
const resetAt = quota?.resetAt || null;
// T13: Only consider it stale if the reset time passed AND there's still usage shown.
// If usage is already 0 (or remaining is 100%), it's naturally reset and doesn't need to be marked as stale.
const passedReset = isPastResetWindow(resetAt);
const remainingPercentageRaw = safePercentage(quota?.remainingPercentage);
const hasPendingUsage =
usedRaw > 0 || (remainingPercentageRaw !== undefined && remainingPercentageRaw < 100);
const staleAfterReset = passedReset && hasPendingUsage;
const used = staleAfterReset ? 0 : usedRaw;
const total = Number.isFinite(totalRaw) ? totalRaw : 0;
const remainingPercentage =
staleAfterReset && total > 0
? 100
: remainingPercentageRaw !== undefined
? remainingPercentageRaw
: undefined;
return {
name,
used: Number.isFinite(used) ? used : 0,
total,
resetAt,
staleAfterReset,
...(remainingPercentage !== undefined ? { remainingPercentage } : {}),
...extras,
};
}
/**
* Parse provider-specific quota structures into normalized array
* @param {string} provider - Provider name (github, antigravity, codex, kiro, claude)
* @param {Object} data - Raw quota data from provider
* @returns {Array<Object>} Normalized quota objects with { name, used, total, resetAt }
*/
export function parseQuotaData(provider, data) {
if (!data || typeof data !== "object") return [];
const normalizedQuotas = [];
const providerId = String(provider || "").toLowerCase();
try {
switch (providerId) {
case "github":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
if (quota?.unlimited && (!quota?.total || quota.total <= 0)) {
return;
}
normalizedQuotas.push(normalizeQuotaEntry(name, quota));
});
}
break;
case "glm":
case "glm-cn":
case "glmt":
case "opencode-go":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(
normalizeQuotaEntry(name, quota, {
displayName: quota?.displayName,
details: Array.isArray(quota?.details) ? quota.details : undefined,
isPercentageOnly:
Number(quota?.total || 0) === 100 && quota?.remainingPercentage !== undefined,
})
);
});
}
break;
case "antigravity":
case "agy":
if (data.quotas) {
Object.entries(data.quotas).forEach(([modelKey, quota]: [string, any]) => {
if (modelKey === "credits") {
// Credit balance: render as "N credits remaining" counter, not a progress bar
const remaining = Number(quota?.remaining ?? 0);
normalizedQuotas.push({
name: "credits",
used: 0,
total: 0,
remaining,
resetAt: null,
unlimited: false,
isCredits: true,
// Show green if >50, yellow if >10, red if ≤10
remainingPercentage: remaining > 50 ? 100 : remaining > 10 ? 60 : 20,
creditCount: remaining,
});
return;
}
if (modelKey === "models") {
// Summary row: skip — individual models are shown via modelQuotas if needed
return;
}
if (quota?.unlimited && (!quota?.total || quota.total <= 0)) {
return;
}
normalizedQuotas.push(
normalizeQuotaEntry(modelKey, quota, {
modelKey: modelKey,
isPercentageOnly: quota?.fractionReported === true,
...(quota?.quotaSource ? { quotaSource: quota.quotaSource } : {}),
...(quota?.fractionReported !== undefined
? { fractionReported: quota.fractionReported }
: {}),
})
);
});
}
break;
case "codex":
if (data.quotas) {
Object.entries(data.quotas).forEach(([quotaType, quota]: [string, any]) => {
normalizedQuotas.push(
normalizeQuotaEntry(quotaType, quota, {
displayName: quota?.displayName,
isPercentageOnly: true,
})
);
});
}
break;
case "kiro":
case "amazon-q":
if (data.quotas) {
Object.entries(data.quotas).forEach(([quotaType, quota]: [string, any]) => {
normalizedQuotas.push(normalizeQuotaEntry(quotaType, quota));
});
}
break;
case "claude":
if (data.message) {
// Handle error message case
normalizedQuotas.push({
name: "error",
used: 0,
total: 0,
resetAt: null,
message: data.message,
});
} else if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(
normalizeQuotaEntry(name, quota, {
isPercentageOnly: true,
})
);
});
}
break;
case "gemini-cli":
if (data.quotas) {
Object.entries(data.quotas).forEach(([modelKey, quota]: [string, any]) => {
normalizedQuotas.push(normalizeQuotaEntry(modelKey, quota, { modelKey }));
});
}
break;
case "nanogpt":
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(normalizeQuotaEntry(name, quota));
});
}
break;
case "deepseek":
// DeepSeek balance: credits-style display with currency
// Match any "credits" key with optional 3-letter currency suffix
if (data.quotas) {
Object.entries(data.quotas).forEach(([quotaKey, quota]: [string, any]) => {
// Match credits, credits_usd, credits_cny, credits_eur, etc.
const match = quotaKey.match(/^credits(?:_([a-z]{3}))?$/);
if (match) {
const remaining = Number(quota?.remaining ?? 0);
// Extract currency from key suffix or use quota.currency, fallback to USD
const currency = quota?.currency ?? (match[1] ? match[1].toUpperCase() : "USD");
normalizedQuotas.push({
name: currency,
used: 0,
total: 0,
remaining,
resetAt: null,
unlimited: false,
isCredits: true,
currency,
creditCount: remaining,
// Color coding based on balance amount: green >20, yellow 5-20, red <5
remainingPercentage: remaining > 20 ? 100 : remaining > 5 ? 60 : 20,
});
} else {
normalizedQuotas.push(normalizeQuotaEntry(quotaKey, quota));
}
});
}
break;
default:
// Generic fallback for unknown providers
if (data.quotas) {
Object.entries(data.quotas).forEach(([name, quota]: [string, any]) => {
normalizedQuotas.push(normalizeQuotaEntry(name, quota));
});
}
}
} catch (error) {
console.error(`Error parsing quota data for ${provider}:`, error);
return [];
}
// Sort quotas according to PROVIDER_MODELS order
const modelOrder = getModelsByProviderId(provider);
if (modelOrder.length > 0) {
const orderMap = new Map(modelOrder.map((m, i) => [m.id, i]));
normalizedQuotas.sort((a, b) => {
// Use modelKey for antigravity, otherwise use name
const keyA = a.modelKey || a.name;
const keyB = b.modelKey || b.name;
const orderA = orderMap.get(keyA) ?? 999;
const orderB = orderMap.get(keyB) ?? 999;
return (orderA as number) - (orderB as number);
});
}
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;
return orderA - orderB;
});
}
return normalizedQuotas;
}
/**
* Resolve the best available plan label using live usage first, then persisted
* provider-specific connection metadata.
@@ -472,107 +204,93 @@ export function resolvePlanValue(plan, providerSpecificData) {
return livePlan || null;
}
function unknownPlanTier(raw: string | null = null) {
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw };
}
function formatUnknownPlanLabel(raw: string) {
return raw
.toLowerCase()
.split(/[\s_-]+/)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function matchClaudePlanTier(raw: string, upper: string) {
const match = upper.match(/(?:DEFAULT_)?CLAUDE_(MAX|PRO|TEAM|ENTERPRISE|FREE)(?:_(\d+X))?/);
if (!match) return null;
const multiplier = match[2] ? ` ${match[2].toLowerCase()}` : "";
const tiers = {
MAX: { key: "ultra", label: `Max${multiplier}`, variant: "success", rank: 4, raw },
PRO: { key: "pro", label: "Pro", variant: "success", rank: 3, raw },
TEAM: { key: "team", label: "Team", variant: "info", rank: 6, raw },
ENTERPRISE: { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw },
FREE: { key: "free", label: "Free", variant: "default", rank: 1, raw },
};
return tiers[match[1]];
}
function matchKeywordPlanTier(raw: string, upper: string) {
if (upper.includes("PRO+") || upper.includes("PRO PLUS") || upper.includes("PROPLUS"))
return { key: "plus", label: "Pro+", variant: "success", rank: 4, raw };
if (upper.includes("ENTERPRISE") || upper.includes("CORP") || upper.includes("ORG"))
return { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw };
if (upper.includes("TEAM") || upper.includes("CHATGPTTEAM"))
return { key: "team", label: "Team", variant: "info", rank: 6, raw };
if (upper.includes("BUSINESS") || upper.includes("STANDARD") || upper.includes("BIZ"))
return { key: "business", label: "Business", variant: "warning", rank: 5, raw };
if (upper.includes("STUDENT"))
return { key: "pro", label: "Student", variant: "success", rank: 3, raw };
if (upper.includes("ULTRA"))
return { key: "ultra", label: "Ultra", variant: "success", rank: 4, raw };
return null;
}
function matchTokenPlanTier(raw: string, upper: string) {
if (hasTierToken(upper, "MAX"))
return { key: "ultra", label: "Max", variant: "success", rank: 4, raw };
if (hasTierToken(upper, "PRO") || hasTierToken(upper, "PREMIUM"))
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
if (hasTierToken(upper, "STARTER"))
return { key: "lite", label: "Starter", variant: "primary", rank: 2, raw };
if (hasTierToken(upper, "LITE") || hasTierToken(upper, "LIGHT"))
return { key: "lite", label: "Lite", variant: "primary", rank: 2, raw };
if (hasTierToken(upper, "PLUS") || hasTierToken(upper, "PAID"))
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
return null;
}
function matchFreePlanTier(raw: string, upper: string) {
return upper.includes("FREE") ||
upper.includes("BASIC") ||
upper.includes("TRIAL") ||
upper.includes("LEGACY")
? { key: "free", label: "Free", variant: "default", rank: 1, raw }
: null;
}
/**
* Normalize provider-specific plan labels into a shared tier taxonomy.
* Supported tiers: enterprise, business, team, ultra, pro, plus, lite, free, unknown.
*/
export function normalizePlanTier(plan) {
const raw = typeof plan === "string" ? plan.trim() : "";
if (!raw) {
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw: null };
}
if (!raw) return unknownPlanTier(null);
const upper = raw.toUpperCase();
// Provider names that are not real plan tiers — treat as unknown
if (PROVIDER_PLAN_FALLBACKS.has(raw.toLowerCase())) {
return { key: "unknown", label: "Unknown", variant: "default", rank: 0, raw };
}
if (PROVIDER_PLAN_FALLBACKS.has(raw.toLowerCase())) return unknownPlanTier(raw);
// Match Anthropic bootstrap strings (claude_max, default_claude_max_20x, etc.)
// before the generic PRO/TEAM checks so underscored values don't fall through.
const claudeMatch = upper.match(/(?:DEFAULT_)?CLAUDE_(MAX|PRO|TEAM|ENTERPRISE|FREE)(?:_(\d+X))?/);
if (claudeMatch) {
const family = claudeMatch[1];
const multiplier = claudeMatch[2] ? ` ${claudeMatch[2].toLowerCase()}` : "";
if (family === "MAX") {
return { key: "ultra", label: `Max${multiplier}`, variant: "success", rank: 4, raw };
}
if (family === "PRO") {
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
}
if (family === "TEAM") {
return { key: "team", label: "Team", variant: "info", rank: 6, raw };
}
if (family === "ENTERPRISE") {
return { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw };
}
if (family === "FREE") {
return { key: "free", label: "Free", variant: "default", rank: 1, raw };
}
}
if (upper.includes("PRO+") || upper.includes("PRO PLUS") || upper.includes("PROPLUS")) {
return { key: "plus", label: "Pro+", variant: "success", rank: 4, raw };
}
if (upper.includes("ENTERPRISE") || upper.includes("CORP") || upper.includes("ORG")) {
return { key: "enterprise", label: "Enterprise", variant: "info", rank: 7, raw };
}
// Team plan (e.g., ChatGPT Team, GitHub Team)
if (upper.includes("TEAM") || upper.includes("CHATGPTTEAM")) {
return { key: "team", label: "Team", variant: "info", rank: 6, raw };
}
if (upper.includes("BUSINESS") || upper.includes("STANDARD") || upper.includes("BIZ")) {
return { key: "business", label: "Business", variant: "warning", rank: 5, raw };
}
if (upper.includes("STUDENT")) {
return { key: "pro", label: "Student", variant: "success", rank: 3, raw };
}
if (upper.includes("ULTRA")) {
return { key: "ultra", label: "Ultra", variant: "success", rank: 4, raw };
}
if (hasTierToken(upper, "MAX")) {
return { key: "ultra", label: "Max", variant: "success", rank: 4, raw };
}
if (hasTierToken(upper, "PRO") || hasTierToken(upper, "PREMIUM")) {
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
}
if (hasTierToken(upper, "STARTER")) {
return { key: "lite", label: "Starter", variant: "primary", rank: 2, raw };
}
if (hasTierToken(upper, "LITE") || hasTierToken(upper, "LIGHT")) {
return { key: "lite", label: "Lite", variant: "primary", rank: 2, raw };
}
if (hasTierToken(upper, "PLUS") || hasTierToken(upper, "PAID")) {
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
}
if (
upper.includes("FREE") ||
upper.includes("BASIC") ||
upper.includes("TRIAL") ||
upper.includes("LEGACY")
) {
return { key: "free", label: "Free", variant: "default", rank: 1, raw };
}
const titleCased = raw
.toLowerCase()
.split(/[\s_-]+/)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
return { key: "unknown", label: titleCased || "Unknown", variant: "default", rank: 0, raw };
const matched =
matchClaudePlanTier(raw, upper) ||
matchKeywordPlanTier(raw, upper) ||
matchTokenPlanTier(raw, upper) ||
matchFreePlanTier(raw, upper);
return matched || { ...unknownPlanTier(raw), label: formatUnknownPlanLabel(raw) || "Unknown" };
}
// === Card Grid Helpers (T7) =================================================
@@ -691,6 +409,68 @@ export function getNextResetSummary(quotas: any[] | undefined): string | null {
return soonestIso ? formatCountdown(soonestIso) : null;
}
function addQuotaModelIdVariants(out: Set<string>, provider: string, modelId: string) {
const raw = modelId.trim().toLowerCase();
const providerId = provider.trim().toLowerCase();
if (!raw) return;
out.add(raw);
if (!providerId) return;
const prefix = `${providerId}/`;
if (raw.startsWith(prefix)) {
const stripped = raw.slice(prefix.length);
if (stripped) out.add(stripped);
} else {
out.add(`${providerId}/${raw}`);
}
}
export function collectHiddenQuotaModelIds(provider: string, payload: unknown): string[] {
const hidden = new Set<string>();
const data = toRecord(payload);
const collect = (entries: unknown) => {
if (!Array.isArray(entries)) return;
for (const entry of entries) {
const record = toRecord(entry);
if (record.isHidden !== true && record.isDeleted !== true) continue;
if (typeof record.id === "string") addQuotaModelIdVariants(hidden, provider, record.id);
}
};
collect(data.models);
collect(data.modelCompatOverrides);
return Array.from(hidden);
}
export function filterHiddenModelQuotas(
provider: string,
quotas: any[] | undefined,
hiddenModelIds: string[] | undefined
): any[] {
if (!Array.isArray(quotas)) return [];
if (!hiddenModelIds || hiddenModelIds.length === 0) return quotas;
const hidden = new Set(
hiddenModelIds.map((id) => id.trim().toLowerCase()).filter((id) => id.length > 0)
);
if (hidden.size === 0) return quotas;
return quotas.filter((quota) => {
if (!quota || quota.isCredits) return true;
const modelId =
typeof quota.modelKey === "string"
? quota.modelKey
: typeof quota.modelId === "string"
? quota.modelId
: "";
if (!modelId) return true;
const candidates = new Set<string>();
addQuotaModelIdVariants(candidates, provider, modelId);
return !Array.from(candidates).some((candidate) => hidden.has(candidate));
});
}
// --- Provider dropdown filter (PR #769 port) -----------------------------
// Pure helpers extracted from <ProviderLimits/> so the filter+dropdown logic
// can be exercised by unit tests without rendering React. Keep them free of

View File

@@ -209,6 +209,13 @@ export function sanitizeProviderSpecificDataForResponse(value: unknown): JsonRec
delete sanitized.awsSecretAccessKey;
delete sanitized.sessionToken;
delete sanitized.awsSessionToken;
delete sanitized.openCodeGoAuthCookie;
delete sanitized.opencodeGoAuthCookie;
delete sanitized.authCookie;
delete sanitized.ollamaUsageCookie;
delete sanitized.ollamaCloudUsageCookie;
delete sanitized.ollamaCloudCookie;
delete sanitized.usageCookie;
return sanitized;
}

View File

@@ -35,7 +35,6 @@ import { isUserCallableAgyModelId } from "@omniroute/open-sse/config/agyModels.t
import { onUsageRecorded } from "./usageEvents";
type JsonRecord = Record<string, unknown>;
type SyncSource = "manual" | "scheduled";
interface ProviderConnectionLike {
@@ -64,6 +63,7 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"zai",
"glmt",
"opencode-go",
"ollama-cloud",
"minimax",
"minimax-cn",
"crof",

View File

@@ -1,5 +1,3 @@
// Provider definitions
/**
* Service kind — declarative tag for what a provider can do beyond basic LLM chat.
* Affects UI filtering and playground routing; does not influence request routing.
@@ -3229,6 +3227,7 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"zai",
"glmt",
"opencode-go",
"ollama-cloud",
"minimax",
"minimax-cn",
"crof",

View File

@@ -179,6 +179,50 @@ export function validateProviderSpecificData(
});
}
for (const key of ["openCodeGoWorkspaceId", "opencodeGoWorkspaceId", "workspaceId"] as const) {
const value = data[key];
if (value !== undefined && value !== null && typeof value !== "string") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be a string`,
path: [key],
});
}
if (typeof value === "string" && value.length > 1000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be at most 1000 characters`,
path: [key],
});
}
}
for (const key of [
"openCodeGoAuthCookie",
"opencodeGoAuthCookie",
"authCookie",
"ollamaUsageCookie",
"ollamaCloudUsageCookie",
"ollamaCloudCookie",
"usageCookie",
] as const) {
const value = data[key];
if (value !== undefined && value !== null && typeof value !== "string") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be a string`,
path: [key],
});
}
if (typeof value === "string" && value.length > 10000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be at most 10000 characters`,
path: [key],
});
}
}
const groupTag = data.tag;
if (
groupTag !== undefined &&

View File

@@ -18,7 +18,14 @@ import {
} from "@/shared/constants/upstreamHeaders";
import { MAX_TIMER_TIMEOUT_MS } from "@/shared/utils/runtimeTimeouts";
import { isHttpUrl, CODEX_REASONING_EFFORT_VALUES, REQUEST_DEFAULT_SERVICE_TIER_VALUES, upstreamHeadersRecordSchema, modelCompatPerProtocolSchema, customHeadersSchema } from "./misc.ts";
import {
isHttpUrl,
CODEX_REASONING_EFFORT_VALUES,
REQUEST_DEFAULT_SERVICE_TIER_VALUES,
upstreamHeadersRecordSchema,
modelCompatPerProtocolSchema,
customHeadersSchema,
} from "./misc.ts";
export function validateProviderSpecificData(
data: Record<string, unknown> | undefined,
@@ -184,6 +191,50 @@ export function validateProviderSpecificData(
});
}
for (const key of ["openCodeGoWorkspaceId", "opencodeGoWorkspaceId", "workspaceId"] as const) {
const value = data[key];
if (value !== undefined && value !== null && typeof value !== "string") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be a string`,
path: [key],
});
}
if (typeof value === "string" && value.length > 1000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be at most 1000 characters`,
path: [key],
});
}
}
for (const key of [
"openCodeGoAuthCookie",
"opencodeGoAuthCookie",
"authCookie",
"ollamaUsageCookie",
"ollamaCloudUsageCookie",
"ollamaCloudCookie",
"usageCookie",
] as const) {
const value = data[key];
if (value !== undefined && value !== null && typeof value !== "string") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be a string`,
path: [key],
});
}
if (typeof value === "string" && value.length > 10000) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `providerSpecificData.${key} must be at most 10000 characters`,
path: [key],
});
}
}
const groupTag = data.tag;
if (
groupTag !== undefined &&
@@ -361,7 +412,10 @@ export const bulkWebSessionImportSchema = z.object({
.array(
z.object({
name: z.string().min(1).max(200),
credential: z.string().min(1).max(64 * 1024, "Credential must be under 64 KB"),
credential: z
.string()
.min(1)
.max(64 * 1024, "Credential must be under 64 KB"),
})
)
.min(1, "entries must contain at least 1 item")
@@ -632,4 +686,4 @@ export const validateProviderApiKeySchema = z
path: ["cx"],
});
}
});
});

View File

@@ -17,6 +17,10 @@ describe("maskKey", () => {
assert.equal(maskKey("sk-12345"), "sk-12345");
});
it("does not double-mask API keys that are already masked by the server", () => {
assert.equal(maskKey("sk-live-****1002"), "sk-live-****1002");
});
it("keeps the first 8 chars and appends an ellipsis when the key is longer", () => {
const full = "sk-or-1234567890abcdef";
const masked = maskKey(full);

View File

@@ -274,11 +274,10 @@ async function resetStorage() {
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
// 10s ceiling: on 2-core CI runners under shard contention the 1500ms budget
// expired mid-flight (observed: 1580ms fail on the upstream-timeout test) —
// green runs return as soon as the condition holds, so the ceiling only
// bounds the failure case.
async function waitFor(fn, timeoutMs = 10000) {
// 30s ceiling: c8 instrumentation plus --test-concurrency=8 can stall CI workers
// well past the upstream timeout budget. Green runs return as soon as the condition
// holds, so the ceiling only bounds the failure case.
async function waitFor(fn, timeoutMs = 30000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const result = await fn();

View File

@@ -0,0 +1,169 @@
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 ollama-cloud", () => {
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as string[]).includes("ollama-cloud"),
"ollama-cloud must be in the usage-supported providers allowlist"
);
});
test("getUsageForProvider returns helpful message when Ollama Cloud has no usage cookie", async () => {
const originalCookie = process.env.OLLAMA_USAGE_COOKIE;
const originalOmniCookie = process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
delete process.env.OLLAMA_USAGE_COOKIE;
delete process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
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: "ollama-cloud-no-cookie",
provider: "ollama-cloud",
apiKey: "ollama-chat-key",
})) as { message?: string };
assert.equal(called, false, "settings scrape must not run without a cookie");
assert.match(result.message ?? "", /Ollama Cloud/);
assert.match(result.message ?? "", /OLLAMA_USAGE_COOKIE/);
} finally {
globalThis.fetch = originalFetch;
if (originalCookie === undefined) delete process.env.OLLAMA_USAGE_COOKIE;
else process.env.OLLAMA_USAGE_COOKIE = originalCookie;
if (originalOmniCookie === undefined) delete process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
else process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE = originalOmniCookie;
}
});
test("getUsageForProvider scrapes Ollama Cloud settings quota", async () => {
const originalFetch = globalThis.fetch;
const originalCookie = process.env.OLLAMA_USAGE_COOKIE;
const originalOmniCookie = process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
delete process.env.OLLAMA_USAGE_COOKIE;
process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE = "__Secure-session=test-cookie";
let requestUrl = "";
let requestHeaders: Headers | null = null;
let redirectMode: RequestRedirect | undefined;
globalThis.fetch = async (input, init) => {
requestUrl = String(input);
requestHeaders = new Headers(init?.headers as HeadersInit | undefined);
redirectMode = init?.redirect;
return new Response(
[
'<span class="capitalize">pro</span>',
'<div data-usage-track aria-label="34% used" style="width: 34%"></div>',
'<span class="local-time" data-time="2026-06-22T15:00:00.000Z"></span>',
'<div data-usage-track style="width: 67%"></div>',
'<span class="local-time" data-time="2026-06-29T15:00:00.000Z"></span>',
].join(""),
{ status: 200, headers: { "content-type": "text/html" } }
);
};
try {
const result = (await usage.getUsageForProvider({
id: "ollama-cloud-settings",
provider: "ollama-cloud",
apiKey: "ollama-chat-key",
})) as {
plan?: string | null;
quotas?: Record<string, { used: number; total: number; remainingPercentage: number }>;
};
assert.equal(requestUrl, "https://ollama.com/settings");
assert.equal(requestHeaders?.get("Cookie"), "__Secure-session=test-cookie");
assert.equal(redirectMode, "manual");
assert.equal(result.plan, "Ollama Cloud pro");
assert.deepEqual(Object.keys(result.quotas ?? {}), ["session", "weekly"]);
assert.equal(result.quotas!.session.used, 34);
assert.equal(result.quotas!.session.remainingPercentage, 66);
assert.equal(result.quotas!.weekly.used, 67);
assert.equal(result.quotas!.weekly.remainingPercentage, 33);
} finally {
globalThis.fetch = originalFetch;
if (originalCookie === undefined) delete process.env.OLLAMA_USAGE_COOKIE;
else process.env.OLLAMA_USAGE_COOKIE = originalCookie;
if (originalOmniCookie === undefined) delete process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
else process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE = originalOmniCookie;
}
});
test("getUsageForProvider keeps Ollama Cloud reset times aligned to usage tracks", async () => {
const originalFetch = globalThis.fetch;
const originalCookie = process.env.OLLAMA_USAGE_COOKIE;
const originalOmniCookie = process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
delete process.env.OLLAMA_USAGE_COOKIE;
process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE = "test-cookie";
globalThis.fetch = async () =>
new Response(
[
'<span class="local-time" data-time="2026-01-01T00:00:00.000Z"></span>',
'<div data-usage-track aria-label="34% used" style="width: 1%">',
'<span class="local-time" data-time="2026-06-22T15:00:00.000Z"></span>',
"</div>",
'<div data-usage-track style="width: 67%">',
'<span style="width: 1%"></span>',
'<span class="local-time" data-time="2026-06-29T15:00:00.000Z"></span>',
"</div>",
].join(""),
{ status: 200, headers: { "content-type": "text/html" } }
);
try {
const result = (await usage.getUsageForProvider({
id: "ollama-cloud-aligned-times",
provider: "ollama-cloud",
apiKey: "ollama-chat-key",
})) as {
quotas?: Record<string, { used: number; resetAt: string | null }>;
};
assert.equal(result.quotas!.session.used, 34);
assert.equal(result.quotas!.session.resetAt, "2026-06-22T15:00:00.000Z");
assert.equal(result.quotas!.weekly.used, 67);
assert.equal(result.quotas!.weekly.resetAt, "2026-06-29T15:00:00.000Z");
} finally {
globalThis.fetch = originalFetch;
if (originalCookie === undefined) delete process.env.OLLAMA_USAGE_COOKIE;
else process.env.OLLAMA_USAGE_COOKIE = originalCookie;
if (originalOmniCookie === undefined) delete process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE;
else process.env.OMNIROUTE_OLLAMA_USAGE_COOKIE = originalOmniCookie;
}
});
test("getUsageForProvider reports expired Ollama Cloud cookies on redirect", async () => {
const originalFetch = globalThis.fetch;
const originalCookie = process.env.OLLAMA_USAGE_COOKIE;
process.env.OLLAMA_USAGE_COOKIE = "expired-cookie";
globalThis.fetch = async () =>
new Response("", {
status: 302,
headers: { location: "/signin" },
});
try {
const result = (await usage.getUsageForProvider({
id: "ollama-cloud-redirect",
provider: "ollama-cloud",
apiKey: "ollama-chat-key",
})) as { message?: string };
assert.match(result.message ?? "", /authentication expired/i);
} finally {
globalThis.fetch = originalFetch;
if (originalCookie === undefined) delete process.env.OLLAMA_USAGE_COOKIE;
else process.env.OLLAMA_USAGE_COOKIE = originalCookie;
}
});

View File

@@ -28,6 +28,7 @@ test("getUsageForProvider returns helpful message when opencode-go has no apiKey
assert.equal(called, false, "quota fetch must not run without an API key");
assert.match(result.message ?? "", /OpenCode Go/);
assert.match(result.message ?? "", /OPENCODE_GO_WORKSPACE_ID/);
} finally {
globalThis.fetch = originalFetch;
}
@@ -104,7 +105,7 @@ test("getUsageForProvider exposes OpenCode Go 5h, weekly, and monthly quotas", a
};
assert.equal(requestUrl, "https://api.z.ai/api/monitor/usage/quota/limit");
assert.equal(requestHeaders?.get("Authorization"), "opencode-go-key");
assert.equal(requestHeaders?.get("Authorization"), "Bearer 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"]);
@@ -136,6 +137,107 @@ test("getUsageForProvider exposes OpenCode Go 5h, weekly, and monthly quotas", a
}
});
test("getUsageForProvider ignores out-of-range OpenCode Go reset timestamps", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
code: 200,
success: true,
data: {
level: "pro",
limits: [
{
type: "TOKENS_LIMIT",
unit: 3,
number: 5,
percentage: 25,
nextResetTime: Number.MAX_VALUE,
},
],
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
try {
const result = (await usage.getUsageForProvider({
id: "opencode-go-huge-reset",
provider: "opencode-go",
apiKey: "opencode-go-key",
})) as { quotas?: Record<string, { resetAt: string | null }> };
assert.equal(result.quotas!.session.resetAt, null);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider scrapes OpenCode Go dashboard quota when workspace cookie is configured", async () => {
const originalFetch = globalThis.fetch;
const originalWorkspace = process.env.OPENCODE_GO_WORKSPACE_ID;
const originalCookie = process.env.OPENCODE_GO_AUTH_COOKIE;
let requestUrl = "";
let requestHeaders: Headers | null = null;
process.env.OPENCODE_GO_WORKSPACE_ID = "workspace-123";
process.env.OPENCODE_GO_AUTH_COOKIE = "auth-cookie-value";
globalThis.fetch = async (input, init) => {
requestUrl = String(input);
requestHeaders = new Headers(init?.headers as HeadersInit | undefined);
return new Response(
[
'<div data-slot="usage-item">',
'<span data-slot="usage-label">Rolling Usage</span>',
'<span data-slot="usage-value">25%</span>',
'<span data-slot="reset-time">Resets in 1 hour 30 minutes</span>',
"</div>",
'<div data-slot="usage-item">',
'<span data-slot="usage-label">Weekly Usage</span>',
'<span data-slot="usage-value">50%</span>',
'<span data-slot="reset-time">Resets in 2 days</span>',
"</div>",
'<div data-slot="usage-item">',
'<span data-slot="usage-label">Monthly Usage</span>',
'<span data-slot="usage-value">10%</span>',
'<span data-slot="reset-time">Resets in 10 days</span>',
"</div>",
].join(""),
{ status: 200, headers: { "content-type": "text/html" } }
);
};
try {
const result = (await usage.getUsageForProvider({
id: "opencode-go-dashboard",
provider: "opencode-go",
apiKey: "opencode-go-key",
})) as {
plan?: string | null;
quotas?: Record<string, { used: number; total: number; remainingPercentage: number }>;
};
assert.equal(requestUrl, "https://opencode.ai/workspace/workspace-123/go");
assert.equal(requestHeaders?.get("Cookie"), "auth=auth-cookie-value");
assert.equal(result.plan, "OpenCode Go");
assert.deepEqual(Object.keys(result.quotas ?? {}), ["session", "weekly", "mcp_monthly"]);
assert.equal(result.quotas!.session.used, 3);
assert.equal(result.quotas!.session.total, 12);
assert.equal(result.quotas!.session.remainingPercentage, 75);
assert.equal(result.quotas!.weekly.used, 15);
assert.equal(result.quotas!.weekly.remainingPercentage, 50);
assert.equal(result.quotas!.mcp_monthly.used, 6);
assert.equal(result.quotas!.mcp_monthly.remainingPercentage, 90);
} finally {
globalThis.fetch = originalFetch;
if (originalWorkspace === undefined) delete process.env.OPENCODE_GO_WORKSPACE_ID;
else process.env.OPENCODE_GO_WORKSPACE_ID = originalWorkspace;
if (originalCookie === undefined) delete process.env.OPENCODE_GO_AUTH_COOKIE;
else process.env.OPENCODE_GO_AUTH_COOKIE = originalCookie;
}
});
test("getUsageForProvider returns message for invalid OpenCode Go API keys", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response("nope", { status: 401 });
@@ -148,15 +250,34 @@ test("getUsageForProvider returns message for invalid OpenCode Go API keys", asy
})) as { message: string };
assert.equal(
result.message,
"OpenCode Go does not expose a public quota API. Chat requests still work. " +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status."
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider returns message when OpenCode Go quota fetch fails", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {
throw new Error("network offline");
};
try {
const result = (await usage.getUsageForProvider({
id: "opencode-go-network-error",
provider: "opencode-go",
apiKey: "opencode-go-key",
})) as { message: string };
assert.match(result.message, /OpenCode Go quota API error:/);
assert.match(result.message, /network offline/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("getUsageForProvider returns message when OpenCode Go quota API returns 200 with auth error in body", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
@@ -173,9 +294,8 @@ test("getUsageForProvider returns message when OpenCode Go quota API returns 200
})) as { message: string };
assert.equal(
result.message,
"OpenCode Go does not expose a public quota API. Chat requests still work. " +
"Set OMNIROUTE_OPENCODE_GO_QUOTA_URL to a working endpoint, or follow " +
"https://github.com/anomalyco/opencode/issues/16017 for upstream status."
"OpenCode Go API key is valid for chat/models but cannot read quota from the Z.AI quota API. " +
"Set OPENCODE_GO_WORKSPACE_ID and OPENCODE_GO_AUTH_COOKIE to enable dashboard quota scraping."
);
} finally {
globalThis.fetch = originalFetch;

View File

@@ -205,6 +205,41 @@ test("GLM quota rows are ordered by session, weekly, then monthly", () => {
);
});
test("hidden provider models are filtered from per-model quota rows", () => {
const quotas = providerLimitUtils.parseQuotaData("antigravity", {
quotas: {
"gpt-oss-120b-medium": { used: 2, total: 100, remainingPercentage: 98 },
"gemini-3.5-pro": { used: 10, total: 100, remainingPercentage: 90 },
credits: { remaining: 42 },
},
});
const hidden = providerLimitUtils.collectHiddenQuotaModelIds("antigravity", {
models: [{ id: "antigravity/gpt-oss-120b-medium", isHidden: true }],
modelCompatOverrides: [{ id: "gemini-3.5-flash", isDeleted: true }],
});
const visible = providerLimitUtils.filterHiddenModelQuotas("antigravity", quotas, hidden);
assert.deepEqual(
visible.map((quota) => quota.modelKey || quota.name),
["gemini-3.5-pro", "credits"]
);
});
test("hidden quota filtering keeps non-model provider quota rows", () => {
const quotas = [
{ name: "weekly", used: 2, total: 100 },
{ name: "credits", isCredits: true, remaining: 10 },
];
const hidden = providerLimitUtils.collectHiddenQuotaModelIds("antigravity", {
modelCompatOverrides: [{ id: "weekly", isHidden: true }],
});
assert.deepEqual(
providerLimitUtils.filterHiddenModelQuotas("antigravity", quotas, hidden),
quotas
);
});
test("dashboard i18n keys used by OrFallback helpers exist in en.json", () => {
const enPath = path.resolve("src/i18n/messages/en.json");
const messages = JSON.parse(readFileSync(enPath, "utf8"));

View File

@@ -175,3 +175,43 @@ test("provider schemas reject oversized OpenRouter preset values", () => {
assert.equal(created.success, false);
assert.equal(updated.success, false);
});
test("provider schemas accept quota scraping provider-specific strings", () => {
const created = createProviderSchema.safeParse({
provider: "opencode-go",
apiKey: "token",
name: "OpenCode Go",
providerSpecificData: {
opencodeGoWorkspaceId: "workspace-123",
opencodeGoAuthCookie: "auth=cookie-value",
},
});
const updated = updateProviderConnectionSchema.safeParse({
providerSpecificData: {
ollamaCloudUsageCookie: "__Secure-session=cookie-value",
},
});
assert.equal(created.success, true);
assert.equal(updated.success, true);
});
test("provider schemas reject malformed quota scraping provider-specific values", () => {
const created = createProviderSchema.safeParse({
provider: "opencode-go",
apiKey: "token",
name: "OpenCode Go",
providerSpecificData: {
opencodeGoWorkspaceId: 123,
opencodeGoAuthCookie: "x".repeat(10001),
},
});
const updated = updateProviderConnectionSchema.safeParse({
providerSpecificData: {
ollamaCloudUsageCookie: 123,
},
});
assert.equal(created.success, false);
assert.equal(updated.success, false);
});

View File

@@ -6,6 +6,7 @@ const {
ensureOpenAIStoreSessionFallback,
getClaudeCodeCompatibleRequestDefaults,
normalizeProviderSpecificData,
sanitizeProviderSpecificDataForResponse,
} = await import("../../src/lib/providers/requestDefaults.ts");
test("buildOpenAIStoreSessionId normalizes external and generated session ids", () => {
@@ -108,3 +109,19 @@ test("normalizeProviderSpecificData trims OpenRouter preset and clears empty val
assert.equal(ignored?.preset, undefined);
assert.equal(ignored?.tag, "primary");
});
test("sanitizeProviderSpecificDataForResponse removes quota scraping cookies", () => {
const sanitized = sanitizeProviderSpecificDataForResponse({
opencodeGoWorkspaceId: "workspace-123",
opencodeGoAuthCookie: "auth-cookie",
ollamaCloudUsageCookie: "ollama-cookie",
usageCookie: "fallback-cookie",
consoleApiKey: "console-key",
tag: "primary",
});
assert.deepEqual(sanitized, {
opencodeGoWorkspaceId: "workspace-123",
tag: "primary",
});
});

View File

@@ -8,9 +8,8 @@ vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AddApiKeyModal } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal"
);
const { default: AddApiKeyModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
@@ -35,10 +34,7 @@ function render(props: Record<string, unknown>) {
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
@@ -108,3 +104,58 @@ describe("AddApiKeyModal — import only free models", () => {
expect(payload.providerSpecificData?.importFreeModelsOnly).toBe(true);
});
});
describe("AddApiKeyModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and auth cookie in providerSpecificData", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "opencode-go", providerName: "OpenCode Go", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
setInputValue(nameInput, "OpenCode Go");
setInputValue(apiKeyInput, "sk-opencode-go-test");
setInputValue(workspaceInput, "workspace-123");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-123");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("saves Ollama Cloud usage cookie in providerSpecificData", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "ollama-cloud", providerName: "Ollama Cloud", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')!;
setInputValue(nameInput, "Ollama Cloud");
setInputValue(apiKeyInput, "ollama-key");
setInputValue(cookieInput, "__Secure-session=ollama-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.ollamaCloudUsageCookie).toBe(
"__Secure-session=ollama-cookie"
);
});
});

View File

@@ -11,9 +11,8 @@ vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: EditConnectionModal } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal"
);
const { default: EditConnectionModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
@@ -38,6 +37,14 @@ function render(props: Record<string, unknown>) {
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
@@ -51,7 +58,9 @@ beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() => Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response))
vi.fn(() =>
Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response)
)
);
vi.stubGlobal("localStorage", {
getItem: () => null,
@@ -159,3 +168,68 @@ describe("EditConnectionModal — import only free models", () => {
expect(onResyncModels).toHaveBeenCalledWith("conn-4");
});
});
describe("EditConnectionModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and replacement auth cookie", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "opencode-go",
connection: {
id: "conn-opencode-go",
provider: "opencode-go",
name: "OpenCode Go",
authType: "apikey",
providerSpecificData: { workspaceId: "workspace-existing" },
},
onSave,
});
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
expect(workspaceInput.value).toBe("workspace-existing");
setInputValue(workspaceInput, "workspace-updated");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-updated");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("omits Ollama Cloud usage cookie when the edit field is left blank", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "ollama-cloud",
connection: {
id: "conn-ollama-cloud",
provider: "ollama-cloud",
name: "Ollama Cloud",
authType: "apikey",
providerSpecificData: {},
},
onSave,
});
expect(el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect("ollamaCloudUsageCookie" in payload.providerSpecificData).toBe(false);
});
});