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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.1",
|
|
4
4
|
"description": "Self-hosted Claude Code & Cursor proxy with Databricks,AWS BedRock,Azure adapters, openrouter, Ollama,llamacpp,LM Studio, workspace tooling, and MCP integration.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"lynkr-setup": "./scripts/setup.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"prestart": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom up -d headroom 2>/dev/null || echo 'Headroom skipped (disabled or Docker not running)'",
|
|
11
|
+
"prestart": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom up -d --build headroom 2>/dev/null || echo 'Headroom skipped (disabled or Docker not running)'",
|
|
12
12
|
"start": "node index.js 2>&1 | npx pino-pretty --sync",
|
|
13
13
|
"stop": "node -e \"if(process.env.HEADROOM_ENABLED==='true'&&process.env.HEADROOM_DOCKER_ENABLED!=='false'){process.exit(0)}else{process.exit(1)}\" && docker compose --profile headroom down || echo 'Headroom skipped (disabled or Docker not running)'",
|
|
14
14
|
"dev": "nodemon index.js",
|
|
15
15
|
"lint": "eslint src index.js",
|
|
16
16
|
"test": "npm run test:unit && npm run test:performance",
|
|
17
|
-
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
|
|
17
|
+
"test:unit": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/routing.test.js test/hybrid-routing-integration.test.js test/web-tools.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/toon-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/distill.test.js test/large-payload.test.js test/code-mode.test.js",
|
|
18
18
|
"test:memory": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js",
|
|
19
19
|
"test:new-features": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
20
20
|
"test:performance": "DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"@babel/parser": "^7.29.0",
|
|
49
49
|
"@babel/traverse": "^7.29.0",
|
|
50
50
|
"@toon-format/toon": "^2.1.0",
|
|
51
|
+
"cockatiel": "^3.2.1",
|
|
51
52
|
"compression": "^1.7.4",
|
|
52
53
|
"diff": "^5.2.0",
|
|
53
54
|
"dotenv": "^16.4.5",
|
package/src/api/openai-router.js
CHANGED
|
@@ -27,6 +27,23 @@ const { IDE_SAFE_TOOLS } = require("../clients/standard-tools");
|
|
|
27
27
|
|
|
28
28
|
const router = express.Router();
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the model name for OpenAI responses.
|
|
32
|
+
* In OpenClaw mode, returns the actual provider/model from routing metadata.
|
|
33
|
+
*/
|
|
34
|
+
function resolveResponseModel(resultBody, requestModel) {
|
|
35
|
+
if (config.openclaw?.enabled && resultBody?._routingMeta) {
|
|
36
|
+
const meta = resultBody._routingMeta;
|
|
37
|
+
if (meta.provider && meta.model) {
|
|
38
|
+
return `${meta.provider}/${meta.model}`;
|
|
39
|
+
}
|
|
40
|
+
if (meta.provider) {
|
|
41
|
+
return meta.provider;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return requestModel;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Client detection - identifies which AI coding tool is making the request
|
|
32
49
|
* @param {Object} headers - Request headers
|
|
@@ -126,6 +143,13 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
126
143
|
mapArgs: (a) => ({
|
|
127
144
|
query: a.query || ""
|
|
128
145
|
})
|
|
146
|
+
},
|
|
147
|
+
"WebAgent": {
|
|
148
|
+
name: "web_agent",
|
|
149
|
+
mapArgs: (a) => ({
|
|
150
|
+
url: a.url || "",
|
|
151
|
+
goal: a.goal || ""
|
|
152
|
+
})
|
|
129
153
|
}
|
|
130
154
|
},
|
|
131
155
|
|
|
@@ -181,6 +205,13 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
181
205
|
path: a.path || a.directory || ".",
|
|
182
206
|
recursive: false
|
|
183
207
|
})
|
|
208
|
+
},
|
|
209
|
+
"WebAgent": {
|
|
210
|
+
name: "web_agent",
|
|
211
|
+
mapArgs: (a) => ({
|
|
212
|
+
url: a.url || "",
|
|
213
|
+
goal: a.goal || ""
|
|
214
|
+
})
|
|
184
215
|
}
|
|
185
216
|
},
|
|
186
217
|
|
|
@@ -237,6 +268,13 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
237
268
|
path: a.path || a.directory || ".",
|
|
238
269
|
recursive: false
|
|
239
270
|
})
|
|
271
|
+
},
|
|
272
|
+
"WebAgent": {
|
|
273
|
+
name: "web_agent",
|
|
274
|
+
mapArgs: (a) => ({
|
|
275
|
+
url: a.url || "",
|
|
276
|
+
goal: a.goal || ""
|
|
277
|
+
})
|
|
240
278
|
}
|
|
241
279
|
},
|
|
242
280
|
|
|
@@ -287,6 +325,13 @@ const CLIENT_TOOL_MAPPINGS = {
|
|
|
287
325
|
mapArgs: (a) => ({
|
|
288
326
|
filepath: a.path || a.directory || "."
|
|
289
327
|
})
|
|
328
|
+
},
|
|
329
|
+
"WebAgent": {
|
|
330
|
+
name: "web_agent",
|
|
331
|
+
mapArgs: (a) => ({
|
|
332
|
+
url: a.url || "",
|
|
333
|
+
goal: a.goal || ""
|
|
334
|
+
})
|
|
290
335
|
}
|
|
291
336
|
}
|
|
292
337
|
};
|
|
@@ -406,8 +451,10 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
406
451
|
// that have a mapping in CLIENT_TOOL_MAPPINGS — this ensures clients like
|
|
407
452
|
// Codex don't see tools they can't handle (Task, WebFetch, NotebookEdit)
|
|
408
453
|
// while Claude Code (unknown client) gets the full IDE_SAFE_TOOLS set.
|
|
454
|
+
// Skip injection if client explicitly opted out (tool_choice: "none" or empty tools array).
|
|
409
455
|
const clientType = detectClient(req.headers);
|
|
410
|
-
|
|
456
|
+
const clientExplicitlyDisabledTools = req.body.tool_choice === "none" || Array.isArray(req.body.tools);
|
|
457
|
+
if (!clientExplicitlyDisabledTools && (!anthropicRequest.tools || anthropicRequest.tools.length === 0)) {
|
|
411
458
|
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
412
459
|
const clientTools = clientMappings
|
|
413
460
|
? IDE_SAFE_TOOLS.filter(t => clientMappings[t.name])
|
|
@@ -469,7 +516,8 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
469
516
|
}
|
|
470
517
|
|
|
471
518
|
// Convert to OpenAI format
|
|
472
|
-
const
|
|
519
|
+
const streamModel = resolveResponseModel(result.body, req.body.model);
|
|
520
|
+
const openaiResponse = convertAnthropicToOpenAI(result.body, streamModel);
|
|
473
521
|
|
|
474
522
|
// Debug: Log what we're about to stream
|
|
475
523
|
logger.debug({
|
|
@@ -509,7 +557,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
509
557
|
id: openaiResponse.id,
|
|
510
558
|
object: "chat.completion.chunk",
|
|
511
559
|
created: openaiResponse.created,
|
|
512
|
-
model:
|
|
560
|
+
model: streamModel,
|
|
513
561
|
system_fingerprint: "fp_lynkr",
|
|
514
562
|
choices: [{
|
|
515
563
|
index: 0,
|
|
@@ -531,7 +579,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
531
579
|
id: openaiResponse.id,
|
|
532
580
|
object: "chat.completion.chunk",
|
|
533
581
|
created: openaiResponse.created,
|
|
534
|
-
model:
|
|
582
|
+
model: streamModel,
|
|
535
583
|
system_fingerprint: "fp_lynkr",
|
|
536
584
|
choices: [{
|
|
537
585
|
index: 0,
|
|
@@ -551,7 +599,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
551
599
|
id: openaiResponse.id,
|
|
552
600
|
object: "chat.completion.chunk",
|
|
553
601
|
created: openaiResponse.created,
|
|
554
|
-
model:
|
|
602
|
+
model: streamModel,
|
|
555
603
|
choices: [{
|
|
556
604
|
index: 0,
|
|
557
605
|
delta: {
|
|
@@ -578,7 +626,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
578
626
|
id: openaiResponse.id,
|
|
579
627
|
object: "chat.completion.chunk",
|
|
580
628
|
created: openaiResponse.created,
|
|
581
|
-
model:
|
|
629
|
+
model: streamModel,
|
|
582
630
|
system_fingerprint: "fp_lynkr",
|
|
583
631
|
choices: [{
|
|
584
632
|
index: 0,
|
|
@@ -649,7 +697,7 @@ router.post("/chat/completions", async (req, res) => {
|
|
|
649
697
|
}, "Orchestrator result structure");
|
|
650
698
|
|
|
651
699
|
// Convert Anthropic response to OpenAI format
|
|
652
|
-
const openaiResponse = convertAnthropicToOpenAI(result.body, req.body.model);
|
|
700
|
+
const openaiResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
|
|
653
701
|
|
|
654
702
|
// Map tool names for known IDE clients
|
|
655
703
|
if (clientType !== "unknown" && openaiResponse.choices?.[0]?.message?.tool_calls?.length > 0) {
|
|
@@ -1450,8 +1498,10 @@ router.post("/responses", async (req, res) => {
|
|
|
1450
1498
|
}, "After Chat→Anthropic conversion");
|
|
1451
1499
|
|
|
1452
1500
|
// Inject tools if client didn't send any (same two-layer filtering as chat/completions).
|
|
1501
|
+
// Skip injection if client explicitly opted out (tool_choice: "none" or empty tools array).
|
|
1453
1502
|
const clientType = detectClient(req.headers);
|
|
1454
|
-
|
|
1503
|
+
const clientExplicitlyDisabledTools = req.body.tool_choice === "none" || Array.isArray(req.body.tools);
|
|
1504
|
+
if (!clientExplicitlyDisabledTools && (!anthropicRequest.tools || anthropicRequest.tools.length === 0)) {
|
|
1455
1505
|
const clientMappings = CLIENT_TOOL_MAPPINGS[clientType];
|
|
1456
1506
|
const clientTools = clientMappings
|
|
1457
1507
|
? IDE_SAFE_TOOLS.filter(t => clientMappings[t.name])
|
|
@@ -1503,7 +1553,8 @@ router.post("/responses", async (req, res) => {
|
|
|
1503
1553
|
}, "=== ORCHESTRATOR RESULT FOR RESPONSES API ===");
|
|
1504
1554
|
|
|
1505
1555
|
// Convert back: Anthropic → OpenAI → Responses
|
|
1506
|
-
const
|
|
1556
|
+
const responsesModel = resolveResponseModel(result.body, req.body.model);
|
|
1557
|
+
const chatResponse = convertAnthropicToOpenAI(result.body, responsesModel);
|
|
1507
1558
|
|
|
1508
1559
|
logger.debug({
|
|
1509
1560
|
chatContent: chatResponse.choices?.[0]?.message?.content?.substring(0, 200),
|
|
@@ -1565,7 +1616,7 @@ router.post("/responses", async (req, res) => {
|
|
|
1565
1616
|
object: "response",
|
|
1566
1617
|
status: "in_progress",
|
|
1567
1618
|
created_at: createdAt,
|
|
1568
|
-
model:
|
|
1619
|
+
model: responsesModel,
|
|
1569
1620
|
output: [],
|
|
1570
1621
|
usage: null
|
|
1571
1622
|
},
|
|
@@ -1582,7 +1633,7 @@ router.post("/responses", async (req, res) => {
|
|
|
1582
1633
|
object: "response",
|
|
1583
1634
|
status: "in_progress",
|
|
1584
1635
|
created_at: createdAt,
|
|
1585
|
-
model:
|
|
1636
|
+
model: responsesModel,
|
|
1586
1637
|
output: [],
|
|
1587
1638
|
usage: null
|
|
1588
1639
|
},
|
|
@@ -1761,7 +1812,7 @@ router.post("/responses", async (req, res) => {
|
|
|
1761
1812
|
object: "response",
|
|
1762
1813
|
status: "completed",
|
|
1763
1814
|
created_at: createdAt,
|
|
1764
|
-
model:
|
|
1815
|
+
model: responsesModel,
|
|
1765
1816
|
output: outputItems,
|
|
1766
1817
|
usage: {
|
|
1767
1818
|
input_tokens: responsesResponse.usage?.prompt_tokens || 0,
|
|
@@ -1814,7 +1865,7 @@ router.post("/responses", async (req, res) => {
|
|
|
1814
1865
|
});
|
|
1815
1866
|
|
|
1816
1867
|
// Convert back: Anthropic → OpenAI → Responses
|
|
1817
|
-
const chatResponse = convertAnthropicToOpenAI(result.body, req.body.model);
|
|
1868
|
+
const chatResponse = convertAnthropicToOpenAI(result.body, resolveResponseModel(result.body, req.body.model));
|
|
1818
1869
|
const responsesResponse = convertChatToResponses(chatResponse);
|
|
1819
1870
|
|
|
1820
1871
|
logger.info({
|
|
@@ -415,8 +415,8 @@ router.get("/health/providers", (req, res) => {
|
|
|
415
415
|
// Get circuit breaker states
|
|
416
416
|
const circuitBreakerStates = {};
|
|
417
417
|
const allBreakers = registry.getAll();
|
|
418
|
-
for (const
|
|
419
|
-
circuitBreakerStates[name] = breaker.state;
|
|
418
|
+
for (const breaker of allBreakers) {
|
|
419
|
+
circuitBreakerStates[breaker.name] = breaker.state;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
// Get all provider health
|
|
@@ -452,7 +452,8 @@ router.get("/health/providers/:name", (req, res) => {
|
|
|
452
452
|
|
|
453
453
|
// Get circuit breaker state for this provider
|
|
454
454
|
const allBreakers = registry.getAll();
|
|
455
|
-
const
|
|
455
|
+
const breakerState = allBreakers.find((b) => b.name === providerName);
|
|
456
|
+
const circuitState = breakerState?.state || "CLOSED";
|
|
456
457
|
|
|
457
458
|
// Get detailed metrics
|
|
458
459
|
const metrics = healthTracker.getProviderMetrics(providerName);
|
|
@@ -477,4 +478,171 @@ router.get("/health/providers/:name", (req, res) => {
|
|
|
477
478
|
}
|
|
478
479
|
});
|
|
479
480
|
|
|
481
|
+
// ============================================================================
|
|
482
|
+
// Routing Telemetry Endpoints
|
|
483
|
+
// ============================================================================
|
|
484
|
+
|
|
485
|
+
const telemetry = require("../routing/telemetry");
|
|
486
|
+
const { getLatencyTracker } = require("../routing/latency-tracker");
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* GET /v1/routing/stats
|
|
490
|
+
*
|
|
491
|
+
* Aggregated routing telemetry statistics.
|
|
492
|
+
*/
|
|
493
|
+
router.get("/routing/stats", (req, res) => {
|
|
494
|
+
try {
|
|
495
|
+
const since = req.query.since ? Number(req.query.since) : undefined;
|
|
496
|
+
const until = req.query.until ? Number(req.query.until) : undefined;
|
|
497
|
+
const stats = telemetry.getStats({ since, until });
|
|
498
|
+
|
|
499
|
+
if (!stats) {
|
|
500
|
+
return res.json({ object: "routing_stats", data: null, message: "No telemetry data available" });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Merge latency tracker percentiles
|
|
504
|
+
const latencyTracker = getLatencyTracker();
|
|
505
|
+
const latencyStats = {};
|
|
506
|
+
for (const [provider, pStats] of latencyTracker.getAllStats()) {
|
|
507
|
+
latencyStats[provider] = pStats;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
res.json({
|
|
511
|
+
object: "routing_stats",
|
|
512
|
+
data: { ...stats, latencyPercentiles: latencyStats },
|
|
513
|
+
timestamp: new Date().toISOString(),
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
logger.error({ error: error.message }, "Error getting routing stats");
|
|
517
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* GET /v1/routing/stats/:provider
|
|
523
|
+
*
|
|
524
|
+
* Per-provider routing telemetry.
|
|
525
|
+
*/
|
|
526
|
+
router.get("/routing/stats/:provider", (req, res) => {
|
|
527
|
+
try {
|
|
528
|
+
const provider = req.params.provider.toLowerCase();
|
|
529
|
+
const since = req.query.since ? Number(req.query.since) : undefined;
|
|
530
|
+
const stats = telemetry.getProviderStats(provider, { since });
|
|
531
|
+
|
|
532
|
+
if (!stats) {
|
|
533
|
+
return res.json({ object: "provider_routing_stats", data: null, message: `No data for ${provider}` });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const latencyTracker = getLatencyTracker();
|
|
537
|
+
const latency = latencyTracker.getStats(provider);
|
|
538
|
+
|
|
539
|
+
res.json({
|
|
540
|
+
object: "provider_routing_stats",
|
|
541
|
+
provider,
|
|
542
|
+
data: { ...stats, latency },
|
|
543
|
+
timestamp: new Date().toISOString(),
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
logger.error({ error: error.message }, "Error getting provider routing stats");
|
|
547
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* GET /v1/routing/telemetry
|
|
553
|
+
*
|
|
554
|
+
* Raw telemetry records (most recent first).
|
|
555
|
+
*/
|
|
556
|
+
router.get("/routing/telemetry", (req, res) => {
|
|
557
|
+
try {
|
|
558
|
+
const filters = {
|
|
559
|
+
provider: req.query.provider,
|
|
560
|
+
tier: req.query.tier,
|
|
561
|
+
since: req.query.since ? Number(req.query.since) : undefined,
|
|
562
|
+
limit: req.query.limit ? Number(req.query.limit) : 100,
|
|
563
|
+
};
|
|
564
|
+
const records = telemetry.query(filters);
|
|
565
|
+
|
|
566
|
+
res.json({ object: "telemetry_list", data: records, count: records.length });
|
|
567
|
+
} catch (error) {
|
|
568
|
+
logger.error({ error: error.message }, "Error querying telemetry");
|
|
569
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* GET /v1/routing/accuracy
|
|
575
|
+
*
|
|
576
|
+
* Routing accuracy analysis (over/under-provisioned percentages).
|
|
577
|
+
*/
|
|
578
|
+
router.get("/routing/accuracy", (req, res) => {
|
|
579
|
+
try {
|
|
580
|
+
const since = req.query.since ? Number(req.query.since) : undefined;
|
|
581
|
+
const accuracy = telemetry.getRoutingAccuracy({ since });
|
|
582
|
+
|
|
583
|
+
res.json({
|
|
584
|
+
object: "routing_accuracy",
|
|
585
|
+
data: accuracy,
|
|
586
|
+
timestamp: new Date().toISOString(),
|
|
587
|
+
});
|
|
588
|
+
} catch (error) {
|
|
589
|
+
logger.error({ error: error.message }, "Error getting routing accuracy");
|
|
590
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// ── Admin: Hot Reload Config + Reset Circuit Breakers ─────────────────
|
|
595
|
+
|
|
596
|
+
router.post("/admin/reload", (req, res) => {
|
|
597
|
+
try {
|
|
598
|
+
config.reloadConfig();
|
|
599
|
+
const registry = getCircuitBreakerRegistry();
|
|
600
|
+
const states = registry.getAll();
|
|
601
|
+
res.json({
|
|
602
|
+
object: "admin_reload",
|
|
603
|
+
status: "ok",
|
|
604
|
+
reloaded: [
|
|
605
|
+
"modelTiers",
|
|
606
|
+
"apiKeys",
|
|
607
|
+
"providerSettings",
|
|
608
|
+
"circuitBreakers",
|
|
609
|
+
],
|
|
610
|
+
circuitBreakers: states.map(s => ({ name: s.name, state: s.state })),
|
|
611
|
+
timestamp: new Date().toISOString(),
|
|
612
|
+
});
|
|
613
|
+
} catch (error) {
|
|
614
|
+
logger.error({ error: error.message }, "Admin reload failed");
|
|
615
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
router.post("/admin/circuit-breakers/reset", (req, res) => {
|
|
620
|
+
try {
|
|
621
|
+
const provider = req.query.provider || req.body?.provider;
|
|
622
|
+
const registry = getCircuitBreakerRegistry();
|
|
623
|
+
|
|
624
|
+
if (provider) {
|
|
625
|
+
const breaker = registry.breakers?.get(provider);
|
|
626
|
+
if (breaker) {
|
|
627
|
+
breaker.reset();
|
|
628
|
+
res.json({ object: "circuit_breaker_reset", provider, status: "reset" });
|
|
629
|
+
} else {
|
|
630
|
+
res.status(404).json({ error: { message: `No circuit breaker for provider: ${provider}` } });
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
registry.resetAll();
|
|
634
|
+
const states = registry.getAll();
|
|
635
|
+
res.json({
|
|
636
|
+
object: "circuit_breaker_reset",
|
|
637
|
+
provider: "all",
|
|
638
|
+
status: "reset",
|
|
639
|
+
breakers: states.map(s => ({ name: s.name, state: s.state })),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
logger.error({ error: error.message }, "Circuit breaker reset failed");
|
|
644
|
+
res.status(500).json({ error: { type: "server_error", message: error.message } });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
480
648
|
module.exports = router;
|
package/src/api/router.js
CHANGED
|
@@ -136,7 +136,7 @@ router.post("/routing/analyze", async (req, res) => {
|
|
|
136
136
|
const { getModelTierSelector } = require("../routing/model-tiers");
|
|
137
137
|
const { getModelRegistry } = require("../routing/model-registry");
|
|
138
138
|
|
|
139
|
-
const analysis = analyzeComplexity(req.body, { weighted: req.query.weighted === "true" });
|
|
139
|
+
const analysis = await analyzeComplexity(req.body, { weighted: req.query.weighted === "true" });
|
|
140
140
|
const agentic = getAgenticDetector().detect(req.body);
|
|
141
141
|
const selector = getModelTierSelector();
|
|
142
142
|
const tier = selector.getTier(analysis.score);
|
|
@@ -210,13 +210,17 @@ router.post("/api/event_logging/batch", (req, res) => {
|
|
|
210
210
|
|
|
211
211
|
router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
212
212
|
try {
|
|
213
|
+
const { createTimer } = require("../utils/perf-timer");
|
|
214
|
+
const timer = createTimer("POST /v1/messages");
|
|
213
215
|
metrics.recordRequest();
|
|
214
216
|
// Support both query parameter (?stream=true) and body parameter ({"stream": true})
|
|
215
217
|
const wantsStream = Boolean(req.query?.stream === 'true' || req.body?.stream);
|
|
216
218
|
const hasTools = Array.isArray(req.body?.tools) && req.body.tools.length > 0;
|
|
219
|
+
timer.mark("parseRequest");
|
|
217
220
|
|
|
218
221
|
// Analyze complexity for routing headers (Phase 3)
|
|
219
|
-
const complexity = analyzeComplexity(req.body);
|
|
222
|
+
const complexity = await analyzeComplexity(req.body);
|
|
223
|
+
timer.mark("analyzeComplexity");
|
|
220
224
|
let preRouteProvider = 'cloud';
|
|
221
225
|
if (complexity.recommendation === 'local') {
|
|
222
226
|
// Use tier config to determine actual provider instead of hardcoding 'ollama'
|
|
@@ -430,6 +434,7 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
430
434
|
}
|
|
431
435
|
|
|
432
436
|
// Non-streaming or tool-based requests (buffered path)
|
|
437
|
+
timer.mark("preProcessMessage");
|
|
433
438
|
const result = await processMessage({
|
|
434
439
|
payload: req.body,
|
|
435
440
|
headers: req.headers,
|
|
@@ -440,6 +445,8 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
440
445
|
maxDurationMs: req.body?.max_duration_ms,
|
|
441
446
|
},
|
|
442
447
|
});
|
|
448
|
+
timer.mark("processMessage");
|
|
449
|
+
timer.done();
|
|
443
450
|
|
|
444
451
|
// Legacy streaming wrapper (for tool-based requests that requested streaming)
|
|
445
452
|
if (wantsStream && hasTools) {
|