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:
@@ -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' },
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ export type QuotaProviderId =
|
||||
| 'github-copilot-addon'
|
||||
| 'google'
|
||||
| 'kimi-for-coding'
|
||||
| 'nano-gpt'
|
||||
| 'openrouter'
|
||||
| 'zai-coding-plan';
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user