diff --git a/packages/ui/src/hooks/useProviderLogo.ts b/packages/ui/src/hooks/useProviderLogo.ts index 9057ba6e..0efbc136 100644 --- a/packages/ui/src/hooks/useProviderLogo.ts +++ b/packages/ui/src/hooks/useProviderLogo.ts @@ -23,6 +23,8 @@ const LOGO_ALIAS = new Map([ ['evroc-ai', 'evroc'], ['evrocai', 'evroc'], ['ollama-cloud', 'ollama'], + ['wafer-ai', 'wafer.ai'], + ['wafer', 'wafer.ai'], ]); const normalizeProviderId = (providerId: string | null | undefined) => { diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index e191950f..29654100 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -18,6 +18,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'minimax-cn-coding-plan', name: 'MiniMax Coding Plan (minimaxi.com)' }, { id: 'minimax-coding-plan', name: 'MiniMax Coding Plan (minimax.io)' }, { id: 'ollama-cloud', name: 'Ollama Cloud' }, + { id: 'wafer', name: 'Wafer.ai' }, ]; export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce< diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 362d37b5..06348dba 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -12,7 +12,8 @@ export type QuotaProviderId = | 'zhipuai-coding-plan' | 'minimax-coding-plan' | 'minimax-cn-coding-plan' - | 'ollama-cloud'; + | 'ollama-cloud' + | 'wafer'; export interface UsageWindow { usedPercent: number | null; diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index 81e730fd..d1735f78 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -97,10 +97,19 @@ type ZhipuaiMcpTimeLimit = { type ZhipuaiPayload = { data?: { limits?: Array; - level?: string; }; }; +type WaferPayload = { + remaining_included_requests?: number | string; + included_request_limit?: number | string; + overage_request_count?: number | string; + current_period_used_percent?: number | string; + window_start?: number | string; + window_end?: number | string; + plan_tier?: string; +}; + export type ProviderResult = { providerId: string; providerName: string; @@ -439,6 +448,11 @@ export const listConfiguredQuotaProviders = () => { configured.add('ollama-cloud'); } + const waferAuth = normalizeAuthEntry(getAuthEntry(auth, ['wafer', 'wafer-ai', 'wafer_ai', 'wafer.ai'])); + if (waferAuth && ((waferAuth as Record).key || (waferAuth as Record).token)) { + configured.add('wafer'); + } + return Array.from(configured); }; @@ -1738,6 +1752,116 @@ export const fetchNanoGptQuota = async (): Promise => { } }; +const WAFER_QUOTA_URL = 'https://pass.wafer.ai/v1/inference/quota'; +const WAFER_WINDOW_SECONDS = 5 * 3600; + +export const fetchWaferQuota = async (): Promise => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, ['wafer', 'wafer-ai', 'wafer_ai', 'wafer.ai'])) as Record | null; + const apiKey = (entry?.key as string | undefined) ?? (entry?.token as string | undefined); + + if (!apiKey) { + return buildResult({ + providerId: 'wafer', + providerName: 'Wafer.ai', + ok: false, + configured: false, + error: 'Not configured', + }); + } + + const timeoutSignal = AbortSignal.timeout(15_000); + + try { + const response = await fetch(WAFER_QUOTA_URL, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Accept-Encoding': 'identity', + }, + signal: timeoutSignal, + }); + + if (!response.ok) { + return buildResult({ + providerId: 'wafer', + providerName: 'Wafer.ai', + ok: false, + configured: true, + error: `API error: ${response.status}`, + }); + } + + const payload = await response.json() as WaferPayload; + const remaining = toNumber(payload?.remaining_included_requests); + const limit = toNumber(payload?.included_request_limit); + const overage = toNumber(payload?.overage_request_count); + const usedPercentRaw = toNumber(payload?.current_period_used_percent); + const windowStart = toTimestamp(payload?.window_start); + const windowEnd = toTimestamp(payload?.window_end); + const planTier = asNonEmptyString(payload?.plan_tier); + + if (remaining === null && limit === null && overage === null && usedPercentRaw === null) { + return buildResult({ + providerId: 'wafer', + providerName: 'Wafer.ai', + ok: false, + configured: true, + error: 'No quota data in response', + }); + } + + const hasOverage = overage !== null && overage > 0; + const usedPercent = hasOverage + ? Math.max(0, usedPercentRaw ?? 0) + : Math.max(0, Math.min(100, usedPercentRaw ?? 0)); + + const windowSeconds = windowStart !== null && windowEnd !== null + ? Math.round((windowEnd - windowStart) / 1000) + : WAFER_WINDOW_SECONDS; + const windowLabel = resolveWindowLabel(windowSeconds); + + let valueLabel: string | null = null; + if (remaining !== null && limit !== null) { + const parts: string[] = []; + if (planTier) parts.push(planTier); + parts.push(`${remaining} / ${limit} left`); + if (hasOverage) parts.push(`+${overage} overage`); + valueLabel = parts.join(' · '); + } + + const windows: Record = {}; + windows[windowLabel] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt: windowEnd, + valueLabel, + }); + + return buildResult({ + providerId: 'wafer', + providerName: 'Wafer.ai', + ok: true, + configured: true, + usage: { windows }, + }); + } catch (error) { + const isTimeout = error instanceof DOMException && error.name === 'AbortError' && timeoutSignal.aborted; + const isParseError = error instanceof SyntaxError; + return buildResult({ + providerId: 'wafer', + providerName: 'Wafer.ai', + ok: false, + configured: true, + error: isTimeout + ? 'Request timed out' + : isParseError + ? 'Invalid response from provider' + : (error instanceof Error ? error.message : 'Request failed'), + }); + } +}; + export const fetchQuotaForProvider = async (providerId: string): Promise => { switch (providerId) { case 'claude': @@ -1766,6 +1890,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + return Boolean(entry?.key || entry?.token); +}; + +export const fetchQuota = async () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, aliases)); + const apiKey = entry?.key ?? entry?.token; + + if (!apiKey) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: false, + error: 'Not configured' + }); + } + + const timeoutSignal = AbortSignal.timeout(15_000); + + try { + const response = await fetch(WAFER_QUOTA_URL, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Accept-Encoding': 'identity' + }, + signal: timeoutSignal + }); + + if (!response.ok) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: `API error: ${response.status}` + }); + } + + const payload = await response.json(); + const remaining = toNumber(payload?.remaining_included_requests); + const limit = toNumber(payload?.included_request_limit); + const overage = toNumber(payload?.overage_request_count); + const usedPercentRaw = toNumber(payload?.current_period_used_percent); + const windowStart = toTimestamp(payload?.window_start); + const windowEnd = toTimestamp(payload?.window_end); + const planTier = asNonEmptyString(payload?.plan_tier); + + if (remaining === null && limit === null && overage === null && usedPercentRaw === null) { + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: 'No quota data in response' + }); + } + + const hasOverage = overage !== null && overage > 0; + const usedPercent = hasOverage + ? Math.max(0, usedPercentRaw ?? 0) + : Math.max(0, Math.min(100, usedPercentRaw ?? 0)); + + const windowSeconds = windowStart !== null && windowEnd !== null + ? Math.round((windowEnd - windowStart) / 1000) + : WAFER_WINDOW_SECONDS; + const windowLabel = resolveWindowLabel(windowSeconds); + + let valueLabel = null; + if (remaining !== null && limit !== null) { + const parts = []; + if (planTier) parts.push(planTier); + parts.push(`${remaining} / ${limit} left`); + if (hasOverage) parts.push(`+${overage} overage`); + valueLabel = parts.join(' · '); + } + + const windows = {}; + windows[windowLabel] = toUsageWindow({ + usedPercent, + windowSeconds, + resetAt: windowEnd, + valueLabel + }); + + return buildResult({ + providerId, + providerName, + ok: true, + configured: true, + usage: { windows } + }); + } catch (error) { + const isTimeout = error instanceof DOMException && error.name === 'AbortError' && timeoutSignal.aborted; + const isParseError = error instanceof SyntaxError; + return buildResult({ + providerId, + providerName, + ok: false, + configured: true, + error: isTimeout + ? 'Request timed out' + : isParseError + ? 'Invalid response from provider' + : (error instanceof Error ? error.message : 'Request failed') + }); + } +};