Files
clawdbot/src/infra/provider-usage.ts
2026-01-09 15:34:58 +00:00

955 lines
25 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
ensureAuthProfileStore,
listProfilesForProvider,
resolveApiKeyForProfile,
resolveAuthProfileOrder,
} from "../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
resolveEnvApiKey,
} from "../agents/model-auth.js";
import { normalizeProviderId } from "../agents/model-selection.js";
import { loadConfig } from "../config/config.js";
export type UsageWindow = {
label: string;
usedPercent: number;
resetAt?: number;
};
export type ProviderUsageSnapshot = {
provider: UsageProviderId;
displayName: string;
windows: UsageWindow[];
plan?: string;
error?: string;
};
export type UsageSummary = {
updatedAt: number;
providers: ProviderUsageSnapshot[];
};
export type UsageProviderId =
| "anthropic"
| "github-copilot"
| "google-gemini-cli"
| "google-antigravity"
| "openai-codex"
| "zai";
type ClaudeUsageResponse = {
five_hour?: { utilization?: number; resets_at?: string };
seven_day?: { utilization?: number; resets_at?: string };
seven_day_sonnet?: { utilization?: number };
seven_day_opus?: { utilization?: number };
};
type ClaudeWebOrganizationsResponse = Array<{
uuid?: string;
name?: string;
}>;
type ClaudeWebUsageResponse = ClaudeUsageResponse;
type CopilotUsageResponse = {
quota_snapshots?: {
premium_interactions?: { percent_remaining?: number | null };
chat?: { percent_remaining?: number | null };
};
copilot_plan?: string;
};
type GeminiUsageResponse = {
buckets?: Array<{ modelId?: string; remainingFraction?: number }>;
};
type CodexUsageResponse = {
rate_limit?: {
primary_window?: {
limit_window_seconds?: number;
used_percent?: number;
reset_at?: number;
};
secondary_window?: {
limit_window_seconds?: number;
used_percent?: number;
reset_at?: number;
};
};
plan_type?: string;
credits?: { balance?: number | string | null };
};
type ZaiUsageResponse = {
success?: boolean;
code?: number;
msg?: string;
data?: {
planName?: string;
plan?: string;
limits?: Array<{
type?: string;
percentage?: number;
unit?: number;
number?: number;
nextResetTime?: string;
}>;
};
};
type ProviderAuth = {
provider: UsageProviderId;
token: string;
accountId?: string;
};
type UsageSummaryOptions = {
now?: number;
timeoutMs?: number;
providers?: UsageProviderId[];
auth?: ProviderAuth[];
agentDir?: string;
fetch?: typeof fetch;
};
const DEFAULT_TIMEOUT_MS = 5000;
const PROVIDER_LABELS: Record<UsageProviderId, string> = {
anthropic: "Claude",
"github-copilot": "Copilot",
"google-gemini-cli": "Gemini",
"google-antigravity": "Antigravity",
"openai-codex": "Codex",
zai: "z.ai",
};
const usageProviders: UsageProviderId[] = [
"anthropic",
"github-copilot",
"google-gemini-cli",
"google-antigravity",
"openai-codex",
"zai",
];
export function resolveUsageProviderId(
provider?: string | null,
): UsageProviderId | undefined {
if (!provider) return undefined;
const normalized = normalizeProviderId(provider);
return usageProviders.includes(normalized as UsageProviderId)
? (normalized as UsageProviderId)
: undefined;
}
const ignoredErrors = new Set([
"No credentials",
"No token",
"No API key",
"Not logged in",
"No auth",
]);
const clampPercent = (value: number) =>
Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
const withTimeout = async <T>(
work: Promise<T>,
ms: number,
fallback: T,
): Promise<T> => {
let timeout: NodeJS.Timeout | undefined;
try {
return await Promise.race([
work,
new Promise<T>((resolve) => {
timeout = setTimeout(() => resolve(fallback), ms);
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
};
function formatResetRemaining(targetMs?: number, now?: number): string | null {
if (!targetMs) return null;
const base = now ?? Date.now();
const diffMs = targetMs - base;
if (diffMs <= 0) return "now";
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 60) return `${diffMins}m`;
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
if (hours < 24) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d ${hours % 24}h`;
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
}).format(new Date(targetMs));
}
function resolveClaudeWebSessionKey(): string | undefined {
const direct =
process.env.CLAUDE_AI_SESSION_KEY?.trim() ??
process.env.CLAUDE_WEB_SESSION_KEY?.trim();
if (direct?.startsWith("sk-ant-")) return direct;
const cookieHeader = process.env.CLAUDE_WEB_COOKIE?.trim();
if (!cookieHeader) return undefined;
const stripped = cookieHeader.replace(/^cookie:\\s*/i, "");
const match = stripped.match(/(?:^|;\\s*)sessionKey=([^;\\s]+)/i);
const value = match?.[1]?.trim();
return value?.startsWith("sk-ant-") ? value : undefined;
}
function pickPrimaryWindow(windows: UsageWindow[]): UsageWindow | undefined {
if (windows.length === 0) return undefined;
return windows.reduce((best, next) =>
next.usedPercent > best.usedPercent ? next : best,
);
}
function formatWindowShort(window: UsageWindow, now?: number): string {
const remaining = clampPercent(100 - window.usedPercent);
const reset = formatResetRemaining(window.resetAt, now);
const resetSuffix = reset ? `${reset}` : "";
return `${remaining.toFixed(0)}% left (${window.label}${resetSuffix})`;
}
export function formatUsageSummaryLine(
summary: UsageSummary,
opts?: { now?: number; maxProviders?: number },
): string | null {
const providers = summary.providers
.filter((entry) => entry.windows.length > 0 && !entry.error)
.slice(0, opts?.maxProviders ?? summary.providers.length);
if (providers.length === 0) return null;
const parts = providers
.map((entry) => {
const window = pickPrimaryWindow(entry.windows);
if (!window) return null;
return `${entry.displayName} ${formatWindowShort(window, opts?.now)}`;
})
.filter(Boolean) as string[];
if (parts.length === 0) return null;
return `📊 Usage: ${parts.join(" · ")}`;
}
export function formatUsageReportLines(
summary: UsageSummary,
opts?: { now?: number },
): string[] {
if (summary.providers.length === 0) {
return ["Usage: no provider usage available."];
}
const lines: string[] = ["Usage:"];
for (const entry of summary.providers) {
const planSuffix = entry.plan ? ` (${entry.plan})` : "";
if (entry.error) {
lines.push(` ${entry.displayName}${planSuffix}: ${entry.error}`);
continue;
}
if (entry.windows.length === 0) {
lines.push(` ${entry.displayName}${planSuffix}: no data`);
continue;
}
lines.push(` ${entry.displayName}${planSuffix}`);
for (const window of entry.windows) {
const remaining = clampPercent(100 - window.usedPercent);
const reset = formatResetRemaining(window.resetAt, opts?.now);
const resetSuffix = reset ? ` · resets ${reset}` : "";
lines.push(
` ${window.label}: ${remaining.toFixed(0)}% left${resetSuffix}`,
);
}
}
return lines;
}
function parseGoogleToken(apiKey: string): { token: string } | null {
if (!apiKey) return null;
try {
const parsed = JSON.parse(apiKey) as { token?: unknown };
if (parsed && typeof parsed.token === "string") {
return { token: parsed.token };
}
} catch {
// ignore
}
return null;
}
async function fetchJson(
url: string,
init: RequestInit,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetchFn(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
async function fetchClaudeUsage(
token: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
const res = await fetchJson(
"https://api.anthropic.com/api/oauth/usage",
{
headers: {
Authorization: `Bearer ${token}`,
"User-Agent": "clawdbot",
Accept: "application/json",
"anthropic-version": "2023-06-01",
"anthropic-beta": "oauth-2025-04-20",
},
},
timeoutMs,
fetchFn,
);
if (!res.ok) {
let message: string | undefined;
try {
const data = (await res.json()) as {
error?: { message?: unknown } | null;
};
const raw = data?.error?.message;
if (typeof raw === "string" && raw.trim()) message = raw.trim();
} catch {
// ignore parse errors
}
// Claude CLI setup-token yields tokens that can be used for inference
// but may not include user:profile scope required by the OAuth usage endpoint.
// When a claude.ai browser sessionKey is available, fall back to the web API.
if (
res.status === 403 &&
message?.includes("scope requirement user:profile")
) {
const sessionKey = resolveClaudeWebSessionKey();
if (sessionKey) {
const web = await fetchClaudeWebUsage(sessionKey, timeoutMs, fetchFn);
if (web) return web;
}
}
const suffix = message ? `: ${message}` : "";
return {
provider: "anthropic",
displayName: PROVIDER_LABELS.anthropic,
windows: [],
error: `HTTP ${res.status}${suffix}`,
};
}
const data = (await res.json()) as ClaudeUsageResponse;
const windows: UsageWindow[] = [];
if (data.five_hour?.utilization !== undefined) {
windows.push({
label: "5h",
usedPercent: clampPercent(data.five_hour.utilization),
resetAt: data.five_hour.resets_at
? new Date(data.five_hour.resets_at).getTime()
: undefined,
});
}
if (data.seven_day?.utilization !== undefined) {
windows.push({
label: "Week",
usedPercent: clampPercent(data.seven_day.utilization),
resetAt: data.seven_day.resets_at
? new Date(data.seven_day.resets_at).getTime()
: undefined,
});
}
const modelWindow = data.seven_day_sonnet || data.seven_day_opus;
if (modelWindow?.utilization !== undefined) {
windows.push({
label: data.seven_day_sonnet ? "Sonnet" : "Opus",
usedPercent: clampPercent(modelWindow.utilization),
});
}
return {
provider: "anthropic",
displayName: PROVIDER_LABELS.anthropic,
windows,
};
}
async function fetchClaudeWebUsage(
sessionKey: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot | null> {
const headers: Record<string, string> = {
Cookie: `sessionKey=${sessionKey}`,
Accept: "application/json",
};
const orgRes = await fetchJson(
"https://claude.ai/api/organizations",
{ headers },
timeoutMs,
fetchFn,
);
if (!orgRes.ok) return null;
const orgs = (await orgRes.json()) as ClaudeWebOrganizationsResponse;
const orgId = orgs?.[0]?.uuid?.trim();
if (!orgId) return null;
const usageRes = await fetchJson(
`https://claude.ai/api/organizations/${orgId}/usage`,
{ headers },
timeoutMs,
fetchFn,
);
if (!usageRes.ok) return null;
const data = (await usageRes.json()) as ClaudeWebUsageResponse;
const windows: UsageWindow[] = [];
if (data.five_hour?.utilization !== undefined) {
windows.push({
label: "5h",
usedPercent: clampPercent(data.five_hour.utilization),
resetAt: data.five_hour.resets_at
? new Date(data.five_hour.resets_at).getTime()
: undefined,
});
}
if (data.seven_day?.utilization !== undefined) {
windows.push({
label: "Week",
usedPercent: clampPercent(data.seven_day.utilization),
resetAt: data.seven_day.resets_at
? new Date(data.seven_day.resets_at).getTime()
: undefined,
});
}
const modelWindow = data.seven_day_sonnet || data.seven_day_opus;
if (modelWindow?.utilization !== undefined) {
windows.push({
label: data.seven_day_sonnet ? "Sonnet" : "Opus",
usedPercent: clampPercent(modelWindow.utilization),
});
}
if (windows.length === 0) return null;
return {
provider: "anthropic",
displayName: PROVIDER_LABELS.anthropic,
windows,
};
}
async function fetchCopilotUsage(
token: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
const res = await fetchJson(
"https://api.github.com/copilot_internal/user",
{
headers: {
Authorization: `token ${token}`,
"Editor-Version": "vscode/1.96.2",
"User-Agent": "GitHubCopilotChat/0.26.7",
"X-Github-Api-Version": "2025-04-01",
},
},
timeoutMs,
fetchFn,
);
if (!res.ok) {
return {
provider: "github-copilot",
displayName: PROVIDER_LABELS["github-copilot"],
windows: [],
error: `HTTP ${res.status}`,
};
}
const data = (await res.json()) as CopilotUsageResponse;
const windows: UsageWindow[] = [];
if (data.quota_snapshots?.premium_interactions) {
const remaining =
data.quota_snapshots.premium_interactions.percent_remaining;
windows.push({
label: "Premium",
usedPercent: clampPercent(100 - (remaining ?? 0)),
});
}
if (data.quota_snapshots?.chat) {
const remaining = data.quota_snapshots.chat.percent_remaining;
windows.push({
label: "Chat",
usedPercent: clampPercent(100 - (remaining ?? 0)),
});
}
return {
provider: "github-copilot",
displayName: PROVIDER_LABELS["github-copilot"],
windows,
plan: data.copilot_plan,
};
}
async function fetchGeminiUsage(
token: string,
timeoutMs: number,
fetchFn: typeof fetch,
provider: UsageProviderId,
): Promise<ProviderUsageSnapshot> {
const res = await fetchJson(
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: "{}",
},
timeoutMs,
fetchFn,
);
if (!res.ok) {
return {
provider,
displayName: PROVIDER_LABELS[provider],
windows: [],
error: `HTTP ${res.status}`,
};
}
const data = (await res.json()) as GeminiUsageResponse;
const quotas: Record<string, number> = {};
for (const bucket of data.buckets || []) {
const model = bucket.modelId || "unknown";
const frac = bucket.remainingFraction ?? 1;
if (!quotas[model] || frac < quotas[model]) quotas[model] = frac;
}
const windows: UsageWindow[] = [];
let proMin = 1;
let flashMin = 1;
let hasPro = false;
let hasFlash = false;
for (const [model, frac] of Object.entries(quotas)) {
const lower = model.toLowerCase();
if (lower.includes("pro")) {
hasPro = true;
if (frac < proMin) proMin = frac;
}
if (lower.includes("flash")) {
hasFlash = true;
if (frac < flashMin) flashMin = frac;
}
}
if (hasPro) {
windows.push({
label: "Pro",
usedPercent: clampPercent((1 - proMin) * 100),
});
}
if (hasFlash) {
windows.push({
label: "Flash",
usedPercent: clampPercent((1 - flashMin) * 100),
});
}
return { provider, displayName: PROVIDER_LABELS[provider], windows };
}
async function fetchCodexUsage(
token: string,
accountId: string | undefined,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
"User-Agent": "CodexBar",
Accept: "application/json",
};
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
const res = await fetchJson(
"https://chatgpt.com/backend-api/wham/usage",
{ method: "GET", headers },
timeoutMs,
fetchFn,
);
if (res.status === 401 || res.status === 403) {
return {
provider: "openai-codex",
displayName: PROVIDER_LABELS["openai-codex"],
windows: [],
error: "Token expired",
};
}
if (!res.ok) {
return {
provider: "openai-codex",
displayName: PROVIDER_LABELS["openai-codex"],
windows: [],
error: `HTTP ${res.status}`,
};
}
const data = (await res.json()) as CodexUsageResponse;
const windows: UsageWindow[] = [];
if (data.rate_limit?.primary_window) {
const pw = data.rate_limit.primary_window;
const windowHours = Math.round((pw.limit_window_seconds || 10800) / 3600);
windows.push({
label: `${windowHours}h`,
usedPercent: clampPercent(pw.used_percent || 0),
resetAt: pw.reset_at ? pw.reset_at * 1000 : undefined,
});
}
if (data.rate_limit?.secondary_window) {
const sw = data.rate_limit.secondary_window;
const windowHours = Math.round((sw.limit_window_seconds || 86400) / 3600);
const label = windowHours >= 24 ? "Day" : `${windowHours}h`;
windows.push({
label,
usedPercent: clampPercent(sw.used_percent || 0),
resetAt: sw.reset_at ? sw.reset_at * 1000 : undefined,
});
}
let plan = data.plan_type;
if (data.credits?.balance !== undefined && data.credits.balance !== null) {
const balance =
typeof data.credits.balance === "number"
? data.credits.balance
: parseFloat(data.credits.balance) || 0;
plan = plan ? `${plan} ($${balance.toFixed(2)})` : `$${balance.toFixed(2)}`;
}
return {
provider: "openai-codex",
displayName: PROVIDER_LABELS["openai-codex"],
windows,
plan,
};
}
async function fetchZaiUsage(
apiKey: string,
timeoutMs: number,
fetchFn: typeof fetch,
): Promise<ProviderUsageSnapshot> {
const res = await fetchJson(
"https://api.z.ai/api/monitor/usage/quota/limit",
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
},
timeoutMs,
fetchFn,
);
if (!res.ok) {
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
windows: [],
error: `HTTP ${res.status}`,
};
}
const data = (await res.json()) as ZaiUsageResponse;
if (!data.success || data.code !== 200) {
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
windows: [],
error: data.msg || "API error",
};
}
const windows: UsageWindow[] = [];
const limits = data.data?.limits || [];
for (const limit of limits) {
const percent = clampPercent(limit.percentage || 0);
const nextReset = limit.nextResetTime
? new Date(limit.nextResetTime).getTime()
: undefined;
let windowLabel = "Limit";
if (limit.unit === 1) windowLabel = `${limit.number}d`;
else if (limit.unit === 3) windowLabel = `${limit.number}h`;
else if (limit.unit === 5) windowLabel = `${limit.number}m`;
if (limit.type === "TOKENS_LIMIT") {
windows.push({
label: `Tokens (${windowLabel})`,
usedPercent: percent,
resetAt: nextReset,
});
} else if (limit.type === "TIME_LIMIT") {
windows.push({
label: "Monthly",
usedPercent: percent,
resetAt: nextReset,
});
}
}
const planName = data.data?.planName || data.data?.plan || undefined;
return {
provider: "zai",
displayName: PROVIDER_LABELS.zai,
windows,
plan: planName,
};
}
function resolveZaiApiKey(): string | undefined {
const envDirect =
process.env.ZAI_API_KEY?.trim() || process.env.Z_AI_API_KEY?.trim();
if (envDirect) return envDirect;
const envResolved = resolveEnvApiKey("zai");
if (envResolved?.apiKey) return envResolved.apiKey;
const cfg = loadConfig();
const key =
getCustomProviderApiKey(cfg, "zai") || getCustomProviderApiKey(cfg, "z-ai");
if (key) return key;
const store = ensureAuthProfileStore();
const apiProfile = [
...listProfilesForProvider(store, "zai"),
...listProfilesForProvider(store, "z-ai"),
].find((id) => store.profiles[id]?.type === "api_key");
if (apiProfile) {
const cred = store.profiles[apiProfile];
if (cred?.type === "api_key" && cred.key?.trim()) {
return cred.key.trim();
}
}
try {
const authPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
if (!fs.existsSync(authPath)) return undefined;
const data = JSON.parse(fs.readFileSync(authPath, "utf-8")) as Record<
string,
{ access?: string }
>;
return data["z-ai"]?.access || data.zai?.access;
} catch {
return undefined;
}
}
async function resolveOAuthToken(params: {
provider: UsageProviderId;
agentDir?: string;
}): Promise<ProviderAuth | null> {
const cfg = loadConfig();
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const order = resolveAuthProfileOrder({
cfg,
store,
provider: params.provider,
});
for (const profileId of order) {
const cred = store.profiles[profileId];
if (!cred || (cred.type !== "oauth" && cred.type !== "token")) continue;
try {
const resolved = await resolveApiKeyForProfile({
// Usage snapshots should work even if config profile metadata is stale.
// (e.g. config says api_key but the store has a token profile.)
cfg: undefined,
store,
profileId,
agentDir: params.agentDir,
});
if (!resolved?.apiKey) continue;
let token = resolved.apiKey;
if (
params.provider === "google-gemini-cli" ||
params.provider === "google-antigravity"
) {
const parsed = parseGoogleToken(resolved.apiKey);
token = parsed?.token ?? resolved.apiKey;
}
return {
provider: params.provider,
token,
accountId:
cred.type === "oauth" && "accountId" in cred
? (cred as { accountId?: string }).accountId
: undefined,
};
} catch {}
}
return null;
}
function resolveOAuthProviders(agentDir?: string): UsageProviderId[] {
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
const cfg = loadConfig();
const providers = usageProviders.filter((provider) => provider !== "zai");
const isOAuthLikeCredential = (id: string) => {
const cred = store.profiles[id];
return cred?.type === "oauth" || cred?.type === "token";
};
return providers.filter((provider) => {
const profiles = listProfilesForProvider(store, provider).filter(
isOAuthLikeCredential,
);
if (profiles.length > 0) return true;
const normalized = normalizeProviderId(provider);
const configuredProfiles = Object.entries(cfg.auth?.profiles ?? {})
.filter(
([, profile]) => normalizeProviderId(profile.provider) === normalized,
)
.map(([id]) => id)
.filter(isOAuthLikeCredential);
return configuredProfiles.length > 0;
});
}
async function resolveProviderAuths(
opts: UsageSummaryOptions,
): Promise<ProviderAuth[]> {
if (opts.auth) return opts.auth;
const targetProviders = opts.providers ?? usageProviders;
const oauthProviders = resolveOAuthProviders(opts.agentDir);
const auths: ProviderAuth[] = [];
for (const provider of targetProviders) {
if (provider === "zai") {
const apiKey = resolveZaiApiKey();
if (apiKey) auths.push({ provider, token: apiKey });
continue;
}
if (!oauthProviders.includes(provider)) continue;
const auth = await resolveOAuthToken({ provider, agentDir: opts.agentDir });
if (auth) auths.push(auth);
}
return auths;
}
export async function loadProviderUsageSummary(
opts: UsageSummaryOptions = {},
): Promise<UsageSummary> {
const now = opts.now ?? Date.now();
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const fetchFn = opts.fetch ?? fetch;
const auths = await resolveProviderAuths(opts);
if (auths.length === 0) {
return { updatedAt: now, providers: [] };
}
const tasks = auths.map((auth) =>
withTimeout(
(async (): Promise<ProviderUsageSnapshot> => {
switch (auth.provider) {
case "anthropic":
return await fetchClaudeUsage(auth.token, timeoutMs, fetchFn);
case "github-copilot":
return await fetchCopilotUsage(auth.token, timeoutMs, fetchFn);
case "google-gemini-cli":
case "google-antigravity":
return await fetchGeminiUsage(
auth.token,
timeoutMs,
fetchFn,
auth.provider,
);
case "openai-codex":
return await fetchCodexUsage(
auth.token,
auth.accountId,
timeoutMs,
fetchFn,
);
case "zai":
return await fetchZaiUsage(auth.token, timeoutMs, fetchFn);
default:
return {
provider: auth.provider,
displayName: PROVIDER_LABELS[auth.provider],
windows: [],
error: "Unsupported provider",
};
}
})(),
timeoutMs + 1000,
{
provider: auth.provider,
displayName: PROVIDER_LABELS[auth.provider],
windows: [],
error: "Timeout",
},
),
);
const snapshots = await Promise.all(tasks);
const providers = snapshots.filter((entry) => {
if (entry.windows.length > 0) return true;
if (!entry.error) return true;
return !ignoredErrors.has(entry.error);
});
return { updatedAt: now, providers };
}