From e3deaeb314a70d63ba7e97505582fc084649103d Mon Sep 17 00:00:00 2001 From: Nelson Pires Date: Sat, 14 Feb 2026 18:56:42 -0300 Subject: [PATCH] feat(quota): add NanoGPT quota provider support (#424) * feat: add NanoGPT quota provider and usage fetch Detect NanoGPT as a configured quota provider. Expose daily and monthly usage windows with reset times. Return configured and ok state based on API key presence. * feat: fetch NanoGPT quota and detect config Detect NanoGPT in configured providers from auth. Fetch and expose daily and monthly usage windows for NanoGPT. * feat: add NanoGPT quota provider option --- packages/ui/src/lib/quota/providers/index.ts | 1 + packages/ui/src/types/quota.ts | 1 + packages/vscode/src/quotaProviders.ts | 112 +++++++++++++++++++ packages/web/server/lib/quota-providers.js | 112 +++++++++++++++++++ 4 files changed, 226 insertions(+) diff --git a/packages/ui/src/lib/quota/providers/index.ts b/packages/ui/src/lib/quota/providers/index.ts index 32705133..3c871b64 100644 --- a/packages/ui/src/lib/quota/providers/index.ts +++ b/packages/ui/src/lib/quota/providers/index.ts @@ -11,6 +11,7 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [ { id: 'github-copilot', name: 'GitHub Copilot' }, { id: 'google', name: 'Google' }, { id: 'kimi-for-coding', name: 'Kimi for Coding' }, + { id: 'nano-gpt', name: 'NanoGPT' }, { id: 'openrouter', name: 'OpenRouter' }, { id: 'zai-coding-plan', name: 'z.ai' }, ]; diff --git a/packages/ui/src/types/quota.ts b/packages/ui/src/types/quota.ts index 59e1210d..7d1dafbe 100644 --- a/packages/ui/src/types/quota.ts +++ b/packages/ui/src/types/quota.ts @@ -6,6 +6,7 @@ export type QuotaProviderId = | 'github-copilot-addon' | 'google' | 'kimi-for-coding' + | 'nano-gpt' | 'openrouter' | 'zai-coding-plan'; diff --git a/packages/vscode/src/quotaProviders.ts b/packages/vscode/src/quotaProviders.ts index 3b28abb5..6d5c2aef 100644 --- a/packages/vscode/src/quotaProviders.ts +++ b/packages/vscode/src/quotaProviders.ts @@ -365,6 +365,11 @@ export const listConfiguredQuotaProviders = () => { configured.add('openrouter'); } + const nanopgAuth = normalizeAuthEntry(getAuthEntry(auth, ['nano-gpt', 'nanogpt', 'nano_gpt'])); + if (nanopgAuth && ((nanopgAuth as Record).key || (nanopgAuth as Record).token)) { + configured.add('nano-gpt'); + } + const copilotAuth = normalizeAuthEntry(getAuthEntry(auth, ['github-copilot', 'copilot'])); if (copilotAuth && ((copilotAuth as Record).access || (copilotAuth as Record).token)) { configured.add('github-copilot'); @@ -1254,6 +1259,111 @@ export const fetchZaiQuota = async (): Promise => { } }; +const NANO_GPT_DAILY_WINDOW_SECONDS = 86400; + +export const fetchNanoGptQuota = async (): Promise => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, ['nano-gpt', 'nanogpt', 'nano_gpt'])) as Record | null; + const apiKey = (entry?.key as string | undefined) ?? (entry?.token as string | undefined); + + if (!apiKey) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: false, + error: 'Not configured', + }); + } + + try { + const response = await fetch('https://nano-gpt.com/api/subscription/v1/usage', { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: true, + error: `API error: ${response.status}`, + }); + } + + const payload = await response.json() as Record; + const windows: Record = {}; + const period = payload.period as Record | undefined; + const daily = payload.daily as Record | undefined; + const monthly = payload.monthly as Record | undefined; + const state = (payload.state as string) ?? 'active'; + + if (daily) { + let usedPercent: number | null = null; + const percentUsed = daily.percentUsed as number | undefined; + if (typeof percentUsed === 'number') { + usedPercent = Math.max(0, Math.min(100, percentUsed * 100)); + } else { + const used = toNumber(daily.used); + const limit = toNumber((daily.limit as number | undefined) ?? (daily.limits as Record)?.daily); + if (used !== null && limit !== null && limit > 0) { + usedPercent = Math.max(0, Math.min(100, (used / limit) * 100)); + } + } + const resetAt = toTimestamp(daily.resetAt); + const valueLabel = state !== 'active' ? `(${state})` : null; + windows['daily'] = toUsageWindow({ + usedPercent, + windowSeconds: NANO_GPT_DAILY_WINDOW_SECONDS, + resetAt, + valueLabel, + }); + } + + if (monthly) { + let usedPercent: number | null = null; + const percentUsed = monthly.percentUsed as number | undefined; + if (typeof percentUsed === 'number') { + usedPercent = Math.max(0, Math.min(100, percentUsed * 100)); + } else { + const used = toNumber(monthly.used); + const limit = toNumber((monthly.limit as number | undefined) ?? (monthly.limits as Record)?.monthly); + if (used !== null && limit !== null && limit > 0) { + usedPercent = Math.max(0, Math.min(100, (used / limit) * 100)); + } + } + const resetAt = toTimestamp((monthly.resetAt as string | number | undefined) ?? (period as Record)?.currentPeriodEnd); + const valueLabel = state !== 'active' ? `(${state})` : null; + windows['monthly'] = toUsageWindow({ + usedPercent, + windowSeconds: null, + resetAt, + valueLabel, + }); + } + + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: true, + configured: true, + usage: { windows }, + }); + } catch (error) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed', + }); + } +}; + export const fetchQuotaForProvider = async (providerId: string): Promise => { switch (providerId) { case 'claude': @@ -1268,6 +1378,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise { configured.add('openrouter'); } + const nanopgAuth = normalizeAuthEntry(getAuthEntry(auth, ['nano-gpt', 'nanogpt', 'nano_gpt'])); + if (nanopgAuth?.key || nanopgAuth?.token) { + configured.add('nano-gpt'); + } + const copilotAuth = normalizeAuthEntry(getAuthEntry(auth, ['github-copilot', 'copilot'])); if (copilotAuth?.access || copilotAuth?.token) { configured.add('github-copilot'); @@ -1195,6 +1200,111 @@ export const fetchZaiQuota = async () => { } }; +const NANO_GPT_DAILY_WINDOW_SECONDS = 86400; + +export const fetchNanoGptQuota = async () => { + const auth = readAuthFile(); + const entry = normalizeAuthEntry(getAuthEntry(auth, ['nano-gpt', 'nanogpt', 'nano_gpt'])); + const apiKey = entry?.key ?? entry?.token; + + if (!apiKey) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: false, + error: 'Not configured' + }); + } + + try { + const response = await fetch('https://nano-gpt.com/api/subscription/v1/usage', { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: true, + error: `API error: ${response.status}` + }); + } + + const payload = await response.json(); + const windows = {}; + const period = payload?.period ?? null; + const daily = payload?.daily ?? null; + const monthly = payload?.monthly ?? null; + const state = payload?.state ?? 'active'; + + if (daily) { + let usedPercent = null; + const percentUsed = daily?.percentUsed; + if (typeof percentUsed === 'number') { + usedPercent = Math.max(0, Math.min(100, percentUsed * 100)); + } else { + const used = toNumber(daily?.used); + const limit = toNumber(daily?.limit ?? daily?.limits?.daily); + if (used !== null && limit !== null && limit > 0) { + usedPercent = Math.max(0, Math.min(100, (used / limit) * 100)); + } + } + const resetAt = toTimestamp(daily?.resetAt); + const valueLabel = state !== 'active' ? `(${state})` : null; + windows['daily'] = toUsageWindow({ + usedPercent, + windowSeconds: NANO_GPT_DAILY_WINDOW_SECONDS, + resetAt, + valueLabel + }); + } + + if (monthly) { + let usedPercent = null; + const percentUsed = monthly?.percentUsed; + if (typeof percentUsed === 'number') { + usedPercent = Math.max(0, Math.min(100, percentUsed * 100)); + } else { + const used = toNumber(monthly?.used); + const limit = toNumber(monthly?.limit ?? monthly?.limits?.monthly); + if (used !== null && limit !== null && limit > 0) { + usedPercent = Math.max(0, Math.min(100, (used / limit) * 100)); + } + } + const resetAt = toTimestamp(monthly?.resetAt ?? period?.currentPeriodEnd); + const valueLabel = state !== 'active' ? `(${state})` : null; + windows['monthly'] = toUsageWindow({ + usedPercent, + windowSeconds: null, + resetAt, + valueLabel + }); + } + + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: true, + configured: true, + usage: { windows } + }); + } catch (error) { + return buildResult({ + providerId: 'nano-gpt', + providerName: 'NanoGPT', + ok: false, + configured: true, + error: error instanceof Error ? error.message : 'Request failed' + }); + } +}; + export const fetchQuotaForProvider = async (providerId) => { switch (providerId) { case 'claude': @@ -1209,6 +1319,8 @@ export const fetchQuotaForProvider = async (providerId) => { return fetchGoogleQuota(); case 'kimi-for-coding': return fetchKimiQuota(); + case 'nano-gpt': + return fetchNanoGptQuota(); case 'openrouter': return fetchOpenRouterQuota(); case 'zai-coding-plan':