Blame Raw
Cohee · 51ad27fb · · 138 lines (4.7 KB)
2 contributors
1import express from 'express';
2import fetch from 'node-fetch';
3import { readSecret, SECRET_KEYS } from './secrets.js';
4
5export const router = express.Router();
6const API_NANOGPT = 'https://nano-gpt.com/api';
7
8/**
9 * Parses a numeric API value, returning 0 for missing or invalid values.
10 * @param {unknown} value Value to parse.
11 * @returns {number}
12 */
13function parseNumber(value) {
14 const number = Number(value);
15 return Number.isFinite(number) ? number : 0;
16}
17
18/**
19 * Normalizes a NanoGPT usage bucket.
20 * @param {any} usage Usage bucket from NanoGPT.
21 * @returns {{ used: number, remaining: number, percentUsed: number, resetAt: number } | null}
22 */
23function normalizeUsage(usage) {
24 if (!usage || typeof usage !== 'object') {
25 return null;
26 }
27
28 return {
29 used: parseNumber(usage.used),
30 remaining: parseNumber(usage.remaining),
31 percentUsed: parseNumber(usage.percentUsed),
32 resetAt: parseNumber(usage.resetAt),
33 };
34}
35
36router.post('/credits', async (req, res) => {
37 try {
38 const key = readSecret(req.user.directories, SECRET_KEYS.NANOGPT);
39
40 if (!key) {
41 console.warn('NanoGPT API key not found');
42 return res.sendStatus(400);
43 }
44
45 const headers = {
46 'Accept': 'application/json',
47 'x-api-key': key,
48 };
49
50 // Fetch both Pay-As-You-Go balance and subscription usage at the same time.
51 const [balanceReq, subReq] = await Promise.allSettled([
52 fetch(`${API_NANOGPT}/check-balance`, { method: 'POST', headers }),
53 fetch(`${API_NANOGPT}/subscription/v1/usage`, { method: 'GET', headers }),
54 ]);
55
56 if (balanceReq.status !== 'fulfilled' || !balanceReq.value.ok) {
57 console.warn('NanoGPT balance request failed', balanceReq.status === 'fulfilled' ? balanceReq.value.statusText : balanceReq.reason);
58 return res.sendStatus(500);
59 }
60
61 /** @type {any} */
62 const balanceData = await balanceReq.value.json();
63 /** @type {any} */
64 const result = {
65 usd_balance: parseNumber(balanceData.usd_balance),
66 nano_balance: parseNumber(balanceData.nano_balance),
67 subscription: null,
68 };
69
70 if (subReq.status === 'fulfilled' && subReq.value.ok) {
71 /** @type {any} */
72 const subData = await subReq.value.json();
73 if (subData.active) {
74 result.subscription = {
75 active: true,
76 state: String(subData.state || ''),
77 allowOverage: Boolean(subData.allowOverage),
78 period: {
79 currentPeriodEnd: String(subData.period?.currentPeriodEnd || ''),
80 },
81 limits: {
82 weeklyInputTokens: parseNumber(subData.limits?.weeklyInputTokens),
83 dailyInputTokens: parseNumber(subData.limits?.dailyInputTokens),
84 dailyImages: parseNumber(subData.limits?.dailyImages),
85 },
86 weekly_tokens: normalizeUsage(subData.weeklyInputTokens),
87 daily_tokens: normalizeUsage(subData.dailyInputTokens),
88 daily_images: normalizeUsage(subData.dailyImages),
89 };
90 }
91 } else if (subReq.status === 'fulfilled') {
92 console.warn('NanoGPT subscription usage request failed', subReq.value.statusText);
93 } else {
94 console.warn('NanoGPT subscription usage request failed', subReq.reason);
95 }
96
97 return res.json(result);
98 } catch (error) {
99 console.error(error);
100 return res.sendStatus(500);
101 }
102});
103
104router.post('/models/providers', async (req, res) => {
105 try {
106 const { model } = req.body;
107
108 if (!model) {
109 return res.status(400).json({ supportsProviderSelection: false, providers: [] });
110 }
111
112 const encodedModel = encodeURIComponent(model);
113 const response = await fetch(`${API_NANOGPT}/models/${encodedModel}/providers`, {
114 method: 'GET',
115 headers: {
116 'Accept': 'application/json',
117 },
118 });
119
120 if (!response.ok) {
121 return res.json({ supportsProviderSelection: false, providers: [] });
122 }
123
124 /** @type {any} */
125 const data = await response.json();
126 const providers = Array.isArray(data?.providers)
127 ? data.providers.filter(p => p?.available !== false).map(p => p.provider).filter(Boolean)
128 : [];
129
130 return res.json({
131 supportsProviderSelection: Boolean(data?.supportsProviderSelection),
132 providers,
133 });
134 } catch (error) {
135 console.error(error);
136 return res.sendStatus(500);
137 }
138});