llmapi-v2 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.env.example +40 -0
  2. package/Dockerfile +17 -0
  3. package/dist/config.d.ts +48 -0
  4. package/dist/config.js +98 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/converter/request.d.ts +6 -0
  7. package/dist/converter/request.js +184 -0
  8. package/dist/converter/request.js.map +1 -0
  9. package/dist/converter/response.d.ts +6 -0
  10. package/dist/converter/response.js +76 -0
  11. package/dist/converter/response.js.map +1 -0
  12. package/dist/converter/stream.d.ts +54 -0
  13. package/dist/converter/stream.js +318 -0
  14. package/dist/converter/stream.js.map +1 -0
  15. package/dist/converter/types.d.ts +239 -0
  16. package/dist/converter/types.js +6 -0
  17. package/dist/converter/types.js.map +1 -0
  18. package/dist/data/posts.d.ts +19 -0
  19. package/dist/data/posts.js +462 -0
  20. package/dist/data/posts.js.map +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +233 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/api-key-auth.d.ts +6 -0
  25. package/dist/middleware/api-key-auth.js +76 -0
  26. package/dist/middleware/api-key-auth.js.map +1 -0
  27. package/dist/middleware/quota-guard.d.ts +10 -0
  28. package/dist/middleware/quota-guard.js +27 -0
  29. package/dist/middleware/quota-guard.js.map +1 -0
  30. package/dist/middleware/rate-limiter.d.ts +5 -0
  31. package/dist/middleware/rate-limiter.js +50 -0
  32. package/dist/middleware/rate-limiter.js.map +1 -0
  33. package/dist/middleware/request-logger.d.ts +6 -0
  34. package/dist/middleware/request-logger.js +37 -0
  35. package/dist/middleware/request-logger.js.map +1 -0
  36. package/dist/middleware/session-auth.d.ts +19 -0
  37. package/dist/middleware/session-auth.js +99 -0
  38. package/dist/middleware/session-auth.js.map +1 -0
  39. package/dist/providers/aliyun.d.ts +13 -0
  40. package/dist/providers/aliyun.js +20 -0
  41. package/dist/providers/aliyun.js.map +1 -0
  42. package/dist/providers/base-provider.d.ts +36 -0
  43. package/dist/providers/base-provider.js +133 -0
  44. package/dist/providers/base-provider.js.map +1 -0
  45. package/dist/providers/deepseek.d.ts +11 -0
  46. package/dist/providers/deepseek.js +18 -0
  47. package/dist/providers/deepseek.js.map +1 -0
  48. package/dist/providers/registry.d.ts +18 -0
  49. package/dist/providers/registry.js +98 -0
  50. package/dist/providers/registry.js.map +1 -0
  51. package/dist/providers/types.d.ts +17 -0
  52. package/dist/providers/types.js +3 -0
  53. package/dist/providers/types.js.map +1 -0
  54. package/dist/routes/admin.d.ts +1 -0
  55. package/dist/routes/admin.js +153 -0
  56. package/dist/routes/admin.js.map +1 -0
  57. package/dist/routes/auth.d.ts +2 -0
  58. package/dist/routes/auth.js +318 -0
  59. package/dist/routes/auth.js.map +1 -0
  60. package/dist/routes/blog.d.ts +1 -0
  61. package/dist/routes/blog.js +29 -0
  62. package/dist/routes/blog.js.map +1 -0
  63. package/dist/routes/dashboard.d.ts +1 -0
  64. package/dist/routes/dashboard.js +184 -0
  65. package/dist/routes/dashboard.js.map +1 -0
  66. package/dist/routes/messages.d.ts +1 -0
  67. package/dist/routes/messages.js +309 -0
  68. package/dist/routes/messages.js.map +1 -0
  69. package/dist/routes/models.d.ts +1 -0
  70. package/dist/routes/models.js +39 -0
  71. package/dist/routes/models.js.map +1 -0
  72. package/dist/routes/payment.d.ts +1 -0
  73. package/dist/routes/payment.js +150 -0
  74. package/dist/routes/payment.js.map +1 -0
  75. package/dist/routes/sitemap.d.ts +1 -0
  76. package/dist/routes/sitemap.js +38 -0
  77. package/dist/routes/sitemap.js.map +1 -0
  78. package/dist/services/alipay.d.ts +27 -0
  79. package/dist/services/alipay.js +106 -0
  80. package/dist/services/alipay.js.map +1 -0
  81. package/dist/services/database.d.ts +4 -0
  82. package/dist/services/database.js +170 -0
  83. package/dist/services/database.js.map +1 -0
  84. package/dist/services/health-checker.d.ts +13 -0
  85. package/dist/services/health-checker.js +95 -0
  86. package/dist/services/health-checker.js.map +1 -0
  87. package/dist/services/mailer.d.ts +3 -0
  88. package/dist/services/mailer.js +91 -0
  89. package/dist/services/mailer.js.map +1 -0
  90. package/dist/services/metrics.d.ts +56 -0
  91. package/dist/services/metrics.js +94 -0
  92. package/dist/services/metrics.js.map +1 -0
  93. package/dist/services/remote-control.d.ts +20 -0
  94. package/dist/services/remote-control.js +209 -0
  95. package/dist/services/remote-control.js.map +1 -0
  96. package/dist/services/remote-ws.d.ts +5 -0
  97. package/dist/services/remote-ws.js +143 -0
  98. package/dist/services/remote-ws.js.map +1 -0
  99. package/dist/services/usage.d.ts +13 -0
  100. package/dist/services/usage.js +39 -0
  101. package/dist/services/usage.js.map +1 -0
  102. package/dist/utils/errors.d.ts +27 -0
  103. package/dist/utils/errors.js +48 -0
  104. package/dist/utils/errors.js.map +1 -0
  105. package/dist/utils/logger.d.ts +2 -0
  106. package/dist/utils/logger.js +14 -0
  107. package/dist/utils/logger.js.map +1 -0
  108. package/docker-compose.yml +19 -0
  109. package/package.json +39 -0
  110. package/public/robots.txt +8 -0
  111. package/src/config.ts +140 -0
  112. package/src/converter/request.ts +207 -0
  113. package/src/converter/response.ts +85 -0
  114. package/src/converter/stream.ts +373 -0
  115. package/src/converter/types.ts +257 -0
  116. package/src/data/posts.ts +474 -0
  117. package/src/index.ts +219 -0
  118. package/src/middleware/api-key-auth.ts +82 -0
  119. package/src/middleware/quota-guard.ts +28 -0
  120. package/src/middleware/rate-limiter.ts +61 -0
  121. package/src/middleware/request-logger.ts +36 -0
  122. package/src/middleware/session-auth.ts +91 -0
  123. package/src/providers/aliyun.ts +16 -0
  124. package/src/providers/base-provider.ts +148 -0
  125. package/src/providers/deepseek.ts +14 -0
  126. package/src/providers/registry.ts +111 -0
  127. package/src/providers/types.ts +26 -0
  128. package/src/routes/admin.ts +169 -0
  129. package/src/routes/auth.ts +369 -0
  130. package/src/routes/blog.ts +28 -0
  131. package/src/routes/dashboard.ts +208 -0
  132. package/src/routes/messages.ts +346 -0
  133. package/src/routes/models.ts +37 -0
  134. package/src/routes/payment.ts +189 -0
  135. package/src/routes/sitemap.ts +40 -0
  136. package/src/services/alipay.ts +116 -0
  137. package/src/services/database.ts +187 -0
  138. package/src/services/health-checker.ts +115 -0
  139. package/src/services/mailer.ts +90 -0
  140. package/src/services/metrics.ts +104 -0
  141. package/src/services/remote-control.ts +226 -0
  142. package/src/services/remote-ws.ts +145 -0
  143. package/src/services/usage.ts +57 -0
  144. package/src/types/express.d.ts +46 -0
  145. package/src/utils/errors.ts +44 -0
  146. package/src/utils/logger.ts +8 -0
  147. package/tsconfig.json +17 -0
  148. package/views/pages/404.ejs +14 -0
  149. package/views/pages/admin.ejs +307 -0
  150. package/views/pages/blog-post.ejs +378 -0
  151. package/views/pages/blog.ejs +148 -0
  152. package/views/pages/dashboard.ejs +441 -0
  153. package/views/pages/docs.ejs +807 -0
  154. package/views/pages/index.ejs +416 -0
  155. package/views/pages/login.ejs +170 -0
  156. package/views/pages/orders.ejs +111 -0
  157. package/views/pages/pricing.ejs +379 -0
  158. package/views/pages/register.ejs +397 -0
  159. package/views/pages/remote.ejs +334 -0
  160. package/views/pages/settings.ejs +373 -0
  161. package/views/partials/header.ejs +70 -0
  162. package/views/partials/nav.ejs +140 -0
