lynkr 8.0.0 → 9.0.1
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/.lynkr/telemetry.db +0 -0
- package/.lynkr/telemetry.db-shm +0 -0
- package/.lynkr/telemetry.db-wal +0 -0
- package/README.md +196 -322
- package/lynkr-skill.tar.gz +0 -0
- package/package.json +4 -3
- package/src/api/openai-router.js +64 -13
- package/src/api/providers-handler.js +171 -3
- package/src/api/router.js +9 -2
- package/src/clients/circuit-breaker.js +10 -247
- package/src/clients/codex-process.js +342 -0
- package/src/clients/codex-utils.js +143 -0
- package/src/clients/databricks.js +210 -63
- package/src/clients/resilience.js +540 -0
- package/src/clients/retry.js +22 -167
- package/src/clients/standard-tools.js +23 -0
- package/src/config/index.js +77 -0
- package/src/context/compression.js +42 -9
- package/src/context/distill.js +492 -0
- package/src/orchestrator/index.js +48 -8
- package/src/routing/complexity-analyzer.js +258 -5
- package/src/routing/index.js +12 -2
- package/src/routing/latency-tracker.js +148 -0
- package/src/routing/model-tiers.js +2 -0
- package/src/routing/quality-scorer.js +113 -0
- package/src/routing/telemetry.js +464 -0
- package/src/server.js +13 -12
- package/src/tools/code-graph.js +538 -0
- package/src/tools/code-mode.js +304 -0
- package/src/tools/index.js +4 -0
- package/src/tools/lazy-loader.js +18 -0
- package/src/tools/mcp-remote.js +7 -0
- package/src/tools/smart-selection.js +11 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -0
- package/src/utils/payload.js +206 -0
- package/src/utils/perf-timer.js +80 -0
- package/.github/FUNDING.yml +0 -15
- package/.github/workflows/README.md +0 -215
- package/.github/workflows/ci.yml +0 -69
- package/.github/workflows/index.yml +0 -62
- package/.github/workflows/web-tools-tests.yml +0 -56
- package/CITATIONS.bib +0 -6
- package/DEPLOYMENT.md +0 -1001
- package/LYNKR-TUI-PLAN.md +0 -984
- package/PERFORMANCE-REPORT.md +0 -866
- package/PLAN-per-client-model-routing.md +0 -252
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -198
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -584
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/docs/toon-integration-spec.md +0 -130
- package/documentation/README.md +0 -101
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -679
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -734
- package/documentation/docker.md +0 -874
- package/documentation/embeddings.md +0 -762
- package/documentation/faq.md +0 -713
- package/documentation/features.md +0 -403
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -758
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -636
- package/documentation/providers.md +0 -1009
- package/documentation/routing.md +0 -476
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -325
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -969
- package/final-test.js +0 -33
- package/headroom-sidecar/config.py +0 -93
- package/headroom-sidecar/requirements.txt +0 -14
- package/headroom-sidecar/server.py +0 -451
- package/monitor-agents.sh +0 -31
- package/scripts/audit-log-reader.js +0 -399
- package/scripts/compact-dictionary.js +0 -204
- package/scripts/test-deduplication.js +0 -448
- package/src/db/database.sqlite +0 -0
- package/te +0 -11622
- package/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -213
- package/test/azure-openai-error-resilience.test.js +0 -238
- package/test/azure-openai-format-conversion.test.js +0 -354
- package/test/azure-openai-integration.test.js +0 -287
- package/test/azure-openai-routing.test.js +0 -175
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -457
- package/test/comprehensive-test-suite.js +0 -928
- package/test/config-validation.test.js +0 -207
- package/test/cursor-integration.test.js +0 -484
- package/test/format-conversion.test.js +0 -578
- package/test/hybrid-routing-integration.test.js +0 -269
- package/test/hybrid-routing-performance.test.js +0 -428
- package/test/llamacpp-integration.test.js +0 -882
- package/test/lmstudio-integration.test.js +0 -347
- package/test/memory/extractor.test.js +0 -398
- package/test/memory/retriever.test.js +0 -613
- package/test/memory/retriever.test.js.bak +0 -585
- package/test/memory/search.test.js +0 -537
- package/test/memory/search.test.js.bak +0 -389
- package/test/memory/store.test.js +0 -344
- package/test/memory/store.test.js.bak +0 -312
- package/test/memory/surprise.test.js +0 -300
- package/test/memory-performance.test.js +0 -472
- package/test/openai-integration.test.js +0 -683
- package/test/openrouter-error-resilience.test.js +0 -418
- package/test/passthrough-mode.test.js +0 -385
- package/test/performance-benchmark.js +0 -351
- package/test/performance-tests.js +0 -528
- package/test/routing.test.js +0 -225
- package/test/toon-compression.test.js +0 -131
- package/test/web-tools.test.js +0 -329
- package/test-agents-simple.js +0 -43
- package/test-cli-connection.sh +0 -33
- package/test-learning-unit.js +0 -126
- package/test-learning.js +0 -112
- package/test-parallel-agents.sh +0 -124
- package/test-parallel-direct.js +0 -155
- package/test-subagents.sh +0 -117
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing Telemetry Module
|
|
3
|
+
*
|
|
4
|
+
* Persists per-request routing telemetry into a dedicated SQLite database
|
|
5
|
+
* at .lynkr/telemetry.db. Provides query helpers for dashboards, accuracy
|
|
6
|
+
* analysis, and automated routing feedback loops.
|
|
7
|
+
*
|
|
8
|
+
* Uses lazy initialisation so the proxy starts even when better-sqlite3 is
|
|
9
|
+
* not installed (it is an optionalDependency).
|
|
10
|
+
*
|
|
11
|
+
* @module routing/telemetry
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const logger = require("../logger");
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Lazy database initialisation
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
let Database;
|
|
23
|
+
try {
|
|
24
|
+
Database = require("better-sqlite3");
|
|
25
|
+
} catch {
|
|
26
|
+
Database = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @type {import('better-sqlite3').Database|null} */
|
|
30
|
+
let db = null;
|
|
31
|
+
|
|
32
|
+
/** @type {boolean} */
|
|
33
|
+
let initialised = false;
|
|
34
|
+
|
|
35
|
+
/** Default retention: 30 days */
|
|
36
|
+
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialise the telemetry database (singleton, idempotent).
|
|
40
|
+
* @returns {boolean} true if the DB is usable
|
|
41
|
+
*/
|
|
42
|
+
function init() {
|
|
43
|
+
if (initialised) return db !== null;
|
|
44
|
+
initialised = true;
|
|
45
|
+
|
|
46
|
+
if (!Database) {
|
|
47
|
+
logger.debug("Telemetry: better-sqlite3 not available, telemetry disabled");
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const dbDir = path.resolve(process.cwd(), ".lynkr");
|
|
53
|
+
if (!fs.existsSync(dbDir)) {
|
|
54
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dbPath = path.join(dbDir, "telemetry.db");
|
|
58
|
+
db = new Database(dbPath, {
|
|
59
|
+
verbose: process.env.DEBUG_SQL ? console.log : null,
|
|
60
|
+
fileMustExist: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Performance pragmas (same pattern as src/db/index.js)
|
|
64
|
+
db.pragma("journal_mode = WAL");
|
|
65
|
+
db.pragma("synchronous = NORMAL");
|
|
66
|
+
db.pragma("cache_size = -16000");
|
|
67
|
+
db.pragma("temp_store = MEMORY");
|
|
68
|
+
db.pragma("busy_timeout = 3000");
|
|
69
|
+
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS routing_telemetry (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
request_id TEXT NOT NULL,
|
|
74
|
+
session_id TEXT,
|
|
75
|
+
timestamp INTEGER NOT NULL,
|
|
76
|
+
complexity_score REAL,
|
|
77
|
+
tier TEXT,
|
|
78
|
+
agentic_type TEXT,
|
|
79
|
+
tool_count INTEGER,
|
|
80
|
+
input_tokens INTEGER,
|
|
81
|
+
message_count INTEGER,
|
|
82
|
+
request_type TEXT,
|
|
83
|
+
provider TEXT NOT NULL,
|
|
84
|
+
model TEXT,
|
|
85
|
+
routing_method TEXT,
|
|
86
|
+
was_fallback INTEGER DEFAULT 0,
|
|
87
|
+
output_tokens INTEGER,
|
|
88
|
+
latency_ms INTEGER,
|
|
89
|
+
status_code INTEGER,
|
|
90
|
+
error_type TEXT,
|
|
91
|
+
cost_usd REAL,
|
|
92
|
+
tool_calls_made INTEGER,
|
|
93
|
+
retry_count INTEGER DEFAULT 0,
|
|
94
|
+
circuit_breaker_state TEXT,
|
|
95
|
+
quality_score REAL,
|
|
96
|
+
tokens_per_second REAL,
|
|
97
|
+
cost_efficiency REAL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_provider
|
|
101
|
+
ON routing_telemetry(provider);
|
|
102
|
+
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_tier
|
|
104
|
+
ON routing_telemetry(tier);
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_timestamp
|
|
107
|
+
ON routing_telemetry(timestamp);
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
logger.info({ dbPath }, "Routing telemetry database initialised");
|
|
111
|
+
return true;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.warn({ err: err.message }, "Failed to initialise telemetry database");
|
|
114
|
+
db = null;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Prepared statements (created lazily after init)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/** @type {Map<string, import('better-sqlite3').Statement>} */
|
|
124
|
+
const stmts = new Map();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get or create a prepared statement.
|
|
128
|
+
* @param {string} key
|
|
129
|
+
* @param {string} sql
|
|
130
|
+
* @returns {import('better-sqlite3').Statement|null}
|
|
131
|
+
*/
|
|
132
|
+
function stmt(key, sql) {
|
|
133
|
+
if (!db) return null;
|
|
134
|
+
if (!stmts.has(key)) {
|
|
135
|
+
stmts.set(key, db.prepare(sql));
|
|
136
|
+
}
|
|
137
|
+
return stmts.get(key);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Public API
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Record a telemetry data point. Executes asynchronously via setImmediate
|
|
146
|
+
* so it never blocks the request path.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} data - Telemetry fields (see table schema)
|
|
149
|
+
*/
|
|
150
|
+
function record(data) {
|
|
151
|
+
if (!init()) return;
|
|
152
|
+
|
|
153
|
+
setImmediate(() => {
|
|
154
|
+
try {
|
|
155
|
+
const insert = stmt(
|
|
156
|
+
"insert",
|
|
157
|
+
`INSERT INTO routing_telemetry (
|
|
158
|
+
request_id, session_id, timestamp, complexity_score, tier,
|
|
159
|
+
agentic_type, tool_count, input_tokens, message_count, request_type,
|
|
160
|
+
provider, model, routing_method, was_fallback, output_tokens,
|
|
161
|
+
latency_ms, status_code, error_type, cost_usd, tool_calls_made,
|
|
162
|
+
retry_count, circuit_breaker_state, quality_score, tokens_per_second,
|
|
163
|
+
cost_efficiency
|
|
164
|
+
) VALUES (
|
|
165
|
+
@request_id, @session_id, @timestamp, @complexity_score, @tier,
|
|
166
|
+
@agentic_type, @tool_count, @input_tokens, @message_count, @request_type,
|
|
167
|
+
@provider, @model, @routing_method, @was_fallback, @output_tokens,
|
|
168
|
+
@latency_ms, @status_code, @error_type, @cost_usd, @tool_calls_made,
|
|
169
|
+
@retry_count, @circuit_breaker_state, @quality_score, @tokens_per_second,
|
|
170
|
+
@cost_efficiency
|
|
171
|
+
)`
|
|
172
|
+
);
|
|
173
|
+
if (!insert) return;
|
|
174
|
+
|
|
175
|
+
insert.run({
|
|
176
|
+
request_id: data.request_id ?? null,
|
|
177
|
+
session_id: data.session_id ?? null,
|
|
178
|
+
timestamp: data.timestamp ?? Date.now(),
|
|
179
|
+
complexity_score: data.complexity_score ?? null,
|
|
180
|
+
tier: data.tier ?? null,
|
|
181
|
+
agentic_type: data.agentic_type ?? null,
|
|
182
|
+
tool_count: data.tool_count ?? null,
|
|
183
|
+
input_tokens: data.input_tokens ?? null,
|
|
184
|
+
message_count: data.message_count ?? null,
|
|
185
|
+
request_type: data.request_type ?? null,
|
|
186
|
+
provider: data.provider,
|
|
187
|
+
model: data.model ?? null,
|
|
188
|
+
routing_method: data.routing_method ?? null,
|
|
189
|
+
was_fallback: data.was_fallback ? 1 : 0,
|
|
190
|
+
output_tokens: data.output_tokens ?? null,
|
|
191
|
+
latency_ms: data.latency_ms ?? null,
|
|
192
|
+
status_code: data.status_code ?? null,
|
|
193
|
+
error_type: data.error_type ?? null,
|
|
194
|
+
cost_usd: data.cost_usd ?? null,
|
|
195
|
+
tool_calls_made: data.tool_calls_made ?? null,
|
|
196
|
+
retry_count: data.retry_count ?? 0,
|
|
197
|
+
circuit_breaker_state: data.circuit_breaker_state ?? null,
|
|
198
|
+
quality_score: data.quality_score ?? null,
|
|
199
|
+
tokens_per_second: data.tokens_per_second ?? null,
|
|
200
|
+
cost_efficiency: data.cost_efficiency ?? null,
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
logger.debug({ err: err.message }, "Telemetry record failed");
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Query telemetry records with optional filters.
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} [filters]
|
|
212
|
+
* @param {string} [filters.provider] - Filter by provider name
|
|
213
|
+
* @param {string} [filters.tier] - Filter by tier
|
|
214
|
+
* @param {number} [filters.since] - Only records after this timestamp (ms)
|
|
215
|
+
* @param {number} [filters.limit] - Max rows to return (default 100)
|
|
216
|
+
* @returns {Object[]} Matching telemetry rows
|
|
217
|
+
*/
|
|
218
|
+
function query(filters = {}) {
|
|
219
|
+
if (!init()) return [];
|
|
220
|
+
|
|
221
|
+
const clauses = [];
|
|
222
|
+
const params = {};
|
|
223
|
+
|
|
224
|
+
if (filters.provider) {
|
|
225
|
+
clauses.push("provider = @provider");
|
|
226
|
+
params.provider = filters.provider;
|
|
227
|
+
}
|
|
228
|
+
if (filters.tier) {
|
|
229
|
+
clauses.push("tier = @tier");
|
|
230
|
+
params.tier = filters.tier;
|
|
231
|
+
}
|
|
232
|
+
if (filters.since) {
|
|
233
|
+
clauses.push("timestamp >= @since");
|
|
234
|
+
params.since = filters.since;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
238
|
+
const limit = filters.limit ?? 100;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const sql = `SELECT * FROM routing_telemetry ${where} ORDER BY timestamp DESC LIMIT ${Number(limit)}`;
|
|
242
|
+
return db.prepare(sql).all(params);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
logger.debug({ err: err.message }, "Telemetry query failed");
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get aggregate statistics over a time range.
|
|
251
|
+
*
|
|
252
|
+
* @param {Object} [timeRange]
|
|
253
|
+
* @param {number} [timeRange.since] - Start timestamp (ms). Defaults to 24 hours ago.
|
|
254
|
+
* @param {number} [timeRange.until] - End timestamp (ms). Defaults to now.
|
|
255
|
+
* @returns {Object|null} Aggregated statistics
|
|
256
|
+
*/
|
|
257
|
+
function getStats(timeRange = {}) {
|
|
258
|
+
if (!init()) return null;
|
|
259
|
+
|
|
260
|
+
const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
|
|
261
|
+
const until = timeRange.until ?? Date.now();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Total requests
|
|
265
|
+
const total = db
|
|
266
|
+
.prepare("SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ?")
|
|
267
|
+
.get(since, until);
|
|
268
|
+
|
|
269
|
+
if (!total || total.cnt === 0) return null;
|
|
270
|
+
|
|
271
|
+
// Average latency per provider
|
|
272
|
+
const latencyRows = db
|
|
273
|
+
.prepare(
|
|
274
|
+
`SELECT provider, AVG(latency_ms) as avg_latency, COUNT(*) as cnt
|
|
275
|
+
FROM routing_telemetry
|
|
276
|
+
WHERE timestamp BETWEEN ? AND ? AND latency_ms IS NOT NULL
|
|
277
|
+
GROUP BY provider`
|
|
278
|
+
)
|
|
279
|
+
.all(since, until);
|
|
280
|
+
|
|
281
|
+
const avgLatencyByProvider = {};
|
|
282
|
+
for (const row of latencyRows) {
|
|
283
|
+
avgLatencyByProvider[row.provider] = Math.round(row.avg_latency);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Average quality per tier
|
|
287
|
+
const qualityRows = db
|
|
288
|
+
.prepare(
|
|
289
|
+
`SELECT tier, AVG(quality_score) as avg_quality, COUNT(*) as cnt
|
|
290
|
+
FROM routing_telemetry
|
|
291
|
+
WHERE timestamp BETWEEN ? AND ? AND quality_score IS NOT NULL AND tier IS NOT NULL
|
|
292
|
+
GROUP BY tier`
|
|
293
|
+
)
|
|
294
|
+
.all(since, until);
|
|
295
|
+
|
|
296
|
+
const avgQualityByTier = {};
|
|
297
|
+
for (const row of qualityRows) {
|
|
298
|
+
avgQualityByTier[row.tier] = Math.round(row.avg_quality * 10) / 10;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Error rate
|
|
302
|
+
const errors = db
|
|
303
|
+
.prepare(
|
|
304
|
+
"SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ? AND error_type IS NOT NULL"
|
|
305
|
+
)
|
|
306
|
+
.get(since, until);
|
|
307
|
+
|
|
308
|
+
const errorRate = Math.round((errors.cnt / total.cnt) * 1000) / 10; // one decimal %
|
|
309
|
+
|
|
310
|
+
// Over/under provisioned percentages
|
|
311
|
+
const accuracy = getRoutingAccuracy({ since, until });
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
totalRequests: total.cnt,
|
|
315
|
+
avgLatencyByProvider,
|
|
316
|
+
avgQualityByTier,
|
|
317
|
+
errorRate,
|
|
318
|
+
overProvisionedPct: accuracy ? accuracy.overProvisionedPct : 0,
|
|
319
|
+
underProvisionedPct: accuracy ? accuracy.underProvisionedPct : 0,
|
|
320
|
+
};
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger.debug({ err: err.message }, "Telemetry getStats failed");
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get aggregated statistics for a specific provider.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} provider - Provider name
|
|
331
|
+
* @param {Object} [timeRange]
|
|
332
|
+
* @param {number} [timeRange.since]
|
|
333
|
+
* @param {number} [timeRange.until]
|
|
334
|
+
* @returns {Object|null}
|
|
335
|
+
*/
|
|
336
|
+
function getProviderStats(provider, timeRange = {}) {
|
|
337
|
+
if (!init()) return null;
|
|
338
|
+
|
|
339
|
+
const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
|
|
340
|
+
const until = timeRange.until ?? Date.now();
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const row = db
|
|
344
|
+
.prepare(
|
|
345
|
+
`SELECT
|
|
346
|
+
COUNT(*) as total,
|
|
347
|
+
AVG(latency_ms) as avg_latency,
|
|
348
|
+
AVG(quality_score) as avg_quality,
|
|
349
|
+
AVG(output_tokens) as avg_output_tokens,
|
|
350
|
+
SUM(CASE WHEN error_type IS NOT NULL THEN 1 ELSE 0 END) as errors,
|
|
351
|
+
SUM(CASE WHEN was_fallback = 1 THEN 1 ELSE 0 END) as fallbacks,
|
|
352
|
+
AVG(tokens_per_second) as avg_tps,
|
|
353
|
+
SUM(cost_usd) as total_cost
|
|
354
|
+
FROM routing_telemetry
|
|
355
|
+
WHERE provider = ? AND timestamp BETWEEN ? AND ?`
|
|
356
|
+
)
|
|
357
|
+
.get(provider, since, until);
|
|
358
|
+
|
|
359
|
+
if (!row || row.total === 0) return null;
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
total: row.total,
|
|
363
|
+
avgLatency: row.avg_latency ? Math.round(row.avg_latency) : null,
|
|
364
|
+
avgQuality: row.avg_quality ? Math.round(row.avg_quality * 10) / 10 : null,
|
|
365
|
+
avgOutputTokens: row.avg_output_tokens ? Math.round(row.avg_output_tokens) : null,
|
|
366
|
+
errorRate: Math.round((row.errors / row.total) * 1000) / 10,
|
|
367
|
+
fallbackRate: Math.round((row.fallbacks / row.total) * 1000) / 10,
|
|
368
|
+
avgTokensPerSecond: row.avg_tps ? Math.round(row.avg_tps * 10) / 10 : null,
|
|
369
|
+
totalCost: row.total_cost ? Math.round(row.total_cost * 10000) / 10000 : null,
|
|
370
|
+
};
|
|
371
|
+
} catch (err) {
|
|
372
|
+
logger.debug({ err: err.message }, "Telemetry getProviderStats failed");
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Calculate routing accuracy: percentage of requests that were over- or
|
|
379
|
+
* under-provisioned.
|
|
380
|
+
*
|
|
381
|
+
* Over-provisioned: quality_score > 80 AND output_tokens < 50 on REASONING or COMPLEX tier.
|
|
382
|
+
* Under-provisioned: quality_score < 45 on SIMPLE tier.
|
|
383
|
+
*
|
|
384
|
+
* @param {Object} [timeRange]
|
|
385
|
+
* @param {number} [timeRange.since]
|
|
386
|
+
* @param {number} [timeRange.until]
|
|
387
|
+
* @returns {Object|null}
|
|
388
|
+
*/
|
|
389
|
+
function getRoutingAccuracy(timeRange = {}) {
|
|
390
|
+
if (!init()) return null;
|
|
391
|
+
|
|
392
|
+
const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
|
|
393
|
+
const until = timeRange.until ?? Date.now();
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const total = db
|
|
397
|
+
.prepare("SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ?")
|
|
398
|
+
.get(since, until);
|
|
399
|
+
|
|
400
|
+
if (!total || total.cnt === 0) return null;
|
|
401
|
+
|
|
402
|
+
const overProvisioned = db
|
|
403
|
+
.prepare(
|
|
404
|
+
`SELECT COUNT(*) as cnt FROM routing_telemetry
|
|
405
|
+
WHERE timestamp BETWEEN ? AND ?
|
|
406
|
+
AND quality_score > 80
|
|
407
|
+
AND output_tokens < 50
|
|
408
|
+
AND tier IN ('REASONING', 'COMPLEX')`
|
|
409
|
+
)
|
|
410
|
+
.get(since, until);
|
|
411
|
+
|
|
412
|
+
const underProvisioned = db
|
|
413
|
+
.prepare(
|
|
414
|
+
`SELECT COUNT(*) as cnt FROM routing_telemetry
|
|
415
|
+
WHERE timestamp BETWEEN ? AND ?
|
|
416
|
+
AND quality_score < 45
|
|
417
|
+
AND tier = 'SIMPLE'`
|
|
418
|
+
)
|
|
419
|
+
.get(since, until);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
totalRequests: total.cnt,
|
|
423
|
+
overProvisioned: overProvisioned.cnt,
|
|
424
|
+
underProvisioned: underProvisioned.cnt,
|
|
425
|
+
overProvisionedPct: Math.round((overProvisioned.cnt / total.cnt) * 1000) / 10,
|
|
426
|
+
underProvisionedPct: Math.round((underProvisioned.cnt / total.cnt) * 1000) / 10,
|
|
427
|
+
};
|
|
428
|
+
} catch (err) {
|
|
429
|
+
logger.debug({ err: err.message }, "Telemetry getRoutingAccuracy failed");
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Delete telemetry records older than a given threshold.
|
|
436
|
+
*
|
|
437
|
+
* @param {number} [olderThanMs] - Age threshold in ms. Defaults to 30 days.
|
|
438
|
+
* @returns {number} Number of rows deleted
|
|
439
|
+
*/
|
|
440
|
+
function cleanup(olderThanMs) {
|
|
441
|
+
if (!init()) return 0;
|
|
442
|
+
|
|
443
|
+
const threshold = Date.now() - (olderThanMs ?? DEFAULT_RETENTION_MS);
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const del = stmt("cleanup", "DELETE FROM routing_telemetry WHERE timestamp < ?");
|
|
447
|
+
if (!del) return 0;
|
|
448
|
+
const result = del.run(threshold);
|
|
449
|
+
logger.debug({ deleted: result.changes }, "Telemetry cleanup complete");
|
|
450
|
+
return result.changes;
|
|
451
|
+
} catch (err) {
|
|
452
|
+
logger.debug({ err: err.message }, "Telemetry cleanup failed");
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
module.exports = {
|
|
458
|
+
record,
|
|
459
|
+
query,
|
|
460
|
+
getStats,
|
|
461
|
+
getProviderStats,
|
|
462
|
+
getRoutingAccuracy,
|
|
463
|
+
cleanup,
|
|
464
|
+
};
|
package/src/server.js
CHANGED
|
@@ -78,18 +78,8 @@ function createApp() {
|
|
|
78
78
|
// Metrics collection
|
|
79
79
|
app.use(metricsMiddleware);
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
level: 6, // Balanced compression level
|
|
84
|
-
threshold: 1024, // Only compress responses > 1KB
|
|
85
|
-
filter: (req, res) => {
|
|
86
|
-
// Don't compress event streams
|
|
87
|
-
if (res.getHeader('Content-Type') === 'text/event-stream') {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
return compression.filter(req, res);
|
|
91
|
-
}
|
|
92
|
-
}));
|
|
81
|
+
// Note: If using a tunnel (ngrok, Cloudflare Tunnel) and seeing BrotliDecompressionError,
|
|
82
|
+
// start ngrok with: ngrok http 8081 --request-header-remove "Accept-Encoding"
|
|
93
83
|
|
|
94
84
|
app.use(express.json({ limit: config.server.jsonLimit }));
|
|
95
85
|
app.use(sessionMiddleware);
|
|
@@ -238,6 +228,17 @@ async function start() {
|
|
|
238
228
|
});
|
|
239
229
|
}
|
|
240
230
|
|
|
231
|
+
// Register Codex process shutdown callback
|
|
232
|
+
shutdownManager.onShutdown(async () => {
|
|
233
|
+
try {
|
|
234
|
+
const { getCodexProcess } = require("./clients/codex-process");
|
|
235
|
+
const codex = getCodexProcess();
|
|
236
|
+
if (codex.child) {
|
|
237
|
+
await codex.shutdown();
|
|
238
|
+
}
|
|
239
|
+
} catch { /* ignore if codex never started */ }
|
|
240
|
+
});
|
|
241
|
+
|
|
241
242
|
// Initialize hot reload config watcher
|
|
242
243
|
if (config.hotReload?.enabled !== false) {
|
|
243
244
|
const watcher = initConfigWatcher({
|