@agentplugged/claw 0.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/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +94 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/capture.d.ts +13 -0
- package/dist/memory/capture.d.ts.map +1 -0
- package/dist/memory/capture.js +102 -0
- package/dist/memory/capture.js.map +1 -0
- package/dist/memory/db.d.ts +56 -0
- package/dist/memory/db.d.ts.map +1 -0
- package/dist/memory/db.js +206 -0
- package/dist/memory/db.js.map +1 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/llm-extract.d.ts +13 -0
- package/dist/memory/llm-extract.d.ts.map +1 -0
- package/dist/memory/llm-extract.js +135 -0
- package/dist/memory/llm-extract.js.map +1 -0
- package/dist/memory/recall.d.ts +16 -0
- package/dist/memory/recall.d.ts.map +1 -0
- package/dist/memory/recall.js +131 -0
- package/dist/memory/recall.js.map +1 -0
- package/dist/observability/collector.d.ts +40 -0
- package/dist/observability/collector.d.ts.map +1 -0
- package/dist/observability/collector.js +119 -0
- package/dist/observability/collector.js.map +1 -0
- package/dist/observability/db.d.ts +60 -0
- package/dist/observability/db.d.ts.map +1 -0
- package/dist/observability/db.js +189 -0
- package/dist/observability/db.js.map +1 -0
- package/dist/observability/index.d.ts +7 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +35 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/queries.d.ts +54 -0
- package/dist/observability/queries.d.ts.map +1 -0
- package/dist/observability/queries.js +181 -0
- package/dist/observability/queries.js.map +1 -0
- package/dist/router/classifier.d.ts +8 -0
- package/dist/router/classifier.d.ts.map +1 -0
- package/dist/router/classifier.js +90 -0
- package/dist/router/classifier.js.map +1 -0
- package/dist/router/fallback.d.ts +18 -0
- package/dist/router/fallback.d.ts.map +1 -0
- package/dist/router/fallback.js +102 -0
- package/dist/router/fallback.js.map +1 -0
- package/dist/router/index.d.ts +5 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +214 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/leak-detector.d.ts +33 -0
- package/dist/router/leak-detector.d.ts.map +1 -0
- package/dist/router/leak-detector.js +320 -0
- package/dist/router/leak-detector.js.map +1 -0
- package/dist/router/logger.d.ts +18 -0
- package/dist/router/logger.d.ts.map +1 -0
- package/dist/router/logger.js +130 -0
- package/dist/router/logger.js.map +1 -0
- package/dist/router/models.d.ts +5 -0
- package/dist/router/models.d.ts.map +1 -0
- package/dist/router/models.js +96 -0
- package/dist/router/models.js.map +1 -0
- package/dist/router/providers.d.ts +4 -0
- package/dist/router/providers.d.ts.map +1 -0
- package/dist/router/providers.js +323 -0
- package/dist/router/providers.js.map +1 -0
- package/dist/router/strategy.d.ts +29 -0
- package/dist/router/strategy.d.ts.map +1 -0
- package/dist/router/strategy.js +169 -0
- package/dist/router/strategy.js.map +1 -0
- package/dist/router/types.d.ts +63 -0
- package/dist/router/types.d.ts.map +1 -0
- package/dist/router/types.js +3 -0
- package/dist/router/types.js.map +1 -0
- package/dist/sidecar/auth.d.ts +7 -0
- package/dist/sidecar/auth.d.ts.map +1 -0
- package/dist/sidecar/auth.js +36 -0
- package/dist/sidecar/auth.js.map +1 -0
- package/dist/sidecar/index.d.ts +2 -0
- package/dist/sidecar/index.d.ts.map +1 -0
- package/dist/sidecar/index.js +336 -0
- package/dist/sidecar/index.js.map +1 -0
- package/dist/sidecar/routes/addons.d.ts +6 -0
- package/dist/sidecar/routes/addons.d.ts.map +1 -0
- package/dist/sidecar/routes/addons.js +332 -0
- package/dist/sidecar/routes/addons.js.map +1 -0
- package/dist/sidecar/routes/backup.d.ts +7 -0
- package/dist/sidecar/routes/backup.d.ts.map +1 -0
- package/dist/sidecar/routes/backup.js +204 -0
- package/dist/sidecar/routes/backup.js.map +1 -0
- package/dist/sidecar/routes/channels.d.ts +4 -0
- package/dist/sidecar/routes/channels.d.ts.map +1 -0
- package/dist/sidecar/routes/channels.js +120 -0
- package/dist/sidecar/routes/channels.js.map +1 -0
- package/dist/sidecar/routes/health.d.ts +5 -0
- package/dist/sidecar/routes/health.d.ts.map +1 -0
- package/dist/sidecar/routes/health.js +28 -0
- package/dist/sidecar/routes/health.js.map +1 -0
- package/dist/sidecar/routes/memory.d.ts +7 -0
- package/dist/sidecar/routes/memory.d.ts.map +1 -0
- package/dist/sidecar/routes/memory.js +234 -0
- package/dist/sidecar/routes/memory.js.map +1 -0
- package/dist/sidecar/routes/metrics.d.ts +5 -0
- package/dist/sidecar/routes/metrics.d.ts.map +1 -0
- package/dist/sidecar/routes/metrics.js +273 -0
- package/dist/sidecar/routes/metrics.js.map +1 -0
- package/dist/sidecar/routes/restart.d.ts +4 -0
- package/dist/sidecar/routes/restart.d.ts.map +1 -0
- package/dist/sidecar/routes/restart.js +81 -0
- package/dist/sidecar/routes/restart.js.map +1 -0
- package/dist/sidecar/routes/router-config.d.ts +5 -0
- package/dist/sidecar/routes/router-config.d.ts.map +1 -0
- package/dist/sidecar/routes/router-config.js +150 -0
- package/dist/sidecar/routes/router-config.js.map +1 -0
- package/dist/sidecar/routes/security.d.ts +8 -0
- package/dist/sidecar/routes/security.d.ts.map +1 -0
- package/dist/sidecar/routes/security.js +308 -0
- package/dist/sidecar/routes/security.js.map +1 -0
- package/dist/sidecar/routes/skills.d.ts +5 -0
- package/dist/sidecar/routes/skills.d.ts.map +1 -0
- package/dist/sidecar/routes/skills.js +146 -0
- package/dist/sidecar/routes/skills.js.map +1 -0
- package/dist/sidecar/routes/soul.d.ts +4 -0
- package/dist/sidecar/routes/soul.d.ts.map +1 -0
- package/dist/sidecar/routes/soul.js +115 -0
- package/dist/sidecar/routes/soul.js.map +1 -0
- package/dist/sidecar/routes/team.d.ts +4 -0
- package/dist/sidecar/routes/team.d.ts.map +1 -0
- package/dist/sidecar/routes/team.js +139 -0
- package/dist/sidecar/routes/team.js.map +1 -0
- package/dist/sidecar/routes/update.d.ts +4 -0
- package/dist/sidecar/routes/update.d.ts.map +1 -0
- package/dist/sidecar/routes/update.js +96 -0
- package/dist/sidecar/routes/update.js.map +1 -0
- package/dist/sidecar/utils.d.ts +9 -0
- package/dist/sidecar/utils.d.ts.map +1 -0
- package/dist/sidecar/utils.js +57 -0
- package/dist/sidecar/utils.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getTodayStats = getTodayStats;
|
|
4
|
+
exports.getWeeklyStats = getWeeklyStats;
|
|
5
|
+
exports.getMonthlyStats = getMonthlyStats;
|
|
6
|
+
exports.getActiveSessions = getActiveSessions;
|
|
7
|
+
exports.getRecentSessions = getRecentSessions;
|
|
8
|
+
exports.getSessionEvents = getSessionEvents;
|
|
9
|
+
exports.getTopSkillsByCost = getTopSkillsByCost;
|
|
10
|
+
exports.getModelDistribution = getModelDistribution;
|
|
11
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
+
function emptyDayStats(date) {
|
|
13
|
+
return {
|
|
14
|
+
date,
|
|
15
|
+
messages: 0,
|
|
16
|
+
tokens_in: 0,
|
|
17
|
+
tokens_out: 0,
|
|
18
|
+
cost: 0,
|
|
19
|
+
tool_calls: 0,
|
|
20
|
+
sessions: 0,
|
|
21
|
+
errors: 0,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Converts flat daily_stats rows into typed DayStats objects.
|
|
26
|
+
*/
|
|
27
|
+
function rowsToDayStats(rows) {
|
|
28
|
+
const byDate = new Map();
|
|
29
|
+
const NUMERIC_METRICS = new Set([
|
|
30
|
+
"messages",
|
|
31
|
+
"tokens_in",
|
|
32
|
+
"tokens_out",
|
|
33
|
+
"cost",
|
|
34
|
+
"tool_calls",
|
|
35
|
+
"sessions",
|
|
36
|
+
"errors",
|
|
37
|
+
]);
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
if (!byDate.has(row.date)) {
|
|
40
|
+
byDate.set(row.date, emptyDayStats(row.date));
|
|
41
|
+
}
|
|
42
|
+
const entry = byDate.get(row.date);
|
|
43
|
+
if (NUMERIC_METRICS.has(row.metric)) {
|
|
44
|
+
const metric = row.metric;
|
|
45
|
+
entry[metric] += row.value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return byDate;
|
|
49
|
+
}
|
|
50
|
+
function todayString() {
|
|
51
|
+
return new Date().toISOString().slice(0, 10);
|
|
52
|
+
}
|
|
53
|
+
// ─── Queries ──────────────────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Returns aggregated stats for today.
|
|
56
|
+
*/
|
|
57
|
+
function getTodayStats(db) {
|
|
58
|
+
const today = todayString();
|
|
59
|
+
const rows = db
|
|
60
|
+
.prepare("SELECT date, metric, value FROM daily_stats WHERE date = ?")
|
|
61
|
+
.all(today);
|
|
62
|
+
const map = rowsToDayStats(rows);
|
|
63
|
+
return map.get(today) ?? emptyDayStats(today);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns aggregated stats for each of the last 7 days.
|
|
67
|
+
*/
|
|
68
|
+
function getWeeklyStats(db) {
|
|
69
|
+
const rows = db
|
|
70
|
+
.prepare(`SELECT date, metric, value FROM daily_stats
|
|
71
|
+
WHERE date >= date('now', '-6 days')
|
|
72
|
+
ORDER BY date ASC`)
|
|
73
|
+
.all();
|
|
74
|
+
const map = rowsToDayStats(rows);
|
|
75
|
+
// Ensure all 7 days are present (fill gaps with zeros)
|
|
76
|
+
const result = [];
|
|
77
|
+
for (let i = 6; i >= 0; i--) {
|
|
78
|
+
const d = new Date();
|
|
79
|
+
d.setDate(d.getDate() - i);
|
|
80
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
81
|
+
result.push(map.get(dateStr) ?? emptyDayStats(dateStr));
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns aggregated stats for each of the last 30 days.
|
|
87
|
+
*/
|
|
88
|
+
function getMonthlyStats(db) {
|
|
89
|
+
const rows = db
|
|
90
|
+
.prepare(`SELECT date, metric, value FROM daily_stats
|
|
91
|
+
WHERE date >= date('now', '-29 days')
|
|
92
|
+
ORDER BY date ASC`)
|
|
93
|
+
.all();
|
|
94
|
+
const map = rowsToDayStats(rows);
|
|
95
|
+
const result = [];
|
|
96
|
+
for (let i = 29; i >= 0; i--) {
|
|
97
|
+
const d = new Date();
|
|
98
|
+
d.setDate(d.getDate() - i);
|
|
99
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
100
|
+
result.push(map.get(dateStr) ?? emptyDayStats(dateStr));
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns sessions currently in 'active' status.
|
|
106
|
+
*/
|
|
107
|
+
function getActiveSessions(db) {
|
|
108
|
+
return db
|
|
109
|
+
.prepare(`SELECT * FROM sessions WHERE status = 'active' ORDER BY started_at DESC`)
|
|
110
|
+
.all();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Returns the most recent sessions (default: 20).
|
|
114
|
+
*/
|
|
115
|
+
function getRecentSessions(db, limit = 20) {
|
|
116
|
+
return db
|
|
117
|
+
.prepare(`SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?`)
|
|
118
|
+
.all(limit);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns all events for a given session, ordered chronologically.
|
|
122
|
+
*/
|
|
123
|
+
function getSessionEvents(db, sessionId) {
|
|
124
|
+
return db
|
|
125
|
+
.prepare(`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`)
|
|
126
|
+
.all(sessionId);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Returns the top skills ranked by cost over the last N days.
|
|
130
|
+
* Skill name is extracted from the tool_call event's data.tool field.
|
|
131
|
+
*/
|
|
132
|
+
function getTopSkillsByCost(db, days = 30) {
|
|
133
|
+
// We join events (tool_calls) with their parent session cost.
|
|
134
|
+
// For simplicity in V1 we use the tool name from events and
|
|
135
|
+
// the session total_cost proportionally — a direct approach is cleaner:
|
|
136
|
+
// extract cost from llm_call events that follow tool_call events.
|
|
137
|
+
// Here we aggregate llm_call events grouped by tool calls within same session.
|
|
138
|
+
const cutoff = new Date();
|
|
139
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
140
|
+
const cutoffStr = cutoff.toISOString().slice(0, 19);
|
|
141
|
+
const rows = db
|
|
142
|
+
.prepare(`SELECT
|
|
143
|
+
json_extract(data, '$.tool') AS tool_name,
|
|
144
|
+
COUNT(*) AS calls
|
|
145
|
+
FROM events
|
|
146
|
+
WHERE type = 'tool_call'
|
|
147
|
+
AND timestamp >= ?
|
|
148
|
+
AND json_extract(data, '$.tool') IS NOT NULL
|
|
149
|
+
GROUP BY tool_name
|
|
150
|
+
ORDER BY calls DESC
|
|
151
|
+
LIMIT 20`)
|
|
152
|
+
.all(cutoffStr);
|
|
153
|
+
// For V1 cost is estimated as 0 since we don't track per-skill cost precisely.
|
|
154
|
+
// This will be improved in V2 with skill attribution.
|
|
155
|
+
return rows.map((r) => ({
|
|
156
|
+
skill: r.tool_name,
|
|
157
|
+
cost: 0,
|
|
158
|
+
calls: r.calls,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns model distribution (call count and total cost) over the last N days.
|
|
163
|
+
*/
|
|
164
|
+
function getModelDistribution(db, days = 30) {
|
|
165
|
+
const cutoff = new Date();
|
|
166
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
167
|
+
const cutoffStr = cutoff.toISOString().slice(0, 19);
|
|
168
|
+
return db
|
|
169
|
+
.prepare(`SELECT
|
|
170
|
+
json_extract(data, '$.model') AS model,
|
|
171
|
+
COUNT(*) AS count,
|
|
172
|
+
SUM(CAST(json_extract(data, '$.cost') AS REAL)) AS cost
|
|
173
|
+
FROM events
|
|
174
|
+
WHERE type = 'llm_call'
|
|
175
|
+
AND timestamp >= ?
|
|
176
|
+
AND json_extract(data, '$.model') IS NOT NULL
|
|
177
|
+
GROUP BY model
|
|
178
|
+
ORDER BY count DESC`)
|
|
179
|
+
.all(cutoffStr);
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/observability/queries.ts"],"names":[],"mappings":";;AA6EA,sCAWC;AAKD,wCAqBC;AAKD,0CAoBC;AAKD,8CAMC;AAKD,8CASC;AAKD,4CASC;AAMD,gDA2CC;AAKD,oDA4BC;AApPD,iFAAiF;AAEjF,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;QACb,IAAI,EAAE,CAAC;QACP,UAAU,EAAE,CAAC;QACb,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,CAAC;KACV,CAAC;AACJ,CAAC;AAQD;;GAEG;AACH,SAAS,cAAc,CAAC,IAAoB;IAC1C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE3C,MAAM,eAAe,GAAG,IAAI,GAAG,CAAS;QACtC,UAAU;QACV,WAAW;QACX,YAAY;QACZ,MAAM;QACN,YAAY;QACZ,UAAU;QACV,QAAQ;KACT,CAAC,CAAC;IAEH,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAEpC,IAAI,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAsC,CAAC;YACzD,KAAK,CAAC,MAAM,CAAY,IAAI,GAAG,CAAC,KAAK,CAAC;QACzC,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,iFAAiF;AAEjF;;GAEG;AACH,SAAgB,aAAa,CAAC,EAAqB;IACjD,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;IAE5B,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN,4DAA4D,CAC7D;SACA,GAAG,CAAC,KAAK,CAAC,CAAC;IAEd,MAAM,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc,CAAC,EAAqB;IAClD,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;yBAEmB,CACpB;SACA,GAAG,EAAE,CAAC;IAET,MAAM,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAEjC,uDAAuD;IACvD,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,eAAe,CAAC,EAAqB;IACnD,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;yBAEmB,CACpB;SACA,GAAG,EAAE,CAAC;IAET,MAAM,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAEjC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;QACrB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAAC,EAAqB;IACrD,OAAO,EAAE;SACN,OAAO,CACN,yEAAyE,CAC1E;SACA,GAAG,EAAE,CAAC;AACX,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAC/B,EAAqB,EACrB,KAAK,GAAG,EAAE;IAEV,OAAO,EAAE;SACN,OAAO,CACN,yDAAyD,CAC1D;SACA,GAAG,CAAC,KAAK,CAAC,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB,CAC9B,EAAqB,EACrB,SAAiB;IAEjB,OAAO,EAAE;SACN,OAAO,CACN,kEAAkE,CACnE;SACA,GAAG,CAAC,SAAS,CAAC,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAgB,kBAAkB,CAChC,EAAqB,EACrB,IAAI,GAAG,EAAE;IAET,8DAA8D;IAC9D,4DAA4D;IAC5D,wEAAwE;IACxE,kEAAkE;IAClE,+EAA+E;IAS/E,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEpD,MAAM,IAAI,GAAG,EAAE;SACZ,OAAO,CACN;;;;;;;;;gBASU,CACX;SACA,GAAG,CAAC,SAAS,CAAC,CAAC;IAElB,+EAA+E;IAC/E,sDAAsD;IACtD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtB,KAAK,EAAE,CAAC,CAAC,SAAS;QAClB,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;GAEG;AACH,SAAgB,oBAAoB,CAClC,EAAqB,EACrB,IAAI,GAAG,EAAE;IAQT,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEpD,OAAO,EAAE;SACN,OAAO,CACN;;;;;;;;;2BASqB,CACtB;SACA,GAAG,CAAC,SAAS,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ChatMessage } from "./types";
|
|
2
|
+
export type Complexity = "low" | "medium" | "high";
|
|
3
|
+
/**
|
|
4
|
+
* Classifies request complexity in <5ms using pure heuristics.
|
|
5
|
+
* No LLM call is made — this runs synchronously.
|
|
6
|
+
*/
|
|
7
|
+
export declare function classifyComplexity(messages: ChatMessage[]): Complexity;
|
|
8
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/router/classifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAoDnD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,UAAU,CA0CtE"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyComplexity = classifyComplexity;
|
|
4
|
+
const HIGH_COMPLEXITY_KEYWORDS = [
|
|
5
|
+
"analyze",
|
|
6
|
+
"analyse",
|
|
7
|
+
"compare",
|
|
8
|
+
"summarize long",
|
|
9
|
+
"summarise long",
|
|
10
|
+
"write essay",
|
|
11
|
+
"detailed report",
|
|
12
|
+
"comprehensive",
|
|
13
|
+
"in-depth",
|
|
14
|
+
"thorough",
|
|
15
|
+
"critique",
|
|
16
|
+
"evaluate",
|
|
17
|
+
];
|
|
18
|
+
const MEDIUM_COMPLEXITY_KEYWORDS = [
|
|
19
|
+
"explain",
|
|
20
|
+
"describe",
|
|
21
|
+
"summarize",
|
|
22
|
+
"summarise",
|
|
23
|
+
"list",
|
|
24
|
+
"outline",
|
|
25
|
+
"translate",
|
|
26
|
+
];
|
|
27
|
+
function totalContentLength(messages) {
|
|
28
|
+
return messages.reduce((sum, msg) => sum + (msg.content?.length ?? 0), 0);
|
|
29
|
+
}
|
|
30
|
+
function hasToolCalls(messages) {
|
|
31
|
+
return messages.some((msg) => msg.tool_calls && msg.tool_calls.length > 0);
|
|
32
|
+
}
|
|
33
|
+
function systemPromptLength(messages) {
|
|
34
|
+
const sys = messages.find((m) => m.role === "system");
|
|
35
|
+
return sys?.content?.length ?? 0;
|
|
36
|
+
}
|
|
37
|
+
function matchesKeywords(text, keywords) {
|
|
38
|
+
const lower = text.toLowerCase();
|
|
39
|
+
return keywords.some((kw) => lower.includes(kw));
|
|
40
|
+
}
|
|
41
|
+
function lastUserMessage(messages) {
|
|
42
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
43
|
+
return userMessages[userMessages.length - 1]?.content ?? "";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Classifies request complexity in <5ms using pure heuristics.
|
|
47
|
+
* No LLM call is made — this runs synchronously.
|
|
48
|
+
*/
|
|
49
|
+
function classifyComplexity(messages) {
|
|
50
|
+
let score = 0;
|
|
51
|
+
const total = totalContentLength(messages);
|
|
52
|
+
const conversationLength = messages.length;
|
|
53
|
+
const sysLen = systemPromptLength(messages);
|
|
54
|
+
const lastUser = lastUserMessage(messages);
|
|
55
|
+
// Content length scoring
|
|
56
|
+
if (total > 1000) {
|
|
57
|
+
score += 2;
|
|
58
|
+
}
|
|
59
|
+
else if (total > 200) {
|
|
60
|
+
score += 1;
|
|
61
|
+
}
|
|
62
|
+
// Tool calls bump complexity
|
|
63
|
+
if (hasToolCalls(messages)) {
|
|
64
|
+
score += 1;
|
|
65
|
+
}
|
|
66
|
+
// System prompt length factor
|
|
67
|
+
if (sysLen > 500) {
|
|
68
|
+
score += 1;
|
|
69
|
+
}
|
|
70
|
+
// Conversation depth factor
|
|
71
|
+
if (conversationLength > 10) {
|
|
72
|
+
score += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (conversationLength > 5) {
|
|
75
|
+
score += 0;
|
|
76
|
+
}
|
|
77
|
+
// High complexity keywords in last user message
|
|
78
|
+
if (matchesKeywords(lastUser, HIGH_COMPLEXITY_KEYWORDS)) {
|
|
79
|
+
score += 2;
|
|
80
|
+
}
|
|
81
|
+
else if (matchesKeywords(lastUser, MEDIUM_COMPLEXITY_KEYWORDS)) {
|
|
82
|
+
score += 1;
|
|
83
|
+
}
|
|
84
|
+
if (score >= 3)
|
|
85
|
+
return "high";
|
|
86
|
+
if (score >= 1)
|
|
87
|
+
return "medium";
|
|
88
|
+
return "low";
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/router/classifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ModelConfig, ProviderConfig } from "../config";
|
|
2
|
+
import { ChatCompletionRequest, ChatCompletionResponse } from "./types";
|
|
3
|
+
interface ProviderAndModel {
|
|
4
|
+
provider: ProviderConfig;
|
|
5
|
+
model: ModelConfig;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Forwards a request to the primary provider/model with automatic fallback
|
|
9
|
+
* to equivalent-tier models on other providers if the primary fails.
|
|
10
|
+
*
|
|
11
|
+
* Returns the response and a flag indicating whether a fallback was used.
|
|
12
|
+
*/
|
|
13
|
+
export declare function forwardWithFallback(request: ChatCompletionRequest, primary: ProviderAndModel, providers: ProviderConfig[]): Promise<{
|
|
14
|
+
response: ChatCompletionResponse;
|
|
15
|
+
usedFallback: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=fallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback.d.ts","sourceRoot":"","sources":["../../src/router/fallback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGxD,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAKxE,UAAU,gBAAgB;IACxB,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,WAAW,CAAC;CACpB;AA+ED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,qBAAqB,EAC9B,OAAO,EAAE,gBAAgB,EACzB,SAAS,EAAE,cAAc,EAAE,GAC1B,OAAO,CAAC;IAAE,QAAQ,EAAE,sBAAsB,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,CA8CtE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forwardWithFallback = forwardWithFallback;
|
|
4
|
+
const models_1 = require("./models");
|
|
5
|
+
const providers_1 = require("./providers");
|
|
6
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
8
|
+
/**
|
|
9
|
+
* Determines if the error message suggests a retryable condition
|
|
10
|
+
* (rate limit, timeout, 5xx server error).
|
|
11
|
+
*/
|
|
12
|
+
function isRetryableError(err) {
|
|
13
|
+
if (!(err instanceof Error))
|
|
14
|
+
return false;
|
|
15
|
+
const msg = err.message.toLowerCase();
|
|
16
|
+
// Check for status code patterns embedded in error messages
|
|
17
|
+
for (const code of RETRYABLE_STATUS_CODES) {
|
|
18
|
+
if (msg.includes(`${code}`))
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return (msg.includes("timeout") ||
|
|
22
|
+
msg.includes("rate limit") ||
|
|
23
|
+
msg.includes("overloaded") ||
|
|
24
|
+
msg.includes("econnreset") ||
|
|
25
|
+
msg.includes("econnrefused") ||
|
|
26
|
+
msg.includes("network"));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps a fetch call with a timeout.
|
|
30
|
+
*/
|
|
31
|
+
async function withTimeout(promise, ms) {
|
|
32
|
+
let timeoutId;
|
|
33
|
+
const timeout = new Promise((_, reject) => {
|
|
34
|
+
timeoutId = setTimeout(() => {
|
|
35
|
+
reject(new Error(`Request timed out after ${ms}ms`));
|
|
36
|
+
}, ms);
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const result = await Promise.race([promise, timeout]);
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
clearTimeout(timeoutId);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Builds a list of fallback candidates with the same tier as the primary model,
|
|
50
|
+
* excluding the primary provider itself.
|
|
51
|
+
*/
|
|
52
|
+
function buildFallbackCandidates(primary, providers) {
|
|
53
|
+
const candidates = [];
|
|
54
|
+
const targetTier = primary.model.tier;
|
|
55
|
+
for (const provider of providers) {
|
|
56
|
+
if (!provider.enabled)
|
|
57
|
+
continue;
|
|
58
|
+
if (provider.name === primary.provider.name)
|
|
59
|
+
continue;
|
|
60
|
+
const registryModels = models_1.MODELS_BY_PROVIDER.get(provider.name) ?? [];
|
|
61
|
+
const availableIds = new Set(provider.models.map((m) => m.id));
|
|
62
|
+
const eligible = registryModels.filter((m) => availableIds.has(m.id) && m.tier === targetTier);
|
|
63
|
+
if (eligible.length > 0) {
|
|
64
|
+
candidates.push({ provider, model: eligible[0] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return candidates;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Forwards a request to the primary provider/model with automatic fallback
|
|
71
|
+
* to equivalent-tier models on other providers if the primary fails.
|
|
72
|
+
*
|
|
73
|
+
* Returns the response and a flag indicating whether a fallback was used.
|
|
74
|
+
*/
|
|
75
|
+
async function forwardWithFallback(request, primary, providers) {
|
|
76
|
+
// Attempt primary
|
|
77
|
+
try {
|
|
78
|
+
const response = await withTimeout((0, providers_1.forwardToProvider)(primary.provider, primary.model, request), REQUEST_TIMEOUT_MS);
|
|
79
|
+
return { response, usedFallback: false };
|
|
80
|
+
}
|
|
81
|
+
catch (primaryErr) {
|
|
82
|
+
if (!isRetryableError(primaryErr)) {
|
|
83
|
+
// Non-retryable error — surface immediately
|
|
84
|
+
throw primaryErr;
|
|
85
|
+
}
|
|
86
|
+
console.warn(`[router/fallback] Primary provider "${primary.provider.name}/${primary.model.id}" failed: ${primaryErr.message}. Attempting fallbacks.`);
|
|
87
|
+
}
|
|
88
|
+
// Attempt fallbacks in order
|
|
89
|
+
const fallbacks = buildFallbackCandidates(primary, providers);
|
|
90
|
+
for (const candidate of fallbacks) {
|
|
91
|
+
try {
|
|
92
|
+
console.info(`[router/fallback] Trying ${candidate.provider.name}/${candidate.model.id}`);
|
|
93
|
+
const response = await withTimeout((0, providers_1.forwardToProvider)(candidate.provider, candidate.model, request), REQUEST_TIMEOUT_MS);
|
|
94
|
+
return { response, usedFallback: true };
|
|
95
|
+
}
|
|
96
|
+
catch (fallbackErr) {
|
|
97
|
+
console.warn(`[router/fallback] Fallback "${candidate.provider.name}/${candidate.model.id}" also failed: ${fallbackErr.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`All providers failed for tier "${primary.model.tier}". No fallback succeeded.`);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=fallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/router/fallback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAc,UAAU,EAAE,MAAM,WAAW,CAAC;AAwKnD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,EAAE,EAAE,QAAQ,CAAC,QAAQ,GACpB,IAAI,CAAC,MAAM,CA6Bb"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createRouterServer = createRouterServer;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const config_1 = require("../config");
|
|
39
|
+
const classifier_1 = require("./classifier");
|
|
40
|
+
const strategy_1 = require("./strategy");
|
|
41
|
+
const fallback_1 = require("./fallback");
|
|
42
|
+
const logger_1 = require("./logger");
|
|
43
|
+
const leak_detector_1 = require("./leak-detector");
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Helpers
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
function readBody(req) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
51
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
52
|
+
req.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function sendJson(res, status, body) {
|
|
56
|
+
const json = JSON.stringify(body);
|
|
57
|
+
res.writeHead(status, {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"Content-Length": Buffer.byteLength(json),
|
|
60
|
+
});
|
|
61
|
+
res.end(json);
|
|
62
|
+
}
|
|
63
|
+
function sendError(res, status, message) {
|
|
64
|
+
sendJson(res, status, {
|
|
65
|
+
error: { message, type: "router_error", code: status },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Request handler
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
async function handleChatCompletions(req, res, config, db) {
|
|
72
|
+
const startMs = Date.now();
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = await readBody(req);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return sendError(res, 400, "Failed to read request body");
|
|
79
|
+
}
|
|
80
|
+
let request;
|
|
81
|
+
try {
|
|
82
|
+
request = JSON.parse(body);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return sendError(res, 400, "Invalid JSON body");
|
|
86
|
+
}
|
|
87
|
+
if (!request.messages || !Array.isArray(request.messages)) {
|
|
88
|
+
return sendError(res, 400, "Missing or invalid 'messages' field");
|
|
89
|
+
}
|
|
90
|
+
// Classify complexity (synchronous, <5ms)
|
|
91
|
+
const complexity = (0, classifier_1.classifyComplexity)(request.messages);
|
|
92
|
+
// Select the best model for this request
|
|
93
|
+
let selected;
|
|
94
|
+
try {
|
|
95
|
+
selected = (0, strategy_1.selectModel)(complexity, config.strategy, config.providers);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error("[router] Model selection failed:", err);
|
|
99
|
+
return sendError(res, 503, err.message);
|
|
100
|
+
}
|
|
101
|
+
// Forward with fallback
|
|
102
|
+
let chatResponse;
|
|
103
|
+
try {
|
|
104
|
+
chatResponse = await (0, fallback_1.forwardWithFallback)(request, selected, config.providers);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const latencyMs = Date.now() - startMs;
|
|
108
|
+
(0, logger_1.logRouterRequest)(db, {
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
model: selected.model.id,
|
|
111
|
+
provider: selected.provider.name,
|
|
112
|
+
inputTokens: 0,
|
|
113
|
+
outputTokens: 0,
|
|
114
|
+
cost: 0,
|
|
115
|
+
latencyMs,
|
|
116
|
+
complexity,
|
|
117
|
+
strategy: config.strategy,
|
|
118
|
+
fallback: false,
|
|
119
|
+
success: false,
|
|
120
|
+
error: err.message,
|
|
121
|
+
});
|
|
122
|
+
console.error("[router] All providers failed:", err);
|
|
123
|
+
return sendError(res, 502, err.message);
|
|
124
|
+
}
|
|
125
|
+
const latencyMs = Date.now() - startMs;
|
|
126
|
+
const { response, usedFallback } = chatResponse;
|
|
127
|
+
const inputTokens = response.usage?.prompt_tokens ?? 0;
|
|
128
|
+
const outputTokens = response.usage?.completion_tokens ?? 0;
|
|
129
|
+
const cost = (0, logger_1.calculateCost)(inputTokens, outputTokens, selected.model.inputPrice, selected.model.outputPrice);
|
|
130
|
+
// Leak detection — scan response content before sending to client
|
|
131
|
+
let leakDetected = false;
|
|
132
|
+
if (response.choices) {
|
|
133
|
+
for (const choice of response.choices) {
|
|
134
|
+
const content = choice.message?.content;
|
|
135
|
+
if (typeof content === "string") {
|
|
136
|
+
const result = (0, leak_detector_1.detectLeaks)(content);
|
|
137
|
+
if (result.leaked) {
|
|
138
|
+
leakDetected = true;
|
|
139
|
+
console.warn(`[router] Leak detected in response (model=${selected.model.id}): ${result.findings.length} finding(s)`, result.findings.map((f) => `${f.type}: ${f.match}`));
|
|
140
|
+
choice.message.content = (0, leak_detector_1.sanitizeResponse)(content);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Persist log (synchronous — non-blocking on the happy path since it's fast)
|
|
146
|
+
(0, logger_1.logRouterRequest)(db, {
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
model: selected.model.id,
|
|
149
|
+
provider: selected.provider.name,
|
|
150
|
+
inputTokens,
|
|
151
|
+
outputTokens,
|
|
152
|
+
cost,
|
|
153
|
+
latencyMs,
|
|
154
|
+
complexity,
|
|
155
|
+
strategy: config.strategy,
|
|
156
|
+
fallback: usedFallback,
|
|
157
|
+
success: true,
|
|
158
|
+
leakDetected,
|
|
159
|
+
});
|
|
160
|
+
sendJson(res, 200, response);
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Server factory
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
function createRouterServer(config, db) {
|
|
166
|
+
const server = http.createServer(async (req, res) => {
|
|
167
|
+
const method = req.method?.toUpperCase();
|
|
168
|
+
const url = req.url ?? "/";
|
|
169
|
+
// Health check
|
|
170
|
+
if (method === "GET" && url === "/health") {
|
|
171
|
+
return sendJson(res, 200, {
|
|
172
|
+
status: "ok",
|
|
173
|
+
version: "0.1.0",
|
|
174
|
+
strategy: config.strategy,
|
|
175
|
+
providers: config.providers
|
|
176
|
+
.filter((p) => p.enabled)
|
|
177
|
+
.map((p) => p.name),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Chat completions — OpenAI-compatible endpoint
|
|
181
|
+
if (method === "POST" && url === "/v1/chat/completions") {
|
|
182
|
+
return handleChatCompletions(req, res, config, db);
|
|
183
|
+
}
|
|
184
|
+
// 404 for everything else
|
|
185
|
+
return sendError(res, 404, `Route not found: ${method} ${url}`);
|
|
186
|
+
});
|
|
187
|
+
return server;
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Entry point (when run directly)
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
if (require.main === module) {
|
|
193
|
+
const config = (0, config_1.loadConfig)();
|
|
194
|
+
const db = (0, logger_1.openDatabase)(config.dataDir);
|
|
195
|
+
const server = createRouterServer(config, db);
|
|
196
|
+
server.listen(config.routerPort, () => {
|
|
197
|
+
console.log(`[router] Claw Router Proxy listening on port ${config.routerPort}`);
|
|
198
|
+
console.log(`[router] Strategy: ${config.strategy} | Providers: ${config.providers
|
|
199
|
+
.filter((p) => p.enabled)
|
|
200
|
+
.map((p) => p.name)
|
|
201
|
+
.join(", ") || "none"}`);
|
|
202
|
+
});
|
|
203
|
+
process.on("SIGTERM", () => {
|
|
204
|
+
console.log("[router] Shutting down...");
|
|
205
|
+
db.close();
|
|
206
|
+
server.close(() => process.exit(0));
|
|
207
|
+
});
|
|
208
|
+
process.on("SIGINT", () => {
|
|
209
|
+
console.log("[router] Interrupted, shutting down...");
|
|
210
|
+
db.close();
|
|
211
|
+
server.close(() => process.exit(0));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=index.js.map
|