@@ -0,0 +1,189 @@
1
+ import { Router } from 'express';
2
+ import type { Request, Response } from 'express';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { getPool } from '../services/database';
5
+ import { sessionAuth } from '../middleware/session-auth';
6
+ import { createQrPayment, queryAlipayOrder, verifyNotifySign } from '../services/alipay';
7
+ import { logger } from '../utils/logger';
8
+
9
+ export const paymentRouter = Router();
10
+
11
+ const PLAN_PRICES: Record<string, number> = {
12
+ starter: 9,
13
+ pro: 29,
14
+ unlimited: 99,
15
+ };
16
+
17
+ /**
18
+ * POST /api/payment/create-order
19
+ * Create an order and generate Alipay QR code.
20
+ */
21
+ paymentRouter.post('/create-order', sessionAuth, async (req: Request, res: Response) => {
22
+ try {
23
+ const { plan } = req.body;
24
+ const price = PLAN_PRICES[plan];
25
+ if (!price) {
26
+ res.status(400).json({ success: false, error: 'Invalid plan' });
27
+ return;
28
+ }
29
+
30
+ const pool = getPool();
31
+
32
+ // Get plan ID
33
+ const [plans] = await pool.execute('SELECT id, display_name FROM plans WHERE name = ?', [plan]);
34
+ const planRow = (plans as any[])[0];
35
+ if (!planRow) {
36
+ res.status(400).json({ success: false, error: 'Plan not found' });
37
+ return;
38
+ }
39
+
40
+ // Create order
41
+ const orderNo = `LLM${Date.now()}${Math.random().toString(36).slice(2, 6).toUpperCase()}`;
42
+ await pool.execute(
43
+ 'INSERT INTO orders (order_no, user_id, plan_id, amount) VALUES (?, ?, ?, ?)',
44
+ [orderNo, req.userId, planRow.id, price],
45
+ );
46
+
47
+ // Generate Alipay QR code
48
+ const qrUrl = await createQrPayment(orderNo, String(price), `LLM API ${planRow.display_name} Monthly`);
49
+ if (!qrUrl) {
50
+ res.status(500).json({ success: false, error: 'Failed to create payment' });
51
+ return;
52
+ }
53
+
54
+ // Convert QR URL to data URL for frontend display
55
+ const QRCode = require('qrcode');
56
+ const qrDataUrl = await QRCode.toDataURL(qrUrl, { width: 256 });
57
+
58
+ res.json({ success: true, orderNo, qrCode: qrDataUrl, amount: price });
59
+ } catch (err) {
60
+ logger.error({ err }, 'create order error');
61
+ res.status(500).json({ success: false, error: 'Internal error' });
62
+ }
63
+ });
64
+
65
+ /**
66
+ * POST /api/payment/notify
67
+ * Alipay async callback (no auth required).
68
+ */
69
+ paymentRouter.post('/notify', async (req: Request, res: Response) => {
70
+ try {
71
+ const params = req.body;
72
+
73
+ // Verify signature
74
+ if (!verifyNotifySign(params)) {
75
+ res.status(400).send('fail');
76
+ return;
77
+ }
78
+
79
+ const orderNo = params.out_trade_no;
80
+ const tradeNo = params.trade_no;
81
+ const tradeStatus = params.trade_status;
82
+
83
+ if (tradeStatus !== 'TRADE_SUCCESS' && tradeStatus !== 'TRADE_FINISHED') {
84
+ res.send('success');
85
+ return;
86
+ }
87
+
88
+ const pool = getPool();
89
+
90
+ // Get order
91
+ const [orders] = await pool.execute('SELECT * FROM orders WHERE order_no = ?', [orderNo]);
92
+ const order = (orders as any[])[0];
93
+ if (!order || order.status === 'paid') {
94
+ res.send('success');
95
+ return;
96
+ }
97
+
98
+ // Mark order as paid
99
+ await pool.execute(
100
+ 'UPDATE orders SET status = ?, trade_no = ?, paid_at = NOW() WHERE order_no = ?',
101
+ ['paid', tradeNo, orderNo],
102
+ );
103
+
104
+ // Upgrade user subscription
105
+ const [existingSub] = await pool.execute(
106
+ 'SELECT id FROM subscriptions WHERE user_id = ?',
107
+ [order.user_id],
108
+ );
109
+
110
+ if ((existingSub as any[]).length > 0) {
111
+ await pool.execute(
112
+ "UPDATE subscriptions SET plan_id = ?, tokens_used = 0, period_start = NOW(), period_end = NOW() + INTERVAL '30 days' WHERE user_id = ?",
113
+ [order.plan_id, order.user_id],
114
+ );
115
+ } else {
116
+ await pool.execute(
117
+ "INSERT INTO subscriptions (user_id, plan_id, period_end) VALUES (?, ?, NOW() + INTERVAL '30 days')",
118
+ [order.user_id, order.plan_id],
119
+ );
120
+ }
121
+
122
+ logger.info({ orderNo, userId: order.user_id, planId: order.plan_id }, 'Payment confirmed');
123
+ res.send('success');
124
+ } catch (err) {
125
+ logger.error({ err }, 'payment notify error');
126
+ res.status(500).send('fail');
127
+ }
128
+ });
129
+
130
+ /**
131
+ * GET /api/payment/check/:orderNo
132
+ * Poll for payment status.
133
+ */
134
+ paymentRouter.get('/check/:orderNo', sessionAuth, async (req: Request, res: Response) => {
135
+ try {
136
+ const pool = getPool();
137
+ const [orders] = await pool.execute(
138
+ 'SELECT status, trade_no FROM orders WHERE order_no = ? AND user_id = ?',
139
+ [req.params.orderNo, req.userId!],
140
+ );
141
+ const order = (orders as any[])[0];
142
+ if (!order) {
143
+ res.status(404).json({ success: false, error: 'Order not found' });
144
+ return;
145
+ }
146
+
147
+ if (order.status === 'paid') {
148
+ res.json({ success: true, status: 'paid' });
149
+ return;
150
+ }
151
+
152
+ // Check with Alipay
153
+ const result = await queryAlipayOrder(req.params.orderNo);
154
+ if (result?.status === 'paid') {
155
+ // Update locally
156
+ await pool.execute(
157
+ 'UPDATE orders SET status = ?, trade_no = ?, paid_at = NOW() WHERE order_no = ?',
158
+ ['paid', result.tradeNo || '', req.params.orderNo],
159
+ );
160
+ res.json({ success: true, status: 'paid' });
161
+ return;
162
+ }
163
+
164
+ res.json({ success: true, status: 'pending' });
165
+ } catch (err) {
166
+ logger.error({ err }, 'check order error');
167
+ res.status(500).json({ success: false, error: 'Internal error' });
168
+ }
169
+ });
170
+
171
+ /**
172
+ * GET /api/payment/orders
173
+ * User's order history.
174
+ */
175
+ paymentRouter.get('/orders', sessionAuth, async (req: Request, res: Response) => {
176
+ try {
177
+ const pool = getPool();
178
+ const [rows] = await pool.execute(`
179
+ SELECT o.*, p.display_name as plan_name
180
+ FROM orders o JOIN plans p ON o.plan_id = p.id
181
+ WHERE o.user_id = ? ORDER BY o.created_at DESC LIMIT 20
182
+ `, [req.userId!]);
183
+
184
+ res.json({ success: true, orders: rows });
185
+ } catch (err) {
186
+ logger.error({ err }, 'list orders error');
187
+ res.status(500).json({ success: false, error: 'Internal error' });
188
+ }
189
+ });
@@ -0,0 +1,40 @@
1
+ import { Router } from 'express';
2
+ import { getRecentPosts } from '../data/posts';
3
+
4
+ export const sitemapRouter = Router();
5
+
6
+ sitemapRouter.get('/sitemap.xml', (_req, res) => {
7
+ const baseUrl = process.env.SITE_URL || 'https://llmapi.pro';
8
+ const posts = getRecentPosts(100);
9
+
10
+ const staticPages = [
11
+ { url: '/', priority: '1.0', changefreq: 'weekly' },
12
+ { url: '/pricing', priority: '0.9', changefreq: 'weekly' },
13
+ { url: '/docs', priority: '0.9', changefreq: 'weekly' },
14
+ { url: '/blog', priority: '0.8', changefreq: 'daily' },
15
+ { url: '/login', priority: '0.3', changefreq: 'monthly' },
16
+ { url: '/register', priority: '0.5', changefreq: 'monthly' },
17
+ ];
18
+
19
+ const urls = staticPages.map(p => `
20
+ <url>
21
+ <loc>${baseUrl}${p.url}</loc>
22
+ <changefreq>${p.changefreq}</changefreq>
23
+ <priority>${p.priority}</priority>
24
+ </url>`).join('');
25
+
26
+ const postUrls = posts.map(p => `
27
+ <url>
28
+ <loc>${baseUrl}/blog/${p.slug}</loc>
29
+ <lastmod>${p.date}</lastmod>
30
+ <changefreq>monthly</changefreq>
31
+ <priority>0.7</priority>
32
+ </url>`).join('');
33
+
34
+ res.setHeader('Content-Type', 'application/xml');
35
+ res.send(`<?xml version="1.0" encoding="UTF-8"?>
36
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
37
+ ${urls}
38
+ ${postUrls}
39
+ </urlset>`);
40
+ });
@@ -0,0 +1,116 @@
1
+ import { logger } from '../utils/logger';
2
+
3
+ // Alipay configuration loaded from env
4
+ let alipayConfig = {
5
+ appId: '',
6
+ privateKey: '',
7
+ alipayPublicKey: '',
8
+ gateway: 'https://openapi.alipay.com/gateway.do',
9
+ notifyUrl: '',
10
+ };
11
+
12
+ export function setAlipayConfig(config: {
13
+ appId: string;
14
+ privateKey: string;
15
+ alipayPublicKey: string;
16
+ gateway?: string;
17
+ notifyUrl: string;
18
+ }): void {
19
+ alipayConfig = { ...alipayConfig, ...config };
20
+ }
21
+
22
+ /**
23
+ * Create QR code payment order via Alipay trade.precreate.
24
+ * Returns the QR code URL string.
25
+ *
26
+ * Note: Full Alipay SDK integration requires the 'alipay-sdk' package.
27
+ * This is a placeholder structure - the actual signing and API call
28
+ * should use the SDK for proper RSA2 signature handling.
29
+ */
30
+ export async function createQrPayment(
31
+ orderNo: string,
32
+ amount: string,
33
+ subject: string,
34
+ ): Promise<string | null> {
35
+ try {
36
+ // Dynamic import to avoid hard dependency when alipay is not configured
37
+ const AlipaySdk = require('alipay-sdk').default;
38
+ const sdk = new AlipaySdk({
39
+ appId: alipayConfig.appId,
40
+ privateKey: alipayConfig.privateKey,
41
+ alipayPublicKey: alipayConfig.alipayPublicKey,
42
+ gateway: alipayConfig.gateway,
43
+ signType: 'RSA2',
44
+ });
45
+
46
+ const result = await sdk.exec('alipay.trade.precreate', {
47
+ notify_url: alipayConfig.notifyUrl,
48
+ bizContent: {
49
+ out_trade_no: orderNo,
50
+ total_amount: amount,
51
+ subject,
52
+ timeout_express: '30m',
53
+ },
54
+ });
55
+
56
+ if (result.code === '10000' && result.qrCode) {
57
+ return result.qrCode;
58
+ }
59
+
60
+ logger.error({ result }, 'Alipay precreate failed');
61
+ return null;
62
+ } catch (err) {
63
+ logger.error({ err }, 'Alipay SDK error');
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Query order status from Alipay.
70
+ */
71
+ export async function queryAlipayOrder(orderNo: string): Promise<{ status: string; tradeNo?: string } | null> {
72
+ try {
73
+ const AlipaySdk = require('alipay-sdk').default;
74
+ const sdk = new AlipaySdk({
75
+ appId: alipayConfig.appId,
76
+ privateKey: alipayConfig.privateKey,
77
+ alipayPublicKey: alipayConfig.alipayPublicKey,
78
+ gateway: alipayConfig.gateway,
79
+ signType: 'RSA2',
80
+ });
81
+
82
+ const result = await sdk.exec('alipay.trade.query', {
83
+ bizContent: { out_trade_no: orderNo },
84
+ });
85
+
86
+ if (result.code === '10000') {
87
+ return {
88
+ status: result.tradeStatus === 'TRADE_SUCCESS' ? 'paid' : 'pending',
89
+ tradeNo: result.tradeNo,
90
+ };
91
+ }
92
+ return null;
93
+ } catch (err) {
94
+ logger.error({ err }, 'Alipay query error');
95
+ return null;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Verify Alipay notification signature.
101
+ */
102
+ export function verifyNotifySign(params: Record<string, string>): boolean {
103
+ try {
104
+ const AlipaySdk = require('alipay-sdk').default;
105
+ const sdk = new AlipaySdk({
106
+ appId: alipayConfig.appId,
107
+ privateKey: alipayConfig.privateKey,
108
+ alipayPublicKey: alipayConfig.alipayPublicKey,
109
+ signType: 'RSA2',
110
+ });
111
+ return sdk.checkNotifySign(params);
112
+ } catch (err) {
113
+ logger.error({ err }, 'Alipay verify error');
114
+ return false;
115
+ }
116
+ }
@@ -0,0 +1,187 @@
1
+ import { Pool } from 'pg';
2
+ import bcrypt from 'bcryptjs';
3
+ import { logger } from '../utils/logger';
4
+
5
+ let pool: Pool;
6
+
7
+ export function getPool() {
8
+ return {
9
+ execute: async (sql: string, params?: any[]) => {
10
+ // Convert ? placeholders to $1, $2, etc for pg
11
+ let idx = 0;
12
+ const pgSql = sql.replace(/\?/g, () => `$${++idx}`);
13
+ const result = await pool.query(pgSql, params);
14
+ return [result.rows, result.fields] as [any[], any];
15
+ },
16
+ };
17
+ }
18
+
19
+ export async function initDatabase(databaseUrl: string): Promise<void> {
20
+ pool = new Pool({
21
+ connectionString: databaseUrl,
22
+ max: 10,
23
+ });
24
+
25
+ // Test connection
26
+ const client = await pool.connect();
27
+ await client.query('SELECT 1');
28
+ client.release();
29
+ logger.info('PostgreSQL connected');
30
+
31
+ await createTables();
32
+ await createIndexes();
33
+ await seedPlans();
34
+ await seedAdmin();
35
+ }
36
+
37
+ async function createTables(): Promise<void> {
38
+ await pool.query(`
39
+ CREATE TABLE IF NOT EXISTS users (
40
+ id SERIAL PRIMARY KEY,
41
+ email VARCHAR(255) UNIQUE NOT NULL,
42
+ password_hash VARCHAR(255) NOT NULL,
43
+ name VARCHAR(100),
44
+ role VARCHAR(10) DEFAULT 'user',
45
+ status VARCHAR(20) DEFAULT 'active',
46
+ verified SMALLINT DEFAULT 0,
47
+ created_at TIMESTAMPTZ DEFAULT NOW(),
48
+ updated_at TIMESTAMPTZ DEFAULT NOW()
49
+ )
50
+ `);
51
+
52
+ await pool.query(`
53
+ CREATE TABLE IF NOT EXISTS api_keys (
54
+ id SERIAL PRIMARY KEY,
55
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
56
+ key_prefix VARCHAR(12),
57
+ key_hash VARCHAR(64),
58
+ name VARCHAR(100) DEFAULT 'Default',
59
+ status VARCHAR(10) DEFAULT 'active',
60
+ rate_limit INT DEFAULT 60,
61
+ last_used_at TIMESTAMPTZ,
62
+ created_at TIMESTAMPTZ DEFAULT NOW()
63
+ )
64
+ `);
65
+
66
+ await pool.query(`
67
+ CREATE TABLE IF NOT EXISTS plans (
68
+ id SERIAL PRIMARY KEY,
69
+ name VARCHAR(50) UNIQUE NOT NULL,
70
+ display_name VARCHAR(100) NOT NULL,
71
+ price_monthly DECIMAL(10,2) DEFAULT 0,
72
+ token_limit_monthly BIGINT DEFAULT 100000,
73
+ rate_limit_rpm INT DEFAULT 10,
74
+ max_api_keys INT DEFAULT 1,
75
+ max_input_tokens INT DEFAULT 131072,
76
+ created_at TIMESTAMPTZ DEFAULT NOW()
77
+ )
78
+ `);
79
+
80
+ await pool.query(`
81
+ CREATE TABLE IF NOT EXISTS subscriptions (
82
+ id SERIAL PRIMARY KEY,
83
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
84
+ plan_id INT REFERENCES plans(id),
85
+ tokens_used BIGINT DEFAULT 0,
86
+ period_start TIMESTAMPTZ DEFAULT NOW(),
87
+ period_end TIMESTAMPTZ,
88
+ created_at TIMESTAMPTZ DEFAULT NOW()
89
+ )
90
+ `);
91
+
92
+ await pool.query(`
93
+ CREATE TABLE IF NOT EXISTS usage_logs (
94
+ id BIGSERIAL PRIMARY KEY,
95
+ user_id INT NOT NULL,
96
+ api_key_id INT,
97
+ model VARCHAR(100),
98
+ provider_name VARCHAR(50),
99
+ input_tokens INT DEFAULT 0,
100
+ output_tokens INT DEFAULT 0,
101
+ thinking_tokens INT DEFAULT 0,
102
+ provider_cost DECIMAL(10,6) DEFAULT 0,
103
+ ttft_ms INT DEFAULT 0,
104
+ tokens_per_sec REAL DEFAULT 0,
105
+ duration_ms INT DEFAULT 0,
106
+ endpoint VARCHAR(100) DEFAULT '/v1/messages',
107
+ status VARCHAR(10) DEFAULT 'success',
108
+ created_at TIMESTAMPTZ DEFAULT NOW()
109
+ )
110
+ `);
111
+
112
+ await pool.query(`
113
+ CREATE TABLE IF NOT EXISTS orders (
114
+ id SERIAL PRIMARY KEY,
115
+ order_no VARCHAR(50) UNIQUE NOT NULL,
116
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
117
+ plan_id INT REFERENCES plans(id),
118
+ amount DECIMAL(10,2) NOT NULL,
119
+ status VARCHAR(20) DEFAULT 'pending',
120
+ payment_method VARCHAR(20) DEFAULT 'alipay',
121
+ trade_no VARCHAR(100),
122
+ paid_at TIMESTAMPTZ,
123
+ created_at TIMESTAMPTZ DEFAULT NOW()
124
+ )
125
+ `);
126
+
127
+ await pool.query(`
128
+ CREATE TABLE IF NOT EXISTS remote_sessions (
129
+ id SERIAL PRIMARY KEY,
130
+ session_id VARCHAR(64) UNIQUE NOT NULL,
131
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
132
+ status VARCHAR(20) DEFAULT 'active',
133
+ encrypted_key TEXT,
134
+ created_at TIMESTAMPTZ DEFAULT NOW(),
135
+ closed_at TIMESTAMPTZ
136
+ )
137
+ `);
138
+
139
+ logger.info('Database tables ensured');
140
+ }
141
+
142
+ async function createIndexes(): Promise<void> {
143
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_key_lookup ON api_keys (key_prefix, key_hash, status)`);
144
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_user_period ON subscriptions (user_id, period_start)`);
145
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_user_created ON usage_logs (user_id, created_at)`);
146
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_provider ON usage_logs (provider_name, created_at)`);
147
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_remote_session ON remote_sessions (session_id, status)`);
148
+ await pool.query(`CREATE INDEX IF NOT EXISTS idx_remote_user ON remote_sessions (user_id, status)`);
149
+
150
+ logger.info('Database indexes ensured');
151
+ }
152
+
153
+ async function seedPlans(): Promise<void> {
154
+ await pool.query(`
155
+ INSERT INTO plans (name, display_name, price_monthly, token_limit_monthly, rate_limit_rpm, max_api_keys, max_input_tokens) VALUES
156
+ ('free', 'Free', 0, 50000, 10, 1, 65536),
157
+ ('starter', 'Starter', 9, 2000000, 30, 3, 131072),
158
+ ('pro', 'Pro', 29, 10000000, 60, 5, 131072),
159
+ ('unlimited', 'Team', 99, 50000000, 120, 10, 131072)
160
+ ON CONFLICT (name) DO NOTHING
161
+ `);
162
+ logger.info('Default plans seeded');
163
+ }
164
+
165
+ async function seedAdmin(): Promise<void> {
166
+ const { rows } = await pool.query('SELECT id FROM users WHERE role = $1', ['admin']);
167
+ if (rows.length > 0) return;
168
+
169
+ const hash = await bcrypt.hash('admin123', 10);
170
+ const { rows: insertedRows } = await pool.query(
171
+ 'INSERT INTO users (email, password_hash, name, role, verified, status) VALUES ($1, $2, $3, $4, 1, $5) RETURNING id',
172
+ ['admin@llmapi.pro', hash, 'Admin', 'admin', 'active'],
173
+ );
174
+ const adminId = insertedRows[0].id;
175
+
176
+ // Give admin the unlimited plan
177
+ const { rows: planRows } = await pool.query('SELECT id FROM plans WHERE name = $1', ['unlimited']);
178
+ const planId = planRows[0]?.id;
179
+ if (planId) {
180
+ await pool.query(
181
+ `INSERT INTO subscriptions (user_id, plan_id, period_end) VALUES ($1, $2, NOW() + INTERVAL '100 years')`,
182
+ [adminId, planId],
183
+ );
184
+ }
185
+
186
+ logger.info('Default admin created (admin@llmapi.pro / admin123)');
187
+ }
@@ -0,0 +1,115 @@
1
+ import { getProviders } from '../providers/registry';
2
+ import type { LLMProvider } from '../providers/types';
3
+ import { logger } from '../utils/logger';
4
+
5
+ interface ProviderHealth {
6
+ name: string;
7
+ healthy: boolean;
8
+ lastCheckAt: number;
9
+ lastLatencyMs: number;
10
+ lastError: string | null;
11
+ consecutiveSuccesses: number;
12
+ consecutiveFailures: number;
13
+ }
14
+
15
+ const healthStatus = new Map<string, ProviderHealth>();
16
+ let intervalId: ReturnType<typeof setInterval> | null = null;
17
+
18
+ export function startHealthChecker(intervalMs = 60_000): void {
19
+ if (intervalId) return;
20
+ checkAll();
21
+ intervalId = setInterval(checkAll, intervalMs);
22
+ logger.info({ intervalMs }, 'Health checker started');
23
+ }
24
+
25
+ export function stopHealthChecker(): void {
26
+ if (intervalId) {
27
+ clearInterval(intervalId);
28
+ intervalId = null;
29
+ }
30
+ }
31
+
32
+ async function checkAll(): Promise<void> {
33
+ const providers = getProviders();
34
+ const checks = Array.from(providers.values()).map(checkProvider);
35
+ await Promise.allSettled(checks);
36
+ }
37
+
38
+ async function checkProvider(provider: LLMProvider): Promise<void> {
39
+ const start = Date.now();
40
+ let status = healthStatus.get(provider.name) || {
41
+ name: provider.name,
42
+ healthy: true,
43
+ lastCheckAt: 0,
44
+ lastLatencyMs: 0,
45
+ lastError: null,
46
+ consecutiveSuccesses: 0,
47
+ consecutiveFailures: 0,
48
+ };
49
+
50
+ try {
51
+ // Health check: send an invalid model name to test if the endpoint is reachable
52
+ // This returns 400 (bad request) but costs ZERO tokens — we only care if the
53
+ // endpoint responds, not if the request succeeds.
54
+ const testBody = JSON.stringify({
55
+ model: '_health_check_',
56
+ messages: [{ role: 'user', content: 'ping' }],
57
+ max_tokens: 1,
58
+ });
59
+
60
+ const res = await provider.proxy('/v1/messages', testBody, {
61
+ 'anthropic-version': '2023-06-01',
62
+ }, false);
63
+
64
+ // Read and discard response
65
+ await new Promise<void>((resolve) => {
66
+ res.on('data', () => {});
67
+ res.on('end', resolve);
68
+ res.on('error', resolve);
69
+ });
70
+
71
+ const latency = Date.now() - start;
72
+
73
+ // 400 = endpoint reachable but invalid model (expected, zero cost)
74
+ // 200 = somehow worked
75
+ // 500+ = server error = unhealthy
76
+ const isUp = res.statusCode !== undefined && res.statusCode < 500;
77
+
78
+ if (isUp) {
79
+ status.healthy = true;
80
+ status.lastCheckAt = Date.now();
81
+ status.lastLatencyMs = latency;
82
+ status.lastError = null;
83
+ status.consecutiveSuccesses++;
84
+ status.consecutiveFailures = 0;
85
+ provider.markHealthy();
86
+ } else {
87
+ throw new Error(`HTTP ${res.statusCode}`);
88
+ }
89
+
90
+ logger.debug({ provider: provider.name, latencyMs: latency, status: res.statusCode }, 'Health check passed');
91
+ } catch (err) {
92
+ const latency = Date.now() - start;
93
+ const message = err instanceof Error ? err.message : String(err);
94
+
95
+ status.lastCheckAt = Date.now();
96
+ status.lastLatencyMs = latency;
97
+ status.lastError = message;
98
+ status.consecutiveSuccesses = 0;
99
+ status.consecutiveFailures++;
100
+
101
+ if (status.consecutiveFailures >= 3) {
102
+ status.healthy = false;
103
+ provider.markUnhealthy(message);
104
+ }
105
+
106
+ logger.warn({ provider: provider.name, err: message, failures: status.consecutiveFailures },
107
+ 'Health check failed');
108
+ }
109
+
110
+ healthStatus.set(provider.name, status);
111
+ }
112
+
113
+ export function getHealthStatus(): ProviderHealth[] {
114
+ return Array.from(healthStatus.values());
115
+ }