aweshare 0.2.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/LICENSE +21 -0
- package/README.md +246 -0
- package/apps/agent/dist/adapters.d.ts +15 -0
- package/apps/agent/dist/adapters.js +25 -0
- package/apps/agent/dist/adapters.js.map +1 -0
- package/apps/agent/dist/cli.d.ts +2 -0
- package/apps/agent/dist/cli.js +175 -0
- package/apps/agent/dist/cli.js.map +1 -0
- package/apps/agent/dist/config.d.ts +38 -0
- package/apps/agent/dist/config.js +150 -0
- package/apps/agent/dist/config.js.map +1 -0
- package/apps/agent/dist/connection.d.ts +40 -0
- package/apps/agent/dist/connection.js +359 -0
- package/apps/agent/dist/connection.js.map +1 -0
- package/apps/agent/dist/doctor.d.ts +14 -0
- package/apps/agent/dist/doctor.js +125 -0
- package/apps/agent/dist/doctor.js.map +1 -0
- package/apps/agent/dist/health.d.ts +15 -0
- package/apps/agent/dist/health.js +45 -0
- package/apps/agent/dist/health.js.map +1 -0
- package/apps/agent/dist/index.d.ts +5 -0
- package/apps/agent/dist/index.js +6 -0
- package/apps/agent/dist/index.js.map +1 -0
- package/apps/agent/package.json +20 -0
- package/apps/hub/dist/admin.d.ts +10 -0
- package/apps/hub/dist/admin.js +208 -0
- package/apps/hub/dist/admin.js.map +1 -0
- package/apps/hub/dist/auth.d.ts +17 -0
- package/apps/hub/dist/auth.js +28 -0
- package/apps/hub/dist/auth.js.map +1 -0
- package/apps/hub/dist/cli.d.ts +2 -0
- package/apps/hub/dist/cli.js +243 -0
- package/apps/hub/dist/cli.js.map +1 -0
- package/apps/hub/dist/config.d.ts +25 -0
- package/apps/hub/dist/config.js +53 -0
- package/apps/hub/dist/config.js.map +1 -0
- package/apps/hub/dist/consumer.d.ts +18 -0
- package/apps/hub/dist/consumer.js +157 -0
- package/apps/hub/dist/consumer.js.map +1 -0
- package/apps/hub/dist/db.d.ts +91 -0
- package/apps/hub/dist/db.js +236 -0
- package/apps/hub/dist/db.js.map +1 -0
- package/apps/hub/dist/http.d.ts +31 -0
- package/apps/hub/dist/http.js +118 -0
- package/apps/hub/dist/http.js.map +1 -0
- package/apps/hub/dist/limiter.d.ts +35 -0
- package/apps/hub/dist/limiter.js +78 -0
- package/apps/hub/dist/limiter.js.map +1 -0
- package/apps/hub/dist/limits.d.ts +30 -0
- package/apps/hub/dist/limits.js +52 -0
- package/apps/hub/dist/limits.js.map +1 -0
- package/apps/hub/dist/meter.d.ts +25 -0
- package/apps/hub/dist/meter.js +135 -0
- package/apps/hub/dist/meter.js.map +1 -0
- package/apps/hub/dist/sanitize.d.ts +3 -0
- package/apps/hub/dist/sanitize.js +56 -0
- package/apps/hub/dist/sanitize.js.map +1 -0
- package/apps/hub/dist/server.d.ts +25 -0
- package/apps/hub/dist/server.js +77 -0
- package/apps/hub/dist/server.js.map +1 -0
- package/apps/hub/dist/tokens.d.ts +9 -0
- package/apps/hub/dist/tokens.js +35 -0
- package/apps/hub/dist/tokens.js.map +1 -0
- package/apps/hub/dist/tunnel.d.ts +108 -0
- package/apps/hub/dist/tunnel.js +424 -0
- package/apps/hub/dist/tunnel.js.map +1 -0
- package/apps/hub/package.json +21 -0
- package/bin/aweshare.mjs +43 -0
- package/package.json +47 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
const SCHEMA_V2 = `
|
|
5
|
+
CREATE TABLE IF NOT EXISTS producers (
|
|
6
|
+
id INTEGER PRIMARY KEY,
|
|
7
|
+
name TEXT UNIQUE NOT NULL,
|
|
8
|
+
token_hash TEXT UNIQUE NOT NULL,
|
|
9
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
10
|
+
revoked_at TEXT
|
|
11
|
+
);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS consumers (
|
|
13
|
+
id INTEGER PRIMARY KEY,
|
|
14
|
+
name TEXT UNIQUE NOT NULL,
|
|
15
|
+
token_hash TEXT UNIQUE NOT NULL,
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
17
|
+
revoked_at TEXT,
|
|
18
|
+
config TEXT
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS grants (
|
|
21
|
+
producer_id INTEGER NOT NULL REFERENCES producers(id),
|
|
22
|
+
consumer_id INTEGER NOT NULL REFERENCES consumers(id),
|
|
23
|
+
alias TEXT NOT NULL,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
25
|
+
expires_at TEXT,
|
|
26
|
+
PRIMARY KEY (producer_id, consumer_id, alias)
|
|
27
|
+
);
|
|
28
|
+
CREATE TABLE IF NOT EXISTS offerings (
|
|
29
|
+
alias TEXT PRIMARY KEY,
|
|
30
|
+
producer_id INTEGER NOT NULL REFERENCES producers(id),
|
|
31
|
+
protocol TEXT NOT NULL,
|
|
32
|
+
upstream_model TEXT NOT NULL,
|
|
33
|
+
backend_id TEXT NOT NULL,
|
|
34
|
+
max_concurrency INTEGER NOT NULL DEFAULT 1,
|
|
35
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
38
|
+
id INTEGER PRIMARY KEY,
|
|
39
|
+
at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
40
|
+
consumer_id INTEGER NOT NULL,
|
|
41
|
+
producer_id INTEGER NOT NULL,
|
|
42
|
+
alias TEXT NOT NULL,
|
|
43
|
+
upstream_model TEXT NOT NULL,
|
|
44
|
+
protocol TEXT NOT NULL,
|
|
45
|
+
streaming INTEGER NOT NULL,
|
|
46
|
+
status INTEGER,
|
|
47
|
+
error_code TEXT,
|
|
48
|
+
duration_ms INTEGER NOT NULL,
|
|
49
|
+
bytes_in INTEGER NOT NULL,
|
|
50
|
+
bytes_out INTEGER NOT NULL,
|
|
51
|
+
prompt_tokens INTEGER,
|
|
52
|
+
completion_tokens INTEGER
|
|
53
|
+
);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_usage_at ON usage_events(at);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_usage_consumer ON usage_events(consumer_id, at);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_usage_producer ON usage_events(producer_id, at);
|
|
57
|
+
`;
|
|
58
|
+
export function openDb(dbFile) {
|
|
59
|
+
mkdirSync(dirname(dbFile), { recursive: true });
|
|
60
|
+
const db = new Database(dbFile);
|
|
61
|
+
db.pragma('journal_mode = WAL');
|
|
62
|
+
db.pragma('foreign_keys = ON');
|
|
63
|
+
const version = db.pragma('user_version', { simple: true });
|
|
64
|
+
if (version < 1) {
|
|
65
|
+
db.exec(SCHEMA_V2);
|
|
66
|
+
db.pragma('user_version = 2');
|
|
67
|
+
}
|
|
68
|
+
else if (version < 2) {
|
|
69
|
+
// v1 -> v2: grant expiry + per-consumer limit overrides
|
|
70
|
+
db.exec('ALTER TABLE grants ADD COLUMN expires_at TEXT');
|
|
71
|
+
db.exec('ALTER TABLE consumers ADD COLUMN config TEXT');
|
|
72
|
+
db.pragma('user_version = 2');
|
|
73
|
+
}
|
|
74
|
+
return db;
|
|
75
|
+
}
|
|
76
|
+
// ---- tokens / identities ----
|
|
77
|
+
export function insertProducer(db, name, tokenHash) {
|
|
78
|
+
const info = db
|
|
79
|
+
.prepare('INSERT INTO producers (name, token_hash) VALUES (?, ?)')
|
|
80
|
+
.run(name, tokenHash);
|
|
81
|
+
return getProducerById(db, Number(info.lastInsertRowid));
|
|
82
|
+
}
|
|
83
|
+
export function insertConsumer(db, name, tokenHash) {
|
|
84
|
+
const info = db
|
|
85
|
+
.prepare('INSERT INTO consumers (name, token_hash) VALUES (?, ?)')
|
|
86
|
+
.run(name, tokenHash);
|
|
87
|
+
return getConsumerById(db, Number(info.lastInsertRowid));
|
|
88
|
+
}
|
|
89
|
+
export function getProducerById(db, id) {
|
|
90
|
+
return db.prepare('SELECT * FROM producers WHERE id = ?').get(id);
|
|
91
|
+
}
|
|
92
|
+
export function getConsumerById(db, id) {
|
|
93
|
+
return db.prepare('SELECT * FROM consumers WHERE id = ?').get(id);
|
|
94
|
+
}
|
|
95
|
+
export function getProducerByName(db, name) {
|
|
96
|
+
return db.prepare('SELECT * FROM producers WHERE name = ?').get(name);
|
|
97
|
+
}
|
|
98
|
+
export function getConsumerByName(db, name) {
|
|
99
|
+
return db.prepare('SELECT * FROM consumers WHERE name = ?').get(name);
|
|
100
|
+
}
|
|
101
|
+
export function findProducerByHash(db, hash) {
|
|
102
|
+
return db.prepare('SELECT * FROM producers WHERE token_hash = ?').get(hash);
|
|
103
|
+
}
|
|
104
|
+
export function findConsumerByHash(db, hash) {
|
|
105
|
+
return db.prepare('SELECT * FROM consumers WHERE token_hash = ?').get(hash);
|
|
106
|
+
}
|
|
107
|
+
export function listTokens(db) {
|
|
108
|
+
const producers = db
|
|
109
|
+
.prepare('SELECT id, name, created_at, revoked_at FROM producers ORDER BY id')
|
|
110
|
+
.all();
|
|
111
|
+
const consumers = db
|
|
112
|
+
.prepare('SELECT id, name, created_at, revoked_at FROM consumers ORDER BY id')
|
|
113
|
+
.all();
|
|
114
|
+
return { producers, consumers };
|
|
115
|
+
}
|
|
116
|
+
export function revokeToken(db, role, id) {
|
|
117
|
+
const table = role === 'producer' ? 'producers' : 'consumers';
|
|
118
|
+
db.prepare(`UPDATE ${table} SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`).run(id);
|
|
119
|
+
}
|
|
120
|
+
// ---- grants ----
|
|
121
|
+
export function addGrant(db, producerId, consumerId, alias, expiresAt) {
|
|
122
|
+
// re-granting refreshes the expiry (INSERT OR IGNORE would keep a stale one)
|
|
123
|
+
db.prepare(`
|
|
124
|
+
INSERT INTO grants (producer_id, consumer_id, alias, expires_at) VALUES (?, ?, ?, ?)
|
|
125
|
+
ON CONFLICT(producer_id, consumer_id, alias) DO UPDATE SET expires_at = excluded.expires_at`).run(producerId, consumerId, alias, expiresAt ?? null);
|
|
126
|
+
}
|
|
127
|
+
export function removeGrant(db, producerId, consumerId, alias) {
|
|
128
|
+
const info = db
|
|
129
|
+
.prepare('DELETE FROM grants WHERE producer_id = ? AND consumer_id = ? AND alias = ?')
|
|
130
|
+
.run(producerId, consumerId, alias);
|
|
131
|
+
return info.changes > 0;
|
|
132
|
+
}
|
|
133
|
+
/** Returns the grant row regardless of expiry; callers decide missing vs expired. */
|
|
134
|
+
export function getGrant(db, producerId, consumerId, alias) {
|
|
135
|
+
return db
|
|
136
|
+
.prepare('SELECT * FROM grants WHERE producer_id = ? AND consumer_id = ? AND alias = ?')
|
|
137
|
+
.get(producerId, consumerId, alias);
|
|
138
|
+
}
|
|
139
|
+
export function listGrants(db, producerId) {
|
|
140
|
+
const sql = `
|
|
141
|
+
SELECT g.alias, g.created_at, g.expires_at, p.name AS producer, c.name AS consumer
|
|
142
|
+
FROM grants g
|
|
143
|
+
JOIN producers p ON p.id = g.producer_id
|
|
144
|
+
JOIN consumers c ON c.id = g.consumer_id
|
|
145
|
+
${producerId !== undefined ? 'WHERE g.producer_id = ?' : ''}
|
|
146
|
+
ORDER BY g.alias, c.name`;
|
|
147
|
+
const stmt = db.prepare(sql);
|
|
148
|
+
return (producerId !== undefined ? stmt.all(producerId) : stmt.all());
|
|
149
|
+
}
|
|
150
|
+
// ---- offerings (cache of what the agent registers; source of truth is agent config) ----
|
|
151
|
+
export function upsertOfferings(db, producerId, offerings) {
|
|
152
|
+
const upsert = db.prepare(`
|
|
153
|
+
INSERT INTO offerings (alias, producer_id, protocol, upstream_model, backend_id, max_concurrency, updated_at)
|
|
154
|
+
VALUES (@alias, @producerId, @protocol, @upstreamModel, @backendId, @maxConcurrency, strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
155
|
+
ON CONFLICT(alias) DO UPDATE SET
|
|
156
|
+
producer_id = excluded.producer_id,
|
|
157
|
+
protocol = excluded.protocol,
|
|
158
|
+
upstream_model = excluded.upstream_model,
|
|
159
|
+
backend_id = excluded.backend_id,
|
|
160
|
+
max_concurrency = excluded.max_concurrency,
|
|
161
|
+
updated_at = excluded.updated_at`);
|
|
162
|
+
const prune = db.prepare('DELETE FROM offerings WHERE producer_id = ? AND alias NOT IN (SELECT value FROM json_each(?))');
|
|
163
|
+
const tx = db.transaction((items) => {
|
|
164
|
+
for (const o of items) {
|
|
165
|
+
upsert.run({
|
|
166
|
+
alias: o.alias,
|
|
167
|
+
producerId,
|
|
168
|
+
protocol: o.protocol,
|
|
169
|
+
upstreamModel: o.upstreamModel,
|
|
170
|
+
backendId: o.backendId,
|
|
171
|
+
maxConcurrency: o.maxConcurrency,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
prune.run(producerId, JSON.stringify(items.map((o) => o.alias)));
|
|
175
|
+
});
|
|
176
|
+
tx(offerings);
|
|
177
|
+
}
|
|
178
|
+
export function getOffering(db, alias) {
|
|
179
|
+
return db.prepare('SELECT * FROM offerings WHERE alias = ?').get(alias);
|
|
180
|
+
}
|
|
181
|
+
export function listOfferingsForConsumer(db, consumerId) {
|
|
182
|
+
return db
|
|
183
|
+
.prepare(`SELECT o.* FROM offerings o
|
|
184
|
+
JOIN grants g ON g.producer_id = o.producer_id AND g.alias = o.alias
|
|
185
|
+
WHERE g.consumer_id = ?
|
|
186
|
+
ORDER BY o.alias`)
|
|
187
|
+
.all(consumerId);
|
|
188
|
+
}
|
|
189
|
+
// ---- usage ----
|
|
190
|
+
export function insertUsageEvent(db, event) {
|
|
191
|
+
db.prepare(`
|
|
192
|
+
INSERT INTO usage_events
|
|
193
|
+
(consumer_id, producer_id, alias, upstream_model, protocol, streaming, status, error_code,
|
|
194
|
+
duration_ms, bytes_in, bytes_out, prompt_tokens, completion_tokens)
|
|
195
|
+
VALUES
|
|
196
|
+
(@consumer_id, @producer_id, @alias, @upstream_model, @protocol, @streaming, @status, @error_code,
|
|
197
|
+
@duration_ms, @bytes_in, @bytes_out, @prompt_tokens, @completion_tokens)`).run(event);
|
|
198
|
+
}
|
|
199
|
+
export function queryUsage(db, filter) {
|
|
200
|
+
const where = [];
|
|
201
|
+
// clamp both ways: a negative LIMIT in SQLite means "no limit"
|
|
202
|
+
const params = {
|
|
203
|
+
limit: Math.min(Math.max(Math.trunc(filter.limit) || 100, 1), 1000),
|
|
204
|
+
};
|
|
205
|
+
if (filter.consumerId !== undefined) {
|
|
206
|
+
where.push('consumer_id = @consumerId');
|
|
207
|
+
params.consumerId = filter.consumerId;
|
|
208
|
+
}
|
|
209
|
+
if (filter.producerId !== undefined) {
|
|
210
|
+
where.push('producer_id = @producerId');
|
|
211
|
+
params.producerId = filter.producerId;
|
|
212
|
+
}
|
|
213
|
+
if (filter.alias !== undefined) {
|
|
214
|
+
where.push('alias = @alias');
|
|
215
|
+
params.alias = filter.alias;
|
|
216
|
+
}
|
|
217
|
+
const sql = `SELECT * FROM usage_events ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY id DESC LIMIT @limit`;
|
|
218
|
+
return db.prepare(sql).all(params);
|
|
219
|
+
}
|
|
220
|
+
// ---- per-consumer limit overrides (sparse JSON in consumers.config) ----
|
|
221
|
+
export function getConsumerConfig(db, consumerId) {
|
|
222
|
+
const row = db.prepare('SELECT config FROM consumers WHERE id = ?').get(consumerId);
|
|
223
|
+
return row?.config ?? null;
|
|
224
|
+
}
|
|
225
|
+
export function setConsumerConfig(db, consumerId, config) {
|
|
226
|
+
db.prepare('UPDATE consumers SET config = ? WHERE id = ?').run(config, consumerId);
|
|
227
|
+
}
|
|
228
|
+
/** Lifetime tokens consumed by a consumer; NULL counts (e.g. Ollama streams) read as 0. */
|
|
229
|
+
export function sumTokensUsed(db, consumerId) {
|
|
230
|
+
const row = db
|
|
231
|
+
.prepare(`SELECT SUM(COALESCE(prompt_tokens, 0) + COALESCE(completion_tokens, 0)) AS total
|
|
232
|
+
FROM usage_events WHERE consumer_id = ?`)
|
|
233
|
+
.get(consumerId);
|
|
234
|
+
return row.total ?? 0;
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAErC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDjB,CAAA;AAiDD,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/C,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC/B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;IAC/B,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC9B,MAAM,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAW,CAAA;IACrE,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAClB,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;IAC/B,CAAC;SAAM,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QACvB,wDAAwD;QACxD,EAAE,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAA;QACxD,EAAE,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAA;QACvD,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;IAC/B,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED,gCAAgC;AAEhC,MAAM,UAAU,cAAc,CAC5B,EAAqB,EACrB,IAAY,EACZ,SAAiB;IAEjB,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CAAC,wDAAwD,CAAC;SACjE,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IACvB,OAAO,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,EAAqB,EACrB,IAAY,EACZ,SAAiB;IAEjB,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CAAC,wDAAwD,CAAC;SACjE,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IACvB,OAAO,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAqB,EAAE,EAAU;IAC/D,OAAO,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAgB,CAAA;AAClF,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAqB,EAAE,EAAU;IAC/D,OAAO,EAAE,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAgB,CAAA;AAClF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAqB,EAAE,IAAY;IACnE,OAAO,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC,GAAG,CAAC,IAAI,CAA4B,CAAA;AAClG,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAqB,EAAE,IAAY;IACnE,OAAO,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC,GAAG,CAAC,IAAI,CAA4B,CAAA;AAClG,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAqB,EAAE,IAAY;IACpE,OAAO,EAAE,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAC,GAAG,CAAC,IAAI,CAE7D,CAAA;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAqB,EAAE,IAAY;IACpE,OAAO,EAAE,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAC,GAAG,CAAC,IAAI,CAE7D,CAAA;AACf,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAqB;IAC9C,MAAM,SAAS,GAAG,EAAE;SACjB,OAAO,CAAC,oEAAoE,CAAC;SAC7E,GAAG,EAAuC,CAAA;IAC7C,MAAM,SAAS,GAAG,EAAE;SACjB,OAAO,CAAC,oEAAoE,CAAC;SAC7E,GAAG,EAAuC,CAAA;IAC7C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAqB,EAAE,IAA6B,EAAE,EAAU;IAC1F,MAAM,KAAK,GAAG,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAA;IAC7D,EAAE,CAAC,OAAO,CACR,UAAU,KAAK,qEAAqE,CACrF,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AACX,CAAC;AAED,mBAAmB;AAEnB,MAAM,UAAU,QAAQ,CACtB,EAAqB,EACrB,UAAkB,EAClB,UAAkB,EAClB,KAAa,EACb,SAAyB;IAEzB,6EAA6E;IAC7E,EAAE,CAAC,OAAO,CAAC;;gGAEmF,CAAC,CAAC,GAAG,CACjG,UAAU,EACV,UAAU,EACV,KAAK,EACL,SAAS,IAAI,IAAI,CAClB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,EAAqB,EACrB,UAAkB,EAClB,UAAkB,EAClB,KAAa;IAEb,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CAAC,4EAA4E,CAAC;SACrF,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,CAAC,CAAA;IACrC,OAAO,IAAI,CAAC,OAAO,GAAG,CAAC,CAAA;AACzB,CAAC;AAUD,qFAAqF;AACrF,MAAM,UAAU,QAAQ,CACtB,EAAqB,EACrB,UAAkB,EAClB,UAAkB,EAClB,KAAa;IAEb,OAAO,EAAE;SACN,OAAO,CAAC,8EAA8E,CAAC;SACvF,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,CAAyB,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAqB,EAAE,UAAmB;IACnE,MAAM,GAAG,GAAG;;;;;MAKR,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE;6BAClC,CAAA;IAC3B,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC5B,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAMjE,CAAA;AACL,CAAC;AAED,2FAA2F;AAE3F,MAAM,UAAU,eAAe,CAC7B,EAAqB,EACrB,UAAkB,EAClB,SAAqB;IAErB,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;;;;uCASW,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,EAAE,CAAC,OAAO,CACtB,+FAA+F,CAChG,CAAA;IACD,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,KAAiB,EAAE,EAAE;QAC9C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,CAAC;gBACT,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,UAAU;gBACV,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,aAAa,EAAE,CAAC,CAAC,aAAa;gBAC9B,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,cAAc,EAAE,CAAC,CAAC,cAAc;aACjC,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;IACF,EAAE,CAAC,SAAS,CAAC,CAAA;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAqB,EAAE,KAAa;IAC9D,OAAO,EAAE,CAAC,OAAO,CAAC,yCAAyC,CAAC,CAAC,GAAG,CAAC,KAAK,CAA4B,CAAA;AACpG,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,EAAqB,EAAE,UAAkB;IAChF,OAAO,EAAE;SACN,OAAO,CACN;;;wBAGkB,CACnB;SACA,GAAG,CAAC,UAAU,CAAkB,CAAA;AACrC,CAAC;AAED,kBAAkB;AAElB,MAAM,UAAU,gBAAgB,CAAC,EAAqB,EAAE,KAAoB;IAC1E,EAAE,CAAC,OAAO,CAAC;;;;;;gFAMmE,CAAC,CAAC,GAAG,CACjF,KAAc,CACf,CAAA;AACH,CAAC;AASD,MAAM,UAAU,UAAU,CAAC,EAAqB,EAAE,MAAmB;IACnE,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,+DAA+D;IAC/D,MAAM,MAAM,GAA4B;QACtC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC;KACpE,CAAA;IACD,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAA;QACvC,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACvC,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAA;QACvC,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACvC,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QAC5B,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;IAC7B,CAAC;IACD,MAAM,GAAG,GAAG,8BAA8B,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,gCAAgC,CAAA;IAC5H,OAAO,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAoB,CAAA;AACvD,CAAC;AAED,2EAA2E;AAE3E,MAAM,UAAU,iBAAiB,CAAC,EAAqB,EAAE,UAAkB;IACzE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC,GAAG,CAAC,UAAU,CAErE,CAAA;IACb,OAAO,GAAG,EAAE,MAAM,IAAI,IAAI,CAAA;AAC5B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,EAAqB,EACrB,UAAkB,EAClB,MAAqB;IAErB,EAAE,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AACpF,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,aAAa,CAAC,EAAqB,EAAE,UAAkB;IACrE,MAAM,GAAG,GAAG,EAAE;SACX,OAAO,CACN;+CACyC,CAC1C;SACA,GAAG,CAAC,UAAU,CAA6B,CAAA;IAC9C,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC,CAAA;AACvB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/** Error carrying an HTTP status and a stable error code. */
|
|
3
|
+
export declare class HttpError extends Error {
|
|
4
|
+
status: number;
|
|
5
|
+
code: string;
|
|
6
|
+
constructor(status: number, code: string, message: string);
|
|
7
|
+
}
|
|
8
|
+
export interface RouteContext {
|
|
9
|
+
req: IncomingMessage;
|
|
10
|
+
res: ServerResponse;
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
query: URLSearchParams;
|
|
13
|
+
url: URL;
|
|
14
|
+
}
|
|
15
|
+
export type RouteHandler = (ctx: RouteContext) => Promise<void> | void;
|
|
16
|
+
export declare class Router {
|
|
17
|
+
private routes;
|
|
18
|
+
add(method: string, pattern: string, handler: RouteHandler): void;
|
|
19
|
+
/** Returns matched handler + params, or 404/405 markers. */
|
|
20
|
+
match(method: string, pathname: string): {
|
|
21
|
+
handler: RouteHandler;
|
|
22
|
+
params: Record<string, string>;
|
|
23
|
+
} | 'not-found' | 'method-not-allowed';
|
|
24
|
+
}
|
|
25
|
+
export declare function sendJson(res: ServerResponse, status: number, body: unknown): void;
|
|
26
|
+
export declare function sendError(res: ServerResponse, err: unknown, requestId?: string): void;
|
|
27
|
+
/** Read the full request body as a Buffer, enforcing a size cap. */
|
|
28
|
+
export declare function readBody(req: IncomingMessage, maxBytes: number): Promise<Buffer>;
|
|
29
|
+
export declare function readJson(req: IncomingMessage, maxBytes: number): Promise<unknown>;
|
|
30
|
+
/** Bearer or x-api-key credential, whichever is present. */
|
|
31
|
+
export declare function extractCredential(req: IncomingMessage): string | null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Error carrying an HTTP status and a stable error code. */
|
|
2
|
+
export class HttpError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
constructor(status, code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class Router {
|
|
12
|
+
routes = [];
|
|
13
|
+
add(method, pattern, handler) {
|
|
14
|
+
this.routes.push({ method, parts: pattern.split('/').filter(Boolean), handler });
|
|
15
|
+
}
|
|
16
|
+
/** Returns matched handler + params, or 404/405 markers. */
|
|
17
|
+
match(method, pathname) {
|
|
18
|
+
let pathExists = false;
|
|
19
|
+
for (const route of this.routes) {
|
|
20
|
+
const params = matchParts(route.parts, pathname);
|
|
21
|
+
if (!params)
|
|
22
|
+
continue;
|
|
23
|
+
pathExists = true;
|
|
24
|
+
if (route.method === method)
|
|
25
|
+
return { handler: route.handler, params };
|
|
26
|
+
}
|
|
27
|
+
return pathExists ? 'method-not-allowed' : 'not-found';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function matchParts(parts, pathname) {
|
|
31
|
+
const segs = pathname.split('/').filter(Boolean);
|
|
32
|
+
if (segs.length !== parts.length)
|
|
33
|
+
return null;
|
|
34
|
+
const params = {};
|
|
35
|
+
for (let i = 0; i < parts.length; i++) {
|
|
36
|
+
const part = parts[i];
|
|
37
|
+
const seg = segs[i];
|
|
38
|
+
if (part === undefined || seg === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
if (part.startsWith(':')) {
|
|
41
|
+
// a segment we cannot decode can never name a real resource — treat as no match
|
|
42
|
+
let decoded;
|
|
43
|
+
try {
|
|
44
|
+
decoded = decodeURIComponent(seg);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
params[part.slice(1)] = decoded;
|
|
50
|
+
}
|
|
51
|
+
else if (part !== seg) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return params;
|
|
56
|
+
}
|
|
57
|
+
export function sendJson(res, status, body) {
|
|
58
|
+
const payload = JSON.stringify(body);
|
|
59
|
+
res.writeHead(status, {
|
|
60
|
+
'content-type': 'application/json',
|
|
61
|
+
'content-length': Buffer.byteLength(payload),
|
|
62
|
+
});
|
|
63
|
+
res.end(payload);
|
|
64
|
+
}
|
|
65
|
+
export function sendError(res, err, requestId) {
|
|
66
|
+
if (res.headersSent) {
|
|
67
|
+
res.destroy();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const httpErr = err instanceof HttpError
|
|
71
|
+
? err
|
|
72
|
+
: new HttpError(500, 'INTERNAL', err instanceof Error ? err.message : String(err));
|
|
73
|
+
sendJson(res, httpErr.status, {
|
|
74
|
+
error: { code: httpErr.code, message: httpErr.message, ...(requestId ? { requestId } : {}) },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Read the full request body as a Buffer, enforcing a size cap. */
|
|
78
|
+
export function readBody(req, maxBytes) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
let size = 0;
|
|
82
|
+
req.on('data', (chunk) => {
|
|
83
|
+
size += chunk.byteLength;
|
|
84
|
+
if (size > maxBytes) {
|
|
85
|
+
// pause, don't destroy: the 413 must reach the client before the
|
|
86
|
+
// connection drops (Node closes the socket after an unconsumed body)
|
|
87
|
+
req.pause();
|
|
88
|
+
reject(new HttpError(413, 'BODY_TOO_LARGE', `body exceeds ${maxBytes} bytes`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
chunks.push(chunk);
|
|
92
|
+
});
|
|
93
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function readJson(req, maxBytes) {
|
|
98
|
+
const body = await readBody(req, maxBytes);
|
|
99
|
+
if (body.byteLength === 0)
|
|
100
|
+
return {};
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(body.toString('utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new HttpError(400, 'INVALID_JSON', 'request body is not valid JSON');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Bearer or x-api-key credential, whichever is present. */
|
|
109
|
+
export function extractCredential(req) {
|
|
110
|
+
const auth = req.headers.authorization;
|
|
111
|
+
if (auth?.startsWith('Bearer '))
|
|
112
|
+
return auth.slice(7).trim();
|
|
113
|
+
const apiKey = req.headers['x-api-key'];
|
|
114
|
+
if (typeof apiKey === 'string' && apiKey.length > 0)
|
|
115
|
+
return apiKey.trim();
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** In-memory per-consumer token bucket + inflight cap, and per-alias concurrency slots. */
|
|
2
|
+
export declare class ConsumerLimiter {
|
|
3
|
+
private buckets;
|
|
4
|
+
/**
|
|
5
|
+
* Returns error code when the consumer is over limit, or null when admitted.
|
|
6
|
+
* Limits are passed per call (per-consumer overrides merged over globals),
|
|
7
|
+
* so config changes take effect on the next request.
|
|
8
|
+
*/
|
|
9
|
+
admit(consumerId: number, limits: {
|
|
10
|
+
rps: number;
|
|
11
|
+
burst: number;
|
|
12
|
+
maxInflight: number;
|
|
13
|
+
}): string | null;
|
|
14
|
+
release(consumerId: number): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sliding-window tokens-per-minute counter, in memory (like the RPS bucket).
|
|
18
|
+
* Tokens are recorded when a request finishes; admit-time checks are best-effort:
|
|
19
|
+
* concurrent requests that all pass before any finishes can overshoot, and requests
|
|
20
|
+
* whose upstream reports no usage (NULL token counts) contribute nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TokenWindow {
|
|
23
|
+
private windows;
|
|
24
|
+
/** True when the last 60s sum is still under the cap. */
|
|
25
|
+
allow(consumerId: number, tpm: number, now?: number): boolean;
|
|
26
|
+
record(consumerId: number, tokens: number, now?: number): void;
|
|
27
|
+
private prune;
|
|
28
|
+
}
|
|
29
|
+
/** Counts in-flight requests per alias; the cap comes from the producer's offering. */
|
|
30
|
+
export declare class AliasSlots {
|
|
31
|
+
private active;
|
|
32
|
+
/** Returns true when the slot was acquired. */
|
|
33
|
+
acquire(alias: string, maxConcurrency: number): boolean;
|
|
34
|
+
release(alias: string): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** In-memory per-consumer token bucket + inflight cap, and per-alias concurrency slots. */
|
|
2
|
+
export class ConsumerLimiter {
|
|
3
|
+
buckets = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* Returns error code when the consumer is over limit, or null when admitted.
|
|
6
|
+
* Limits are passed per call (per-consumer overrides merged over globals),
|
|
7
|
+
* so config changes take effect on the next request.
|
|
8
|
+
*/
|
|
9
|
+
admit(consumerId, limits) {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
let bucket = this.buckets.get(consumerId);
|
|
12
|
+
if (!bucket) {
|
|
13
|
+
bucket = { tokens: limits.burst, lastRefill: now, inflight: 0 };
|
|
14
|
+
this.buckets.set(consumerId, bucket);
|
|
15
|
+
}
|
|
16
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
17
|
+
bucket.tokens = Math.min(limits.burst, bucket.tokens + elapsed * limits.rps);
|
|
18
|
+
bucket.lastRefill = now;
|
|
19
|
+
if (bucket.tokens < 1)
|
|
20
|
+
return 'RATE_LIMITED';
|
|
21
|
+
if (bucket.inflight >= limits.maxInflight)
|
|
22
|
+
return 'RATE_LIMITED';
|
|
23
|
+
bucket.tokens -= 1;
|
|
24
|
+
bucket.inflight += 1;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
release(consumerId) {
|
|
28
|
+
const bucket = this.buckets.get(consumerId);
|
|
29
|
+
if (bucket && bucket.inflight > 0)
|
|
30
|
+
bucket.inflight -= 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sliding-window tokens-per-minute counter, in memory (like the RPS bucket).
|
|
35
|
+
* Tokens are recorded when a request finishes; admit-time checks are best-effort:
|
|
36
|
+
* concurrent requests that all pass before any finishes can overshoot, and requests
|
|
37
|
+
* whose upstream reports no usage (NULL token counts) contribute nothing.
|
|
38
|
+
*/
|
|
39
|
+
export class TokenWindow {
|
|
40
|
+
windows = new Map();
|
|
41
|
+
/** True when the last 60s sum is still under the cap. */
|
|
42
|
+
allow(consumerId, tpm, now = Date.now()) {
|
|
43
|
+
const entries = this.prune(consumerId, now);
|
|
44
|
+
const sum = entries.reduce((acc, e) => acc + e.tokens, 0);
|
|
45
|
+
return sum < tpm;
|
|
46
|
+
}
|
|
47
|
+
record(consumerId, tokens, now = Date.now()) {
|
|
48
|
+
if (tokens <= 0)
|
|
49
|
+
return;
|
|
50
|
+
this.prune(consumerId, now).push({ at: now, tokens });
|
|
51
|
+
}
|
|
52
|
+
prune(consumerId, now) {
|
|
53
|
+
const entries = this.windows.get(consumerId) ?? [];
|
|
54
|
+
const kept = entries.filter((e) => now - e.at < 60_000);
|
|
55
|
+
this.windows.set(consumerId, kept);
|
|
56
|
+
return kept;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Counts in-flight requests per alias; the cap comes from the producer's offering. */
|
|
60
|
+
export class AliasSlots {
|
|
61
|
+
active = new Map();
|
|
62
|
+
/** Returns true when the slot was acquired. */
|
|
63
|
+
acquire(alias, maxConcurrency) {
|
|
64
|
+
const current = this.active.get(alias) ?? 0;
|
|
65
|
+
if (current >= maxConcurrency)
|
|
66
|
+
return false;
|
|
67
|
+
this.active.set(alias, current + 1);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
release(alias) {
|
|
71
|
+
const current = this.active.get(alias) ?? 0;
|
|
72
|
+
if (current <= 1)
|
|
73
|
+
this.active.delete(alias);
|
|
74
|
+
else
|
|
75
|
+
this.active.set(alias, current - 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.js","sourceRoot":"","sources":["../src/limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HubConfig } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sparse per-consumer limit overrides stored in consumers.config (JSON).
|
|
4
|
+
* Only set keys override the hub-wide defaults; everything else falls back.
|
|
5
|
+
*/
|
|
6
|
+
export interface ConsumerLimits {
|
|
7
|
+
/** Requests per second (may be fractional). */
|
|
8
|
+
rps?: number;
|
|
9
|
+
/** Token-bucket burst size. */
|
|
10
|
+
burst?: number;
|
|
11
|
+
/** Max in-flight requests. */
|
|
12
|
+
maxConcurrent?: number;
|
|
13
|
+
/** Max tokens (prompt + completion) in any sliding 60s window; null = unlimited. */
|
|
14
|
+
tpm?: number;
|
|
15
|
+
/** Lifetime token budget (prompt + completion); null = unlimited. */
|
|
16
|
+
maxTotalTokens?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Limits actually enforced for one request: overrides merged over hub globals. */
|
|
19
|
+
export interface ResolvedLimits {
|
|
20
|
+
rps: number;
|
|
21
|
+
burst: number;
|
|
22
|
+
maxInflight: number;
|
|
23
|
+
tpm: number | null;
|
|
24
|
+
maxTotalTokens: number | null;
|
|
25
|
+
}
|
|
26
|
+
/** Strict validation for the admin write path; throws with all problems listed. */
|
|
27
|
+
export declare function parseLimits(input: unknown): ConsumerLimits;
|
|
28
|
+
/** Parse the stored JSON tolerantly (written only via validated endpoint). */
|
|
29
|
+
export declare function safeParseLimits(json: string | null): ConsumerLimits | null;
|
|
30
|
+
export declare function resolveLimits(overrides: ConsumerLimits | null, global: HubConfig): ResolvedLimits;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const INTEGER_KEYS = ['burst', 'maxConcurrent', 'tpm', 'maxTotalTokens'];
|
|
2
|
+
/** Strict validation for the admin write path; throws with all problems listed. */
|
|
3
|
+
export function parseLimits(input) {
|
|
4
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
5
|
+
throw new Error('limits must be a JSON object');
|
|
6
|
+
}
|
|
7
|
+
const raw = input;
|
|
8
|
+
const allowed = ['rps', 'burst', 'maxConcurrent', 'tpm', 'maxTotalTokens'];
|
|
9
|
+
const unknown = Object.keys(raw).filter((k) => !allowed.includes(k));
|
|
10
|
+
if (unknown.length > 0)
|
|
11
|
+
throw new Error(`unknown limit keys: ${unknown.join(', ')}`);
|
|
12
|
+
const out = {};
|
|
13
|
+
const problems = [];
|
|
14
|
+
for (const key of allowed) {
|
|
15
|
+
const value = raw[key];
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
19
|
+
problems.push(`${key} must be a positive number`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (INTEGER_KEYS.includes(key) && !Number.isInteger(value)) {
|
|
23
|
+
problems.push(`${key} must be an integer`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
out[key] = value;
|
|
27
|
+
}
|
|
28
|
+
if (problems.length > 0)
|
|
29
|
+
throw new Error(problems.join('; '));
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** Parse the stored JSON tolerantly (written only via validated endpoint). */
|
|
33
|
+
export function safeParseLimits(json) {
|
|
34
|
+
if (!json)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
return parseLimits(JSON.parse(json));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function resolveLimits(overrides, global) {
|
|
44
|
+
return {
|
|
45
|
+
rps: overrides?.rps ?? global.consumerRps,
|
|
46
|
+
burst: overrides?.burst ?? global.consumerBurst,
|
|
47
|
+
maxInflight: overrides?.maxConcurrent ?? global.consumerMaxConcurrent,
|
|
48
|
+
tpm: overrides?.tpm ?? null,
|
|
49
|
+
maxTotalTokens: overrides?.maxTotalTokens ?? null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"AA4BA,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,gBAAgB,CAAU,CAAA;AAEjF,mFAAmF;AACnF,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;IACjD,CAAC;IACD,MAAM,GAAG,GAAG,KAAgC,CAAA;IAC5C,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAA;IAC1E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpF,MAAM,GAAG,GAAmB,EAAE,CAAA;IAC9B,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,KAAK,MAAM,GAAG,IAAI,OAAmC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;QACtB,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,4BAA4B,CAAC,CAAA;YACjD,SAAQ;QACV,CAAC;QACD,IAAK,YAAkC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAClF,QAAQ,CAAC,IAAI,CAAC,GAAG,GAAG,qBAAqB,CAAC,CAAA;YAC1C,SAAQ;QACV,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IAClB,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC7D,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,IAAI,CAAC;QACH,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAgC,EAAE,MAAiB;IAC/E,OAAO;QACL,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,MAAM,CAAC,WAAW;QACzC,KAAK,EAAE,SAAS,EAAE,KAAK,IAAI,MAAM,CAAC,aAAa;QAC/C,WAAW,EAAE,SAAS,EAAE,aAAa,IAAI,MAAM,CAAC,qBAAqB;QACrE,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,IAAI;QAC3B,cAAc,EAAE,SAAS,EAAE,cAAc,IAAI,IAAI;KAClD,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Protocol } from 'aweshare-protocol';
|
|
2
|
+
export interface TokenCounts {
|
|
3
|
+
promptTokens: number | null;
|
|
4
|
+
completionTokens: number | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort usage extraction from a response stream, on a side channel.
|
|
8
|
+
* Never throws, never blocks the relay; failures simply yield null counts.
|
|
9
|
+
* No content is retained beyond the bytes needed to read token counts.
|
|
10
|
+
*/
|
|
11
|
+
export declare class UsageExtractor {
|
|
12
|
+
private readonly protocol;
|
|
13
|
+
private text;
|
|
14
|
+
private promptTokens;
|
|
15
|
+
private completionTokens;
|
|
16
|
+
private readonly isSse;
|
|
17
|
+
private done;
|
|
18
|
+
constructor(protocol: Protocol, contentType: string | undefined);
|
|
19
|
+
get streaming(): boolean;
|
|
20
|
+
onChunk(bytes: Uint8Array): void;
|
|
21
|
+
finish(): TokenCounts | null;
|
|
22
|
+
private consumeSseEvent;
|
|
23
|
+
private consumeUsage;
|
|
24
|
+
private result;
|
|
25
|
+
}
|