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
This commit is contained in:
Nelson Pires
2026-02-14 23:56:42 +02:00
committed by GitHub
parent 08f6bef405
commit e3deaeb314
4 changed files with 226 additions and 0 deletions
@@ -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' },
];
+1
View File
@@ -6,6 +6,7 @@ export type QuotaProviderId =
| 'github-copilot-addon'
| 'google'
| 'kimi-for-coding'
| 'nano-gpt'
| 'openrouter'
| 'zai-coding-plan';
+112
View File
@@ -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<string, unknown>).key || (nanopgAuth as Record<string, unknown>).token)) {
configured.add('nano-gpt');
}
const copilotAuth = normalizeAuthEntry(getAuthEntry(auth, ['github-copilot', 'copilot']));
if (copilotAuth && ((copilotAuth as Record<string, unknown>).access || (copilotAuth as Record<string, unknown>).token)) {
configured.add('github-copilot');
@@ -1254,6 +1259,111 @@ export const fetchZaiQuota = async (): Promise<ProviderResult> => {
}
};
const NANO_GPT_DAILY_WINDOW_SECONDS = 86400;
export const fetchNanoGptQuota = async (): Promise<ProviderResult> => {
const auth = readAuthFile();
const entry = normalizeAuthEntry(getAuthEntry(auth, ['nano-gpt', 'nanogpt', 'nano_gpt'])) as Record<string, unknown> | 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<string, unknown>;
const windows: Record<string, UsageWindow> = {};
const period = payload.period as Record<string, unknown> | undefined;
const daily = payload.daily as Record<string, unknown> | undefined;
const monthly = payload.monthly as Record<string, unknown> | 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<string, unknown>)?.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<string, unknown>)?.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<string, unknown>)?.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<ProviderResult> => {
switch (providerId) {
case 'claude':
@@ -1268,6 +1378,8 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
return fetchGoogleQuota();
case 'kimi-for-coding':
return fetchKimiQuota();
case 'nano-gpt':
return fetchNanoGptQuota();
case 'openrouter':
return fetchOpenRouterQuota();
case 'zai-coding-plan':
+112
View File
@@ -200,6 +200,11 @@ export const listConfiguredQuotaProviders = () => {
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':