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.
- package/.env.example +40 -0
- package/Dockerfile +17 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/converter/request.d.ts +6 -0
- package/dist/converter/request.js +184 -0
- package/dist/converter/request.js.map +1 -0
- package/dist/converter/response.d.ts +6 -0
- package/dist/converter/response.js +76 -0
- package/dist/converter/response.js.map +1 -0
- package/dist/converter/stream.d.ts +54 -0
- package/dist/converter/stream.js +318 -0
- package/dist/converter/stream.js.map +1 -0
- package/dist/converter/types.d.ts +239 -0
- package/dist/converter/types.js +6 -0
- package/dist/converter/types.js.map +1 -0
- package/dist/data/posts.d.ts +19 -0
- package/dist/data/posts.js +462 -0
- package/dist/data/posts.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +6 -0
- package/dist/middleware/api-key-auth.js +76 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/quota-guard.d.ts +10 -0
- package/dist/middleware/quota-guard.js +27 -0
- package/dist/middleware/quota-guard.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +50 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +6 -0
- package/dist/middleware/request-logger.js +37 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/session-auth.d.ts +19 -0
- package/dist/middleware/session-auth.js +99 -0
- package/dist/middleware/session-auth.js.map +1 -0
- package/dist/providers/aliyun.d.ts +13 -0
- package/dist/providers/aliyun.js +20 -0
- package/dist/providers/aliyun.js.map +1 -0
- package/dist/providers/base-provider.d.ts +36 -0
- package/dist/providers/base-provider.js +133 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/deepseek.d.ts +11 -0
- package/dist/providers/deepseek.js +18 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +98 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/admin.d.ts +1 -0
- package/dist/routes/admin.js +153 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +318 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blog.d.ts +1 -0
- package/dist/routes/blog.js +29 -0
- package/dist/routes/blog.js.map +1 -0
- package/dist/routes/dashboard.d.ts +1 -0
- package/dist/routes/dashboard.js +184 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +309 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/models.d.ts +1 -0
- package/dist/routes/models.js +39 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/payment.d.ts +1 -0
- package/dist/routes/payment.js +150 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/sitemap.d.ts +1 -0
- package/dist/routes/sitemap.js +38 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/services/alipay.d.ts +27 -0
- package/dist/services/alipay.js +106 -0
- package/dist/services/alipay.js.map +1 -0
- package/dist/services/database.d.ts +4 -0
- package/dist/services/database.js +170 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/health-checker.d.ts +13 -0
- package/dist/services/health-checker.js +95 -0
- package/dist/services/health-checker.js.map +1 -0
- package/dist/services/mailer.d.ts +3 -0
- package/dist/services/mailer.js +91 -0
- package/dist/services/mailer.js.map +1 -0
- package/dist/services/metrics.d.ts +56 -0
- package/dist/services/metrics.js +94 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/remote-control.d.ts +20 -0
- package/dist/services/remote-control.js +209 -0
- package/dist/services/remote-control.js.map +1 -0
- package/dist/services/remote-ws.d.ts +5 -0
- package/dist/services/remote-ws.js +143 -0
- package/dist/services/remote-ws.js.map +1 -0
- package/dist/services/usage.d.ts +13 -0
- package/dist/services/usage.js +39 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +39 -0
- package/public/robots.txt +8 -0
- package/src/config.ts +140 -0
- package/src/converter/request.ts +207 -0
- package/src/converter/response.ts +85 -0
- package/src/converter/stream.ts +373 -0
- package/src/converter/types.ts +257 -0
- package/src/data/posts.ts +474 -0
- package/src/index.ts +219 -0
- package/src/middleware/api-key-auth.ts +82 -0
- package/src/middleware/quota-guard.ts +28 -0
- package/src/middleware/rate-limiter.ts +61 -0
- package/src/middleware/request-logger.ts +36 -0
- package/src/middleware/session-auth.ts +91 -0
- package/src/providers/aliyun.ts +16 -0
- package/src/providers/base-provider.ts +148 -0
- package/src/providers/deepseek.ts +14 -0
- package/src/providers/registry.ts +111 -0
- package/src/providers/types.ts +26 -0
- package/src/routes/admin.ts +169 -0
- package/src/routes/auth.ts +369 -0
- package/src/routes/blog.ts +28 -0
- package/src/routes/dashboard.ts +208 -0
- package/src/routes/messages.ts +346 -0
- package/src/routes/models.ts +37 -0
- package/src/routes/payment.ts +189 -0
- package/src/routes/sitemap.ts +40 -0
- package/src/services/alipay.ts +116 -0
- package/src/services/database.ts +187 -0
- package/src/services/health-checker.ts +115 -0
- package/src/services/mailer.ts +90 -0
- package/src/services/metrics.ts +104 -0
- package/src/services/remote-control.ts +226 -0
- package/src/services/remote-ws.ts +145 -0
- package/src/services/usage.ts +57 -0
- package/src/types/express.d.ts +46 -0
- package/src/utils/errors.ts +44 -0
- package/src/utils/logger.ts +8 -0
- package/tsconfig.json +17 -0
- package/views/pages/404.ejs +14 -0
- package/views/pages/admin.ejs +307 -0
- package/views/pages/blog-post.ejs +378 -0
- package/views/pages/blog.ejs +148 -0
- package/views/pages/dashboard.ejs +441 -0
- package/views/pages/docs.ejs +807 -0
- package/views/pages/index.ejs +416 -0
- package/views/pages/login.ejs +170 -0
- package/views/pages/orders.ejs +111 -0
- package/views/pages/pricing.ejs +379 -0
- package/views/pages/register.ejs +397 -0
- package/views/pages/remote.ejs +334 -0
- package/views/pages/settings.ejs +373 -0
- package/views/partials/header.ejs +70 -0
- package/views/partials/nav.ejs +140 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setAlipayConfig = setAlipayConfig;
|
|
4
|
+
exports.createQrPayment = createQrPayment;
|
|
5
|
+
exports.queryAlipayOrder = queryAlipayOrder;
|
|
6
|
+
exports.verifyNotifySign = verifyNotifySign;
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
// Alipay configuration loaded from env
|
|
9
|
+
let alipayConfig = {
|
|
10
|
+
appId: '',
|
|
11
|
+
privateKey: '',
|
|
12
|
+
alipayPublicKey: '',
|
|
13
|
+
gateway: 'https://openapi.alipay.com/gateway.do',
|
|
14
|
+
notifyUrl: '',
|
|
15
|
+
};
|
|
16
|
+
function setAlipayConfig(config) {
|
|
17
|
+
alipayConfig = { ...alipayConfig, ...config };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create QR code payment order via Alipay trade.precreate.
|
|
21
|
+
* Returns the QR code URL string.
|
|
22
|
+
*
|
|
23
|
+
* Note: Full Alipay SDK integration requires the 'alipay-sdk' package.
|
|
24
|
+
* This is a placeholder structure - the actual signing and API call
|
|
25
|
+
* should use the SDK for proper RSA2 signature handling.
|
|
26
|
+
*/
|
|
27
|
+
async function createQrPayment(orderNo, amount, subject) {
|
|
28
|
+
try {
|
|
29
|
+
// Dynamic import to avoid hard dependency when alipay is not configured
|
|
30
|
+
const AlipaySdk = require('alipay-sdk').default;
|
|
31
|
+
const sdk = new AlipaySdk({
|
|
32
|
+
appId: alipayConfig.appId,
|
|
33
|
+
privateKey: alipayConfig.privateKey,
|
|
34
|
+
alipayPublicKey: alipayConfig.alipayPublicKey,
|
|
35
|
+
gateway: alipayConfig.gateway,
|
|
36
|
+
signType: 'RSA2',
|
|
37
|
+
});
|
|
38
|
+
const result = await sdk.exec('alipay.trade.precreate', {
|
|
39
|
+
notify_url: alipayConfig.notifyUrl,
|
|
40
|
+
bizContent: {
|
|
41
|
+
out_trade_no: orderNo,
|
|
42
|
+
total_amount: amount,
|
|
43
|
+
subject,
|
|
44
|
+
timeout_express: '30m',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (result.code === '10000' && result.qrCode) {
|
|
48
|
+
return result.qrCode;
|
|
49
|
+
}
|
|
50
|
+
logger_1.logger.error({ result }, 'Alipay precreate failed');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
logger_1.logger.error({ err }, 'Alipay SDK error');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Query order status from Alipay.
|
|
60
|
+
*/
|
|
61
|
+
async function queryAlipayOrder(orderNo) {
|
|
62
|
+
try {
|
|
63
|
+
const AlipaySdk = require('alipay-sdk').default;
|
|
64
|
+
const sdk = new AlipaySdk({
|
|
65
|
+
appId: alipayConfig.appId,
|
|
66
|
+
privateKey: alipayConfig.privateKey,
|
|
67
|
+
alipayPublicKey: alipayConfig.alipayPublicKey,
|
|
68
|
+
gateway: alipayConfig.gateway,
|
|
69
|
+
signType: 'RSA2',
|
|
70
|
+
});
|
|
71
|
+
const result = await sdk.exec('alipay.trade.query', {
|
|
72
|
+
bizContent: { out_trade_no: orderNo },
|
|
73
|
+
});
|
|
74
|
+
if (result.code === '10000') {
|
|
75
|
+
return {
|
|
76
|
+
status: result.tradeStatus === 'TRADE_SUCCESS' ? 'paid' : 'pending',
|
|
77
|
+
tradeNo: result.tradeNo,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
logger_1.logger.error({ err }, 'Alipay query error');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify Alipay notification signature.
|
|
89
|
+
*/
|
|
90
|
+
function verifyNotifySign(params) {
|
|
91
|
+
try {
|
|
92
|
+
const AlipaySdk = require('alipay-sdk').default;
|
|
93
|
+
const sdk = new AlipaySdk({
|
|
94
|
+
appId: alipayConfig.appId,
|
|
95
|
+
privateKey: alipayConfig.privateKey,
|
|
96
|
+
alipayPublicKey: alipayConfig.alipayPublicKey,
|
|
97
|
+
signType: 'RSA2',
|
|
98
|
+
});
|
|
99
|
+
return sdk.checkNotifySign(params);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
logger_1.logger.error({ err }, 'Alipay verify error');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=alipay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alipay.js","sourceRoot":"","sources":["../../src/services/alipay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPool = getPool;
|
|
7
|
+
exports.initDatabase = initDatabase;
|
|
8
|
+
const pg_1 = require("pg");
|
|
9
|
+
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
let pool;
|
|
12
|
+
function getPool() {
|
|
13
|
+
return {
|
|
14
|
+
execute: async (sql, params) => {
|
|
15
|
+
// Convert ? placeholders to $1, $2, etc for pg
|
|
16
|
+
let idx = 0;
|
|
17
|
+
const pgSql = sql.replace(/\?/g, () => `$${++idx}`);
|
|
18
|
+
const result = await pool.query(pgSql, params);
|
|
19
|
+
return [result.rows, result.fields];
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function initDatabase(databaseUrl) {
|
|
24
|
+
pool = new pg_1.Pool({
|
|
25
|
+
connectionString: databaseUrl,
|
|
26
|
+
max: 10,
|
|
27
|
+
});
|
|
28
|
+
// Test connection
|
|
29
|
+
const client = await pool.connect();
|
|
30
|
+
await client.query('SELECT 1');
|
|
31
|
+
client.release();
|
|
32
|
+
logger_1.logger.info('PostgreSQL connected');
|
|
33
|
+
await createTables();
|
|
34
|
+
await createIndexes();
|
|
35
|
+
await seedPlans();
|
|
36
|
+
await seedAdmin();
|
|
37
|
+
}
|
|
38
|
+
async function createTables() {
|
|
39
|
+
await pool.query(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
41
|
+
id SERIAL PRIMARY KEY,
|
|
42
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
43
|
+
password_hash VARCHAR(255) NOT NULL,
|
|
44
|
+
name VARCHAR(100),
|
|
45
|
+
role VARCHAR(10) DEFAULT 'user',
|
|
46
|
+
status VARCHAR(20) DEFAULT 'active',
|
|
47
|
+
verified SMALLINT DEFAULT 0,
|
|
48
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
49
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
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
|
+
await pool.query(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
67
|
+
id SERIAL PRIMARY KEY,
|
|
68
|
+
name VARCHAR(50) UNIQUE NOT NULL,
|
|
69
|
+
display_name VARCHAR(100) NOT NULL,
|
|
70
|
+
price_monthly DECIMAL(10,2) DEFAULT 0,
|
|
71
|
+
token_limit_monthly BIGINT DEFAULT 100000,
|
|
72
|
+
rate_limit_rpm INT DEFAULT 10,
|
|
73
|
+
max_api_keys INT DEFAULT 1,
|
|
74
|
+
max_input_tokens INT DEFAULT 131072,
|
|
75
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
76
|
+
)
|
|
77
|
+
`);
|
|
78
|
+
await pool.query(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
80
|
+
id SERIAL PRIMARY KEY,
|
|
81
|
+
user_id INT REFERENCES users(id) ON DELETE CASCADE,
|
|
82
|
+
plan_id INT REFERENCES plans(id),
|
|
83
|
+
tokens_used BIGINT DEFAULT 0,
|
|
84
|
+
period_start TIMESTAMPTZ DEFAULT NOW(),
|
|
85
|
+
period_end TIMESTAMPTZ,
|
|
86
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
await pool.query(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS usage_logs (
|
|
91
|
+
id BIGSERIAL PRIMARY KEY,
|
|
92
|
+
user_id INT NOT NULL,
|
|
93
|
+
api_key_id INT,
|
|
94
|
+
model VARCHAR(100),
|
|
95
|
+
provider_name VARCHAR(50),
|
|
96
|
+
input_tokens INT DEFAULT 0,
|
|
97
|
+
output_tokens INT DEFAULT 0,
|
|
98
|
+
thinking_tokens INT DEFAULT 0,
|
|
99
|
+
provider_cost DECIMAL(10,6) DEFAULT 0,
|
|
100
|
+
ttft_ms INT DEFAULT 0,
|
|
101
|
+
tokens_per_sec REAL DEFAULT 0,
|
|
102
|
+
duration_ms INT DEFAULT 0,
|
|
103
|
+
endpoint VARCHAR(100) DEFAULT '/v1/messages',
|
|
104
|
+
status VARCHAR(10) DEFAULT 'success',
|
|
105
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
await pool.query(`
|
|
109
|
+
CREATE TABLE IF NOT EXISTS orders (
|
|
110
|
+
id SERIAL PRIMARY KEY,
|
|
111
|
+
order_no VARCHAR(50) UNIQUE NOT NULL,
|
|
112
|
+
user_id INT REFERENCES users(id) ON DELETE CASCADE,
|
|
113
|
+
plan_id INT REFERENCES plans(id),
|
|
114
|
+
amount DECIMAL(10,2) NOT NULL,
|
|
115
|
+
status VARCHAR(20) DEFAULT 'pending',
|
|
116
|
+
payment_method VARCHAR(20) DEFAULT 'alipay',
|
|
117
|
+
trade_no VARCHAR(100),
|
|
118
|
+
paid_at TIMESTAMPTZ,
|
|
119
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
await pool.query(`
|
|
123
|
+
CREATE TABLE IF NOT EXISTS remote_sessions (
|
|
124
|
+
id SERIAL PRIMARY KEY,
|
|
125
|
+
session_id VARCHAR(64) UNIQUE NOT NULL,
|
|
126
|
+
user_id INT REFERENCES users(id) ON DELETE CASCADE,
|
|
127
|
+
status VARCHAR(20) DEFAULT 'active',
|
|
128
|
+
encrypted_key TEXT,
|
|
129
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
130
|
+
closed_at TIMESTAMPTZ
|
|
131
|
+
)
|
|
132
|
+
`);
|
|
133
|
+
logger_1.logger.info('Database tables ensured');
|
|
134
|
+
}
|
|
135
|
+
async function createIndexes() {
|
|
136
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_key_lookup ON api_keys (key_prefix, key_hash, status)`);
|
|
137
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_user_period ON subscriptions (user_id, period_start)`);
|
|
138
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_user_created ON usage_logs (user_id, created_at)`);
|
|
139
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_provider ON usage_logs (provider_name, created_at)`);
|
|
140
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_remote_session ON remote_sessions (session_id, status)`);
|
|
141
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS idx_remote_user ON remote_sessions (user_id, status)`);
|
|
142
|
+
logger_1.logger.info('Database indexes ensured');
|
|
143
|
+
}
|
|
144
|
+
async function seedPlans() {
|
|
145
|
+
await pool.query(`
|
|
146
|
+
INSERT INTO plans (name, display_name, price_monthly, token_limit_monthly, rate_limit_rpm, max_api_keys, max_input_tokens) VALUES
|
|
147
|
+
('free', 'Free', 0, 50000, 10, 1, 65536),
|
|
148
|
+
('starter', 'Starter', 9, 2000000, 30, 3, 131072),
|
|
149
|
+
('pro', 'Pro', 29, 10000000, 60, 5, 131072),
|
|
150
|
+
('unlimited', 'Team', 99, 50000000, 120, 10, 131072)
|
|
151
|
+
ON CONFLICT (name) DO NOTHING
|
|
152
|
+
`);
|
|
153
|
+
logger_1.logger.info('Default plans seeded');
|
|
154
|
+
}
|
|
155
|
+
async function seedAdmin() {
|
|
156
|
+
const { rows } = await pool.query('SELECT id FROM users WHERE role = $1', ['admin']);
|
|
157
|
+
if (rows.length > 0)
|
|
158
|
+
return;
|
|
159
|
+
const hash = await bcryptjs_1.default.hash('admin123', 10);
|
|
160
|
+
const { rows: insertedRows } = await pool.query('INSERT INTO users (email, password_hash, name, role, verified, status) VALUES ($1, $2, $3, $4, 1, $5) RETURNING id', ['admin@llmapi.pro', hash, 'Admin', 'admin', 'active']);
|
|
161
|
+
const adminId = insertedRows[0].id;
|
|
162
|
+
// Give admin the unlimited plan
|
|
163
|
+
const { rows: planRows } = await pool.query('SELECT id FROM plans WHERE name = $1', ['unlimited']);
|
|
164
|
+
const planId = planRows[0]?.id;
|
|
165
|
+
if (planId) {
|
|
166
|
+
await pool.query(`INSERT INTO subscriptions (user_id, plan_id, period_end) VALUES ($1, $2, NOW() + INTERVAL '100 years')`, [adminId, planId]);
|
|
167
|
+
}
|
|
168
|
+
logger_1.logger.info('Default admin created (admin@llmapi.pro / admin123)');
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/services/database.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface ProviderHealth {
|
|
2
|
+
name: string;
|
|
3
|
+
healthy: boolean;
|
|
4
|
+
lastCheckAt: number;
|
|
5
|
+
lastLatencyMs: number;
|
|
6
|
+
lastError: string | null;
|
|
7
|
+
consecutiveSuccesses: number;
|
|
8
|
+
consecutiveFailures: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function startHealthChecker(intervalMs?: number): void;
|
|
11
|
+
export declare function stopHealthChecker(): void;
|
|
12
|
+
export declare function getHealthStatus(): ProviderHealth[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startHealthChecker = startHealthChecker;
|
|
4
|
+
exports.stopHealthChecker = stopHealthChecker;
|
|
5
|
+
exports.getHealthStatus = getHealthStatus;
|
|
6
|
+
const registry_1 = require("../providers/registry");
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
const healthStatus = new Map();
|
|
9
|
+
let intervalId = null;
|
|
10
|
+
function startHealthChecker(intervalMs = 60_000) {
|
|
11
|
+
if (intervalId)
|
|
12
|
+
return;
|
|
13
|
+
checkAll();
|
|
14
|
+
intervalId = setInterval(checkAll, intervalMs);
|
|
15
|
+
logger_1.logger.info({ intervalMs }, 'Health checker started');
|
|
16
|
+
}
|
|
17
|
+
function stopHealthChecker() {
|
|
18
|
+
if (intervalId) {
|
|
19
|
+
clearInterval(intervalId);
|
|
20
|
+
intervalId = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function checkAll() {
|
|
24
|
+
const providers = (0, registry_1.getProviders)();
|
|
25
|
+
const checks = Array.from(providers.values()).map(checkProvider);
|
|
26
|
+
await Promise.allSettled(checks);
|
|
27
|
+
}
|
|
28
|
+
async function checkProvider(provider) {
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
let status = healthStatus.get(provider.name) || {
|
|
31
|
+
name: provider.name,
|
|
32
|
+
healthy: true,
|
|
33
|
+
lastCheckAt: 0,
|
|
34
|
+
lastLatencyMs: 0,
|
|
35
|
+
lastError: null,
|
|
36
|
+
consecutiveSuccesses: 0,
|
|
37
|
+
consecutiveFailures: 0,
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
// Health check: send an invalid model name to test if the endpoint is reachable
|
|
41
|
+
// This returns 400 (bad request) but costs ZERO tokens — we only care if the
|
|
42
|
+
// endpoint responds, not if the request succeeds.
|
|
43
|
+
const testBody = JSON.stringify({
|
|
44
|
+
model: '_health_check_',
|
|
45
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
46
|
+
max_tokens: 1,
|
|
47
|
+
});
|
|
48
|
+
const res = await provider.proxy('/v1/messages', testBody, {
|
|
49
|
+
'anthropic-version': '2023-06-01',
|
|
50
|
+
}, false);
|
|
51
|
+
// Read and discard response
|
|
52
|
+
await new Promise((resolve) => {
|
|
53
|
+
res.on('data', () => { });
|
|
54
|
+
res.on('end', resolve);
|
|
55
|
+
res.on('error', resolve);
|
|
56
|
+
});
|
|
57
|
+
const latency = Date.now() - start;
|
|
58
|
+
// 400 = endpoint reachable but invalid model (expected, zero cost)
|
|
59
|
+
// 200 = somehow worked
|
|
60
|
+
// 500+ = server error = unhealthy
|
|
61
|
+
const isUp = res.statusCode !== undefined && res.statusCode < 500;
|
|
62
|
+
if (isUp) {
|
|
63
|
+
status.healthy = true;
|
|
64
|
+
status.lastCheckAt = Date.now();
|
|
65
|
+
status.lastLatencyMs = latency;
|
|
66
|
+
status.lastError = null;
|
|
67
|
+
status.consecutiveSuccesses++;
|
|
68
|
+
status.consecutiveFailures = 0;
|
|
69
|
+
provider.markHealthy();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
throw new Error(`HTTP ${res.statusCode}`);
|
|
73
|
+
}
|
|
74
|
+
logger_1.logger.debug({ provider: provider.name, latencyMs: latency, status: res.statusCode }, 'Health check passed');
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const latency = Date.now() - start;
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
status.lastCheckAt = Date.now();
|
|
80
|
+
status.lastLatencyMs = latency;
|
|
81
|
+
status.lastError = message;
|
|
82
|
+
status.consecutiveSuccesses = 0;
|
|
83
|
+
status.consecutiveFailures++;
|
|
84
|
+
if (status.consecutiveFailures >= 3) {
|
|
85
|
+
status.healthy = false;
|
|
86
|
+
provider.markUnhealthy(message);
|
|
87
|
+
}
|
|
88
|
+
logger_1.logger.warn({ provider: provider.name, err: message, failures: status.consecutiveFailures }, 'Health check failed');
|
|
89
|
+
}
|
|
90
|
+
healthStatus.set(provider.name, status);
|
|
91
|
+
}
|
|
92
|
+
function getHealthStatus() {
|
|
93
|
+
return Array.from(healthStatus.values());
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=health-checker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-checker.js","sourceRoot":"","sources":["../../src/services/health-checker.ts"],"names":[],"mappings":";;AAiBA,gDAKC;AAED,8CAKC;AAmFD,0CAEC;AAlHD,oDAAqD;AAErD,4CAAyC;AAYzC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA0B,CAAC;AACvD,IAAI,UAAU,GAA0C,IAAI,CAAC;AAE7D,SAAgB,kBAAkB,CAAC,UAAU,GAAG,MAAM;IACpD,IAAI,UAAU;QAAE,OAAO;IACvB,QAAQ,EAAE,CAAC;IACX,UAAU,GAAG,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC/C,eAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,EAAE,wBAAwB,CAAC,CAAC;AACxD,CAAC;AAED,SAAgB,iBAAiB;IAC/B,IAAI,UAAU,EAAE,CAAC;QACf,aAAa,CAAC,UAAU,CAAC,CAAC;QAC1B,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ;IACrB,MAAM,SAAS,GAAG,IAAA,uBAAY,GAAE,CAAC;IACjC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACjE,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,QAAqB;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI;QAC9C,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,OAAO,EAAE,IAAI;QACb,WAAW,EAAE,CAAC;QACd,aAAa,EAAE,CAAC;QAChB,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,CAAC;QACvB,mBAAmB,EAAE,CAAC;KACvB,CAAC;IAEF,IAAI,CAAC;QACH,gFAAgF;QAChF,6EAA6E;QAC7E,kDAAkD;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;YAC9B,KAAK,EAAE,gBAAgB;YACvB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;YAC7C,UAAU,EAAE,CAAC;SACd,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,cAAc,EAAE,QAAQ,EAAE;YACzD,mBAAmB,EAAE,YAAY;SAClC,EAAE,KAAK,CAAC,CAAC;QAEV,4BAA4B;QAC5B,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YACzB,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YACvB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QAEnC,mEAAmE;QACnE,uBAAuB;QACvB,kCAAkC;QAClC,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,KAAK,SAAS,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;QAElE,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;YACtB,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAChC,MAAM,CAAC,aAAa,GAAG,OAAO,CAAC;YAC/B,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YACxB,MAAM,CAAC,oBAAoB,EAAE,CAAC;YAC9B,MAAM,CAAC,mBAAmB,GAAG,CAAC,CAAC;YAC/B,QAAQ,CAAC,WAAW,EAAE,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5C,CAAC;QAED,eAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAC/G,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACnC,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAEjE,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAChC,MAAM,CAAC,aAAa,GAAG,OAAO,CAAC;QAC/B,MAAM,CAAC,SAAS,GAAG,OAAO,CAAC;QAC3B,MAAM,CAAC,oBAAoB,GAAG,CAAC,CAAC;QAChC,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAE7B,IAAI,MAAM,CAAC,mBAAmB,IAAI,CAAC,EAAE,CAAC;YACpC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC;YACvB,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;QAED,eAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,mBAAmB,EAAE,EACzF,qBAAqB,CAAC,CAAC;IAC3B,CAAC;IAED,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED,SAAgB,eAAe;IAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setMailerApiKey = setMailerApiKey;
|
|
7
|
+
exports.sendVerificationEmail = sendVerificationEmail;
|
|
8
|
+
exports.sendWelcomeEmail = sendWelcomeEmail;
|
|
9
|
+
const https_1 = __importDefault(require("https"));
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
let resendApiKey = '';
|
|
12
|
+
function setMailerApiKey(key) {
|
|
13
|
+
resendApiKey = key;
|
|
14
|
+
}
|
|
15
|
+
async function sendEmail(to, subject, html) {
|
|
16
|
+
if (!resendApiKey) {
|
|
17
|
+
logger_1.logger.warn('Resend API key not configured, skipping email');
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const payload = JSON.stringify({
|
|
22
|
+
from: 'LLM API <noreply@llmapi.pro>',
|
|
23
|
+
to,
|
|
24
|
+
subject,
|
|
25
|
+
html,
|
|
26
|
+
});
|
|
27
|
+
const req = https_1.default.request({
|
|
28
|
+
hostname: 'api.resend.com',
|
|
29
|
+
path: '/emails',
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
'Authorization': `Bearer ${resendApiKey}`,
|
|
34
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
35
|
+
},
|
|
36
|
+
}, (res) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
res.on('data', (c) => chunks.push(c));
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
if (res.statusCode && res.statusCode < 300) {
|
|
41
|
+
resolve(true);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger_1.logger.error({ status: res.statusCode, body: Buffer.concat(chunks).toString() }, 'Email send failed');
|
|
45
|
+
resolve(false);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on('error', (err) => { logger_1.logger.error({ err }, 'Email request error'); resolve(false); });
|
|
50
|
+
req.write(payload);
|
|
51
|
+
req.end();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const BRAND_STYLE = `
|
|
55
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
56
|
+
max-width: 480px; margin: 0 auto; padding: 32px;
|
|
57
|
+
background: #FAF6F1; border-radius: 12px;
|
|
58
|
+
`;
|
|
59
|
+
async function sendVerificationEmail(email, code, name) {
|
|
60
|
+
const html = `
|
|
61
|
+
<div style="${BRAND_STYLE}">
|
|
62
|
+
<h2 style="color:#1A1915;">Hi ${name},</h2>
|
|
63
|
+
<p>Your verification code is:</p>
|
|
64
|
+
<div style="font-size:32px;letter-spacing:8px;font-weight:bold;color:#D97757;text-align:center;padding:16px;background:#fff;border-radius:8px;margin:16px 0;">
|
|
65
|
+
${code}
|
|
66
|
+
</div>
|
|
67
|
+
<p style="color:#666;font-size:14px;">This code expires in 30 minutes.</p>
|
|
68
|
+
<hr style="border:none;border-top:1px solid #e0d8d0;margin:24px 0;">
|
|
69
|
+
<p style="color:#999;font-size:12px;">LLM API - Claude Code API Relay</p>
|
|
70
|
+
</div>
|
|
71
|
+
`;
|
|
72
|
+
return sendEmail(email, `Your verification code: ${code}`, html);
|
|
73
|
+
}
|
|
74
|
+
async function sendWelcomeEmail(email, name) {
|
|
75
|
+
const html = `
|
|
76
|
+
<div style="${BRAND_STYLE}">
|
|
77
|
+
<h2 style="color:#1A1915;">Welcome, ${name}!</h2>
|
|
78
|
+
<p>Your LLM API account is ready. Here's how to get started:</p>
|
|
79
|
+
<ol>
|
|
80
|
+
<li>Go to your <a href="https://llmapi.pro/dashboard" style="color:#D97757;">Dashboard</a></li>
|
|
81
|
+
<li>Create an API key</li>
|
|
82
|
+
<li>Configure Claude Code with your key</li>
|
|
83
|
+
</ol>
|
|
84
|
+
<p>Need help? Check our <a href="https://llmapi.pro/docs" style="color:#D97757;">documentation</a>.</p>
|
|
85
|
+
<hr style="border:none;border-top:1px solid #e0d8d0;margin:24px 0;">
|
|
86
|
+
<p style="color:#999;font-size:12px;">LLM API - Claude Code API Relay</p>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
return sendEmail(email, 'Welcome to LLM API!', html);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=mailer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mailer.js","sourceRoot":"","sources":["../../src/services/mailer.ts"],"names":[],"mappings":";;;;;AAKA,0CAEC;AAkDD,sDAcC;AAED,4CAgBC;AAzFD,kDAA0B;AAC1B,4CAAyC;AAEzC,IAAI,YAAY,GAAG,EAAE,CAAC;AAEtB,SAAgB,eAAe,CAAC,GAAW;IACzC,YAAY,GAAG,GAAG,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,EAAU,EAAE,OAAe,EAAE,IAAY;IAChE,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,eAAM,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;QAC7D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;YAC7B,IAAI,EAAE,8BAA8B;YACpC,EAAE;YACF,OAAO;YACP,IAAI;SACL,CAAC,CAAC;QAEH,MAAM,GAAG,GAAG,eAAK,CAAC,OAAO,CAAC;YACxB,QAAQ,EAAE,gBAAgB;YAC1B,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,eAAe,EAAE,UAAU,YAAY,EAAE;gBACzC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC;aAC7C;SACF,EAAE,CAAC,GAAG,EAAE,EAAE;YACT,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACtC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACjB,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC;oBAC3C,OAAO,CAAC,IAAI,CAAC,CAAC;gBAChB,CAAC;qBAAM,CAAC;oBACN,eAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,mBAAmB,CAAC,CAAC;oBACtG,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,GAAG,eAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5F,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,WAAW,GAAG;;;;CAInB,CAAC;AAEK,KAAK,UAAU,qBAAqB,CAAC,KAAa,EAAE,IAAY,EAAE,IAAY;IACnF,MAAM,IAAI,GAAG;kBACG,WAAW;sCACS,IAAI;;;UAGhC,IAAI;;;;;;GAMX,CAAC;IACF,OAAO,SAAS,CAAC,KAAK,EAAE,2BAA2B,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC;AACnE,CAAC;AAEM,KAAK,UAAU,gBAAgB,CAAC,KAAa,EAAE,IAAY;IAChE,MAAM,IAAI,GAAG;kBACG,WAAW;4CACe,IAAI;;;;;;;;;;;GAW7C,CAAC;IACF,OAAO,SAAS,CAAC,KAAK,EAAE,qBAAqB,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory metrics collector.
|
|
3
|
+
* Tracks request counts, latency, and provider stats.
|
|
4
|
+
*/
|
|
5
|
+
interface Counter {
|
|
6
|
+
total: number;
|
|
7
|
+
success: number;
|
|
8
|
+
error: number;
|
|
9
|
+
}
|
|
10
|
+
declare class Metrics {
|
|
11
|
+
private requests;
|
|
12
|
+
private streamRequests;
|
|
13
|
+
private providerRequests;
|
|
14
|
+
private latency;
|
|
15
|
+
private ttft;
|
|
16
|
+
private activeStreams;
|
|
17
|
+
private startTime;
|
|
18
|
+
recordRequest(success: boolean, stream: boolean): void;
|
|
19
|
+
recordProviderRequest(provider: string, success: boolean): void;
|
|
20
|
+
recordLatency(ms: number): void;
|
|
21
|
+
recordTTFT(ms: number): void;
|
|
22
|
+
streamStarted(): void;
|
|
23
|
+
streamEnded(): void;
|
|
24
|
+
getSnapshot(): {
|
|
25
|
+
uptime_ms: number;
|
|
26
|
+
uptime_human: string;
|
|
27
|
+
requests: {
|
|
28
|
+
total: number;
|
|
29
|
+
success: number;
|
|
30
|
+
error: number;
|
|
31
|
+
};
|
|
32
|
+
stream_requests: {
|
|
33
|
+
total: number;
|
|
34
|
+
success: number;
|
|
35
|
+
error: number;
|
|
36
|
+
};
|
|
37
|
+
active_streams: number;
|
|
38
|
+
latency: {
|
|
39
|
+
avg_ms: number;
|
|
40
|
+
min_ms: number;
|
|
41
|
+
max_ms: number;
|
|
42
|
+
count: number;
|
|
43
|
+
} | null;
|
|
44
|
+
ttft: {
|
|
45
|
+
avg_ms: number;
|
|
46
|
+
min_ms: number;
|
|
47
|
+
max_ms: number;
|
|
48
|
+
count: number;
|
|
49
|
+
} | null;
|
|
50
|
+
providers: {
|
|
51
|
+
[k: string]: Counter;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export declare const metrics: Metrics;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple in-memory metrics collector.
|
|
4
|
+
* Tracks request counts, latency, and provider stats.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.metrics = void 0;
|
|
8
|
+
class Metrics {
|
|
9
|
+
requests = { total: 0, success: 0, error: 0 };
|
|
10
|
+
streamRequests = { total: 0, success: 0, error: 0 };
|
|
11
|
+
providerRequests = new Map();
|
|
12
|
+
latency = { count: 0, sum: 0, min: Infinity, max: 0 };
|
|
13
|
+
ttft = { count: 0, sum: 0, min: Infinity, max: 0 };
|
|
14
|
+
activeStreams = 0;
|
|
15
|
+
startTime = Date.now();
|
|
16
|
+
recordRequest(success, stream) {
|
|
17
|
+
this.requests.total++;
|
|
18
|
+
if (success)
|
|
19
|
+
this.requests.success++;
|
|
20
|
+
else
|
|
21
|
+
this.requests.error++;
|
|
22
|
+
if (stream) {
|
|
23
|
+
this.streamRequests.total++;
|
|
24
|
+
if (success)
|
|
25
|
+
this.streamRequests.success++;
|
|
26
|
+
else
|
|
27
|
+
this.streamRequests.error++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
recordProviderRequest(provider, success) {
|
|
31
|
+
let counter = this.providerRequests.get(provider);
|
|
32
|
+
if (!counter) {
|
|
33
|
+
counter = { total: 0, success: 0, error: 0 };
|
|
34
|
+
this.providerRequests.set(provider, counter);
|
|
35
|
+
}
|
|
36
|
+
counter.total++;
|
|
37
|
+
if (success)
|
|
38
|
+
counter.success++;
|
|
39
|
+
else
|
|
40
|
+
counter.error++;
|
|
41
|
+
}
|
|
42
|
+
recordLatency(ms) {
|
|
43
|
+
this.latency.count++;
|
|
44
|
+
this.latency.sum += ms;
|
|
45
|
+
this.latency.min = Math.min(this.latency.min, ms);
|
|
46
|
+
this.latency.max = Math.max(this.latency.max, ms);
|
|
47
|
+
}
|
|
48
|
+
recordTTFT(ms) {
|
|
49
|
+
this.ttft.count++;
|
|
50
|
+
this.ttft.sum += ms;
|
|
51
|
+
this.ttft.min = Math.min(this.ttft.min, ms);
|
|
52
|
+
this.ttft.max = Math.max(this.ttft.max, ms);
|
|
53
|
+
}
|
|
54
|
+
streamStarted() { this.activeStreams++; }
|
|
55
|
+
streamEnded() { this.activeStreams = Math.max(0, this.activeStreams - 1); }
|
|
56
|
+
getSnapshot() {
|
|
57
|
+
const uptime = Date.now() - this.startTime;
|
|
58
|
+
return {
|
|
59
|
+
uptime_ms: uptime,
|
|
60
|
+
uptime_human: formatDuration(uptime),
|
|
61
|
+
requests: { ...this.requests },
|
|
62
|
+
stream_requests: { ...this.streamRequests },
|
|
63
|
+
active_streams: this.activeStreams,
|
|
64
|
+
latency: this.latency.count > 0 ? {
|
|
65
|
+
avg_ms: Math.round(this.latency.sum / this.latency.count),
|
|
66
|
+
min_ms: this.latency.min === Infinity ? 0 : this.latency.min,
|
|
67
|
+
max_ms: this.latency.max,
|
|
68
|
+
count: this.latency.count,
|
|
69
|
+
} : null,
|
|
70
|
+
ttft: this.ttft.count > 0 ? {
|
|
71
|
+
avg_ms: Math.round(this.ttft.sum / this.ttft.count),
|
|
72
|
+
min_ms: this.ttft.min === Infinity ? 0 : this.ttft.min,
|
|
73
|
+
max_ms: this.ttft.max,
|
|
74
|
+
count: this.ttft.count,
|
|
75
|
+
} : null,
|
|
76
|
+
providers: Object.fromEntries(this.providerRequests),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function formatDuration(ms) {
|
|
81
|
+
const s = Math.floor(ms / 1000);
|
|
82
|
+
const m = Math.floor(s / 60);
|
|
83
|
+
const h = Math.floor(m / 60);
|
|
84
|
+
const d = Math.floor(h / 24);
|
|
85
|
+
if (d > 0)
|
|
86
|
+
return `${d}d ${h % 24}h`;
|
|
87
|
+
if (h > 0)
|
|
88
|
+
return `${h}h ${m % 60}m`;
|
|
89
|
+
if (m > 0)
|
|
90
|
+
return `${m}m ${s % 60}s`;
|
|
91
|
+
return `${s}s`;
|
|
92
|
+
}
|
|
93
|
+
exports.metrics = new Metrics();
|
|
94
|
+
//# sourceMappingURL=metrics.js.map
|