lynkr 9.4.6 → 9.6.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/README.md +49 -15
- package/install.sh +21 -5
- package/package.json +4 -2
- package/public/dashboard.html +13 -1
- package/scripts/check-native.js +97 -0
- package/src/agents/decomposition/dispatcher.js +185 -0
- package/src/agents/decomposition/gate.js +136 -0
- package/src/agents/decomposition/index.js +183 -0
- package/src/agents/decomposition/model-call.js +75 -0
- package/src/agents/decomposition/planner.js +223 -0
- package/src/agents/decomposition/synthesizer.js +89 -0
- package/src/agents/decomposition/telemetry.js +55 -0
- package/src/clients/databricks.js +215 -4
- package/src/clients/openrouter-utils.js +19 -27
- package/src/clients/prompt-cache-injection.js +49 -0
- package/src/clients/responses-format.js +7 -0
- package/src/clients/tool-call-repair.js +130 -0
- package/src/config/index.js +32 -0
- package/src/context/caveman.js +94 -0
- package/src/context/output-format-guard.js +99 -0
- package/src/context/tool-dedup.js +95 -0
- package/src/context/tool-result-compressor.js +106 -0
- package/src/dashboard/api.js +69 -18
- package/src/orchestrator/bypass.js +135 -0
- package/src/orchestrator/index.js +33 -2
- package/src/routing/index.js +47 -0
- package/src/routing/model-registry.js +89 -26
- package/src/routing/risk-analyzer.js +7 -2
- package/src/routing/session-affinity.js +96 -0
- package/src/routing/telemetry.js +16 -3
- package/src/routing/tier-fallback.js +91 -0
- package/src/server.js +2 -0
- package/src/tools/decompose.js +91 -0
- package/src/tools/lazy-loader.js +8 -0
- package/.impeccable/live/config.json +0 -8
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caveman Terse-Output Injector
|
|
3
|
+
*
|
|
4
|
+
* Appends a brevity instruction to the system prompt so the model produces
|
|
5
|
+
* terser responses, reducing OUTPUT tokens. Opt-in and off by default — it
|
|
6
|
+
* changes model behavior, so it's only applied when explicitly enabled.
|
|
7
|
+
*
|
|
8
|
+
* Enable with CAVEMAN_ENABLED=true. Level via CAVEMAN_LEVEL=lite|full|ultra
|
|
9
|
+
* (default: lite). Adapted from 9router's caveman injector / the caveman skill
|
|
10
|
+
* (https://github.com/JuliusBrussee/caveman).
|
|
11
|
+
*
|
|
12
|
+
* @module context/caveman
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const config = require("../config");
|
|
16
|
+
const logger = require("../logger");
|
|
17
|
+
|
|
18
|
+
const LEVELS = ["lite", "full", "ultra"];
|
|
19
|
+
|
|
20
|
+
// Shared guardrails so brevity never corrupts the substance that matters.
|
|
21
|
+
const BOUNDARIES =
|
|
22
|
+
"Code blocks, file paths, commands, errors, URLs: keep exact. " +
|
|
23
|
+
"Security warnings, irreversible-action confirmations, and multi-step ordered " +
|
|
24
|
+
"sequences: write in full normal prose. Resume terse style afterward.";
|
|
25
|
+
|
|
26
|
+
const EXAMPLES =
|
|
27
|
+
'Not: "Sure! I\'d be happy to help. The issue is likely caused by..." ' +
|
|
28
|
+
'Yes: "Bug in auth middleware. Token expiry uses `<` not `<=`. Fix:"';
|
|
29
|
+
|
|
30
|
+
const PERSISTENCE = "Apply this to every response unless a guardrail above applies.";
|
|
31
|
+
|
|
32
|
+
const PROMPTS = {
|
|
33
|
+
lite: [
|
|
34
|
+
"Respond tersely. Keep grammar and full sentences but drop filler, hedging, and pleasantries (just/really/basically/sure/of course/I'd be happy to).",
|
|
35
|
+
"Pattern: state the thing, the action, the reason. Then the next step.",
|
|
36
|
+
EXAMPLES,
|
|
37
|
+
BOUNDARIES,
|
|
38
|
+
PERSISTENCE,
|
|
39
|
+
].join(" "),
|
|
40
|
+
|
|
41
|
+
full: [
|
|
42
|
+
"Respond like a terse caveman. All technical substance stays exact; only fluff dies.",
|
|
43
|
+
"Drop articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries, and hedging. Fragments OK. Prefer short synonyms (big not extensive, fix not implement a solution for).",
|
|
44
|
+
"Pattern: [thing] [action] [reason]. [next step].",
|
|
45
|
+
EXAMPLES,
|
|
46
|
+
BOUNDARIES,
|
|
47
|
+
PERSISTENCE,
|
|
48
|
+
].join(" "),
|
|
49
|
+
|
|
50
|
+
ultra: [
|
|
51
|
+
"Respond ultra-terse. Maximum compression. Telegraphic.",
|
|
52
|
+
"Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, use arrows for causality (X → Y). One word when one word is enough.",
|
|
53
|
+
"Pattern: [thing] → [result]. [fix].",
|
|
54
|
+
EXAMPLES,
|
|
55
|
+
BOUNDARIES,
|
|
56
|
+
PERSISTENCE,
|
|
57
|
+
].join(" "),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const MARKER = "[brevity]";
|
|
61
|
+
|
|
62
|
+
/** Resolve the configured level, falling back to "lite". */
|
|
63
|
+
function resolveLevel(level) {
|
|
64
|
+
const l = String(level || config.caveman?.level || "lite").toLowerCase();
|
|
65
|
+
return LEVELS.includes(l) ? l : "lite";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Append the brevity instruction to a system prompt string.
|
|
70
|
+
* Idempotent — won't double-inject if the marker is already present.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} system - Existing system prompt (may be empty).
|
|
73
|
+
* @param {object} [opts]
|
|
74
|
+
* @param {boolean} [opts.enabled] - Override config enablement.
|
|
75
|
+
* @param {string} [opts.level] - Override level.
|
|
76
|
+
* @returns {string} system prompt, possibly with brevity instruction appended.
|
|
77
|
+
*/
|
|
78
|
+
function injectCaveman(system, opts = {}) {
|
|
79
|
+
const enabled = opts.enabled ?? config.caveman?.enabled === true;
|
|
80
|
+
if (!enabled) return system || "";
|
|
81
|
+
|
|
82
|
+
const base = system || "";
|
|
83
|
+
if (base.includes(MARKER)) return base;
|
|
84
|
+
|
|
85
|
+
const level = resolveLevel(opts.level);
|
|
86
|
+
const instruction = `\n\n${MARKER} ${PROMPTS[level]}`;
|
|
87
|
+
logger.debug({ level }, "[Caveman] Injected brevity instruction into system prompt");
|
|
88
|
+
return base + instruction;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
injectCaveman,
|
|
93
|
+
LEVELS,
|
|
94
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Format Guard
|
|
3
|
+
*
|
|
4
|
+
* Appends a short formatting instruction to the system prompt so weaker
|
|
5
|
+
* backends (Moonshot/Kimi, Ollama, etc.) stop emitting mangled ASCII/Unicode
|
|
6
|
+
* box-drawing "diagrams" that render as garbage in clients. The actual backend
|
|
7
|
+
* is decided by tier routing — so even when the client asks for "claude-opus",
|
|
8
|
+
* the request may be served by a model that formats poorly. This normalizes the
|
|
9
|
+
* presentation without changing which model serves the request.
|
|
10
|
+
*
|
|
11
|
+
* Always-on (no env flag). It is skipped only for Claude-family backends, which
|
|
12
|
+
* already produce clean GitHub-flavored markdown — injecting there would just
|
|
13
|
+
* waste tokens. The skip biases toward injecting: if we can't tell, we inject
|
|
14
|
+
* (a false-inject is harmless ~50 tokens; a false-skip leaves the garble).
|
|
15
|
+
*
|
|
16
|
+
* Keyed off the ROUTING-RESOLVED provider/model, never the client's requested
|
|
17
|
+
* body.model (which is just a label once tier routing is on).
|
|
18
|
+
*
|
|
19
|
+
* @module context/output-format-guard
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const logger = require("../logger");
|
|
23
|
+
|
|
24
|
+
const MARKER = "[fmt-guard]";
|
|
25
|
+
|
|
26
|
+
// Model names that already produce clean markdown.
|
|
27
|
+
const CLEAN_FORMATTER_RE = /\b(claude|sonnet|opus|haiku)\b/i;
|
|
28
|
+
// Providers that are always Claude-backed.
|
|
29
|
+
const CLEAN_PROVIDERS = new Set(["azure-anthropic"]);
|
|
30
|
+
|
|
31
|
+
const GUARD_TEXT =
|
|
32
|
+
`${MARKER} Formatting rules for your response: use plain GitHub-flavored markdown only. ` +
|
|
33
|
+
"Do NOT draw diagrams or boxes with ASCII or Unicode line-drawing characters " +
|
|
34
|
+
"(such as ┌ ─ │ └ ├ ┤ ┬ ┴ ╔ ═ ║), and do NOT wrap headings or code in decorative borders. " +
|
|
35
|
+
"Represent structure and relationships with normal markdown headings, nested bullet lists, " +
|
|
36
|
+
"numbered lists, or tables. Use standard triple-backtick fenced code blocks for code. " +
|
|
37
|
+
"Keep code, file paths, commands, and URLs exact.";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether the resolved backend already formats cleanly (→ skip injection).
|
|
41
|
+
* @param {string} provider - routing-resolved provider
|
|
42
|
+
* @param {string} model - routing-resolved model (NOT the client's requested model)
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function producesCleanMarkdown(provider, model) {
|
|
46
|
+
if (CLEAN_PROVIDERS.has(String(provider || "").toLowerCase())) return true;
|
|
47
|
+
if (model && CLEAN_FORMATTER_RE.test(String(model))) return true;
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Append the guard text to a system prompt that may be a string or an array of
|
|
53
|
+
* Anthropic content blocks. Idempotent via MARKER. Pure — returns the new value.
|
|
54
|
+
*/
|
|
55
|
+
function appendToSystem(system, text) {
|
|
56
|
+
// String (or empty) system prompt.
|
|
57
|
+
if (system == null || typeof system === "string") {
|
|
58
|
+
const base = system || "";
|
|
59
|
+
if (base.includes(MARKER)) return base;
|
|
60
|
+
return base ? `${base}\n\n${text}` : text;
|
|
61
|
+
}
|
|
62
|
+
// Anthropic array-of-blocks system prompt.
|
|
63
|
+
if (Array.isArray(system)) {
|
|
64
|
+
const already = system.some(
|
|
65
|
+
(b) => typeof b?.text === "string" && b.text.includes(MARKER)
|
|
66
|
+
);
|
|
67
|
+
if (already) return system;
|
|
68
|
+
return [...system, { type: "text", text }];
|
|
69
|
+
}
|
|
70
|
+
return system;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Inject the formatting guard into body.system in place, unless the resolved
|
|
75
|
+
* backend already formats cleanly. Always-on; idempotent.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} body - request body (mutated in place)
|
|
78
|
+
* @param {object} [opts]
|
|
79
|
+
* @param {string} [opts.provider] - routing-resolved provider
|
|
80
|
+
* @param {string} [opts.model] - routing-resolved model
|
|
81
|
+
* @returns {object} body
|
|
82
|
+
*/
|
|
83
|
+
function injectFormatGuard(body, opts = {}) {
|
|
84
|
+
if (!body) return body;
|
|
85
|
+
const { provider, model } = opts;
|
|
86
|
+
if (producesCleanMarkdown(provider, model)) return body;
|
|
87
|
+
|
|
88
|
+
body.system = appendToSystem(body.system, GUARD_TEXT);
|
|
89
|
+
logger.debug({ provider, model }, "[FormatGuard] Injected markdown formatting guard");
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
injectFormatGuard,
|
|
95
|
+
producesCleanMarkdown,
|
|
96
|
+
appendToSystem,
|
|
97
|
+
MARKER,
|
|
98
|
+
GUARD_TEXT,
|
|
99
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-aware Tool Dedup
|
|
3
|
+
*
|
|
4
|
+
* Strips built-in tool definitions when an equivalent MCP tool is present in
|
|
5
|
+
* the request. Sending both wastes tool-schema tokens and gives the model
|
|
6
|
+
* redundant choices. Rule-based and deterministic.
|
|
7
|
+
*
|
|
8
|
+
* Example: if the Exa or Tavily MCP search tools are present, the built-in
|
|
9
|
+
* WebSearch/WebFetch tools are redundant and dropped.
|
|
10
|
+
*
|
|
11
|
+
* Ported from 9router's toolDeduper. Always on — purely removes redundant
|
|
12
|
+
* tool definitions, never adds.
|
|
13
|
+
*
|
|
14
|
+
* @module context/tool-dedup
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const logger = require("../logger");
|
|
18
|
+
|
|
19
|
+
// Each rule: if any `triggers` tool is present, strip any tools matching
|
|
20
|
+
// `strip`. Patterns may be exact strings or RegExp (matched against the name).
|
|
21
|
+
const DEDUP_RULES = [
|
|
22
|
+
{
|
|
23
|
+
// Exa MCP present → drop built-in web tools (Exa is preferred).
|
|
24
|
+
triggers: ["mcp__exa__web_search_exa", "mcp__exa__web_fetch_exa"],
|
|
25
|
+
strip: ["WebSearch", "WebFetch", "web_search", "web_fetch", "mcp__workspace__web_fetch"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
// Tavily MCP present → drop built-in web tools.
|
|
29
|
+
triggers: ["mcp__tavily__tavily_search", "mcp__tavily__tavily_extract"],
|
|
30
|
+
strip: ["WebSearch", "WebFetch", "web_search", "web_fetch", "mcp__workspace__web_fetch"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// Browser MCP present → drop a duplicate Chrome-connector tool family.
|
|
34
|
+
triggers: [/^mcp__browsermcp__/],
|
|
35
|
+
strip: [/^mcp__Claude_in_Chrome__/],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function getToolName(t) {
|
|
40
|
+
return t?.name || t?.function?.name || "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function matches(name, pattern) {
|
|
44
|
+
if (typeof pattern === "string") return name === pattern;
|
|
45
|
+
return pattern instanceof RegExp ? pattern.test(name) : false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove redundant built-in tools that are superseded by present MCP tools.
|
|
50
|
+
*
|
|
51
|
+
* @param {Array} tools - Tool definitions (Anthropic or OpenAI shape).
|
|
52
|
+
* @returns {{tools: Array, stripped: string[]}} filtered tools + names removed.
|
|
53
|
+
*/
|
|
54
|
+
function dedupeTools(tools) {
|
|
55
|
+
if (!Array.isArray(tools) || tools.length === 0) return { tools, stripped: [] };
|
|
56
|
+
|
|
57
|
+
const names = tools.map(getToolName);
|
|
58
|
+
const toStrip = new Set();
|
|
59
|
+
|
|
60
|
+
for (const rule of DEDUP_RULES) {
|
|
61
|
+
const hasTrigger = names.some((n) => rule.triggers.some((p) => matches(n, p)));
|
|
62
|
+
if (!hasTrigger) continue;
|
|
63
|
+
for (const n of names) {
|
|
64
|
+
// Never strip a tool that is itself a trigger.
|
|
65
|
+
if (rule.triggers.some((p) => matches(n, p))) continue;
|
|
66
|
+
if (rule.strip.some((p) => matches(n, p))) toStrip.add(n);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (toStrip.size === 0) return { tools, stripped: [] };
|
|
71
|
+
|
|
72
|
+
const out = tools.filter((t) => !toStrip.has(getToolName(t)));
|
|
73
|
+
return { tools: out, stripped: Array.from(toStrip) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Apply tool dedup to a payload in place. No-op when nothing is stripped.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} payload - Request body with a `tools` array.
|
|
80
|
+
* @returns {string[]} names of stripped tools.
|
|
81
|
+
*/
|
|
82
|
+
function applyToolDedup(payload) {
|
|
83
|
+
if (!payload || !Array.isArray(payload.tools)) return [];
|
|
84
|
+
const { tools, stripped } = dedupeTools(payload.tools);
|
|
85
|
+
if (stripped.length > 0) {
|
|
86
|
+
payload.tools = tools;
|
|
87
|
+
logger.debug({ stripped }, "[ToolDedup] Stripped redundant built-in tools (MCP equivalents present)");
|
|
88
|
+
}
|
|
89
|
+
return stripped;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
dedupeTools,
|
|
94
|
+
applyToolDedup,
|
|
95
|
+
};
|
|
@@ -455,6 +455,107 @@ function compressContainerOutput(text) {
|
|
|
455
455
|
return `${header}\n${dataLines.slice(0, 10).join("\n")}\n... +${dataLines.length - 10} more (${dataLines.length} total)`;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// 11. Grep / ripgrep output ("file:lineno:content"), per-file match cap.
|
|
459
|
+
// Ported from 9router RTK grep filter (rtk/src/cmds/system/pipe_cmd.rs).
|
|
460
|
+
const GREP_PER_FILE_MAX = 10;
|
|
461
|
+
function compressGrep(text) {
|
|
462
|
+
const byFile = new Map();
|
|
463
|
+
let total = 0;
|
|
464
|
+
|
|
465
|
+
for (const line of text.split("\n")) {
|
|
466
|
+
// splitn(3, ':') — only split on the first two colons.
|
|
467
|
+
const first = line.indexOf(":");
|
|
468
|
+
if (first === -1) continue;
|
|
469
|
+
const second = line.indexOf(":", first + 1);
|
|
470
|
+
if (second === -1) continue;
|
|
471
|
+
const file = line.slice(0, first);
|
|
472
|
+
const lineNumStr = line.slice(first + 1, second);
|
|
473
|
+
const content = line.slice(second + 1);
|
|
474
|
+
if (!/^\d+$/.test(lineNumStr)) continue;
|
|
475
|
+
total++;
|
|
476
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
477
|
+
byFile.get(file).push([lineNumStr, content]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Require a meaningful number of matches so we don't mangle prose that
|
|
481
|
+
// happens to contain a "word:123:..." line.
|
|
482
|
+
if (total < 5) return null;
|
|
483
|
+
|
|
484
|
+
const files = Array.from(byFile.keys()).sort();
|
|
485
|
+
let out = `${total} matches in ${files.length}F:\n\n`;
|
|
486
|
+
for (const file of files) {
|
|
487
|
+
const matches = byFile.get(file);
|
|
488
|
+
out += `[file] ${file} (${matches.length}):\n`;
|
|
489
|
+
for (const [lineNum, content] of matches.slice(0, GREP_PER_FILE_MAX)) {
|
|
490
|
+
out += ` ${lineNum.padStart(4)}: ${content.trim()}\n`;
|
|
491
|
+
}
|
|
492
|
+
if (matches.length > GREP_PER_FILE_MAX) {
|
|
493
|
+
out += ` +${matches.length - GREP_PER_FILE_MAX}\n`;
|
|
494
|
+
}
|
|
495
|
+
out += "\n";
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 12. Generic log de-duplication: collapse consecutive duplicate lines and
|
|
501
|
+
// runs of blank lines, with a hard line cap. Ported from 9router RTK dedupLog.
|
|
502
|
+
const DEDUP_LINE_MAX = 2000;
|
|
503
|
+
function compressDedupLog(text) {
|
|
504
|
+
const lines = text.split("\n");
|
|
505
|
+
const out = [];
|
|
506
|
+
let prev = null;
|
|
507
|
+
let runCount = 0;
|
|
508
|
+
let blankStreak = 0;
|
|
509
|
+
|
|
510
|
+
const flushRun = () => {
|
|
511
|
+
if (prev !== null && runCount > 1) {
|
|
512
|
+
out.push(` ... (${runCount - 1} duplicate lines)`);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
for (const line of lines) {
|
|
517
|
+
if (line.trim() === "") {
|
|
518
|
+
if (blankStreak < 1) out.push(line);
|
|
519
|
+
blankStreak += 1;
|
|
520
|
+
flushRun();
|
|
521
|
+
prev = null;
|
|
522
|
+
runCount = 0;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
blankStreak = 0;
|
|
526
|
+
if (line === prev) {
|
|
527
|
+
runCount += 1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
flushRun();
|
|
531
|
+
out.push(line);
|
|
532
|
+
prev = line;
|
|
533
|
+
runCount = 1;
|
|
534
|
+
if (out.length >= DEDUP_LINE_MAX) {
|
|
535
|
+
out.push(`... (truncated at ${DEDUP_LINE_MAX} lines)`);
|
|
536
|
+
return out.join("\n");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
flushRun();
|
|
540
|
+
return out.join("\n");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 13. Last-resort generic truncation: keep head + tail lines, drop the middle.
|
|
544
|
+
// Only kicks in for very long output no specific compressor matched.
|
|
545
|
+
// Ported from 9router RTK smartTruncate.
|
|
546
|
+
const SMART_TRUNCATE_HEAD = 120;
|
|
547
|
+
const SMART_TRUNCATE_TAIL = 60;
|
|
548
|
+
const SMART_TRUNCATE_MIN_LINES = 250;
|
|
549
|
+
function compressSmartTruncate(text) {
|
|
550
|
+
const lines = text.split("\n");
|
|
551
|
+
if (lines.length < SMART_TRUNCATE_MIN_LINES) return null;
|
|
552
|
+
|
|
553
|
+
const head = lines.slice(0, SMART_TRUNCATE_HEAD);
|
|
554
|
+
const tail = lines.slice(lines.length - SMART_TRUNCATE_TAIL);
|
|
555
|
+
const cut = lines.length - head.length - tail.length;
|
|
556
|
+
return [...head, `... +${cut} lines truncated`, ...tail].join("\n");
|
|
557
|
+
}
|
|
558
|
+
|
|
458
559
|
// ── Compression Pipeline ─────────────────────────────────────────────
|
|
459
560
|
|
|
460
561
|
const COMPRESSORS = [
|
|
@@ -466,8 +567,13 @@ const COMPRESSORS = [
|
|
|
466
567
|
{ name: "build_output", fn: compressBuildOutput },
|
|
467
568
|
{ name: "container_output", fn: compressContainerOutput },
|
|
468
569
|
{ name: "json_response", fn: compressJSON },
|
|
570
|
+
{ name: "grep_output", fn: compressGrep },
|
|
469
571
|
{ name: "directory_listing", fn: compressDirectoryListing },
|
|
470
572
|
{ name: "large_file", fn: compressLargeFile },
|
|
573
|
+
// Generic fallbacks last: dedup exact-duplicate spam, then hard head/tail
|
|
574
|
+
// truncation only if nothing more specific applied.
|
|
575
|
+
{ name: "dedup_log", fn: compressDedupLog },
|
|
576
|
+
{ name: "smart_truncate", fn: compressSmartTruncate },
|
|
471
577
|
];
|
|
472
578
|
|
|
473
579
|
// Compression levels tied to routing tiers
|
package/src/dashboard/api.js
CHANGED
|
@@ -5,24 +5,74 @@ const metrics = require('../metrics');
|
|
|
5
5
|
const { getMetricsCollector } = require('../observability/metrics');
|
|
6
6
|
const { TIER_DEFINITIONS } = require('../routing/model-tiers');
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Per-provider type + whether its credentials/endpoint are actually present.
|
|
9
|
+
function providerMeta() {
|
|
9
10
|
const c = config;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
return {
|
|
12
|
+
databricks: { type: 'cloud', configured: !!(c.databricks?.url && c.databricks?.apiKey) },
|
|
13
|
+
'azure-anthropic': { type: 'cloud', configured: !!(c.azureAnthropic?.endpoint && c.azureAnthropic?.apiKey) },
|
|
14
|
+
bedrock: { type: 'cloud', configured: !!c.bedrock?.apiKey },
|
|
15
|
+
openrouter: { type: 'cloud', configured: !!c.openrouter?.apiKey },
|
|
16
|
+
openai: { type: 'cloud', configured: !!c.openai?.apiKey },
|
|
17
|
+
'azure-openai': { type: 'cloud', configured: !!(c.azureOpenAI?.endpoint && c.azureOpenAI?.apiKey) },
|
|
18
|
+
vertex: { type: 'cloud', configured: !!c.vertex?.projectId },
|
|
19
|
+
moonshot: { type: 'cloud', configured: !!c.moonshot?.apiKey },
|
|
20
|
+
ollama: { type: 'local', configured: !!c.ollama?.endpoint },
|
|
21
|
+
llamacpp: { type: 'local', configured: !!c.llamacpp?.endpoint },
|
|
22
|
+
lmstudio: { type: 'local', configured: !!c.lmstudio?.endpoint },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Providers the active routing config actually points at: the provider prefix
|
|
27
|
+
// of each TIER_* value (format `provider:model[:variant]`) plus the base
|
|
28
|
+
// MODEL_PROVIDER. Returns Map<providerName, tierLabels[]>.
|
|
29
|
+
function getReferencedProviders() {
|
|
30
|
+
const refs = new Map();
|
|
31
|
+
const note = (provider, label) => {
|
|
32
|
+
const key = String(provider || '').trim().toLowerCase();
|
|
33
|
+
if (!key) return;
|
|
34
|
+
if (!refs.has(key)) refs.set(key, []);
|
|
35
|
+
if (label && !refs.get(key).includes(label)) refs.get(key).push(label);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const tiers = config.modelTiers || {};
|
|
39
|
+
for (const [tier, val] of Object.entries(tiers)) {
|
|
40
|
+
if (typeof val === 'string' && val.trim()) {
|
|
41
|
+
note(val.split(':')[0], tier);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
note(config.modelProvider?.type, 'default');
|
|
45
|
+
|
|
46
|
+
return refs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Providers used by the routing config that have credentials/endpoints set.
|
|
50
|
+
// Unknown providers (no metadata) are included optimistically since we can't
|
|
51
|
+
// verify their credentials.
|
|
52
|
+
function getConfiguredProviders() {
|
|
53
|
+
const meta = providerMeta();
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const [name, tiers] of getReferencedProviders()) {
|
|
56
|
+
const m = meta[name];
|
|
57
|
+
if (!m || m.configured) {
|
|
58
|
+
out.push({ name, type: m?.type || 'cloud', tiers });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Tiers pointing at a known provider whose credentials/endpoint are missing —
|
|
65
|
+
// surfaced as a warning so a misconfigured tier is visible.
|
|
66
|
+
function getProviderWarnings() {
|
|
67
|
+
const meta = providerMeta();
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const [name, tiers] of getReferencedProviders()) {
|
|
70
|
+
const m = meta[name];
|
|
71
|
+
if (m && !m.configured) {
|
|
72
|
+
out.push({ name, type: m.type, tiers });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
26
76
|
}
|
|
27
77
|
|
|
28
78
|
// Noise provider names injected by unit tests — filter them out of UI
|
|
@@ -92,7 +142,8 @@ function overview(req, res) {
|
|
|
92
142
|
port: config.port,
|
|
93
143
|
version: process.env.npm_package_version || '9.0.2',
|
|
94
144
|
modelProvider: config.modelProvider?.type || 'unknown',
|
|
95
|
-
providers:
|
|
145
|
+
providers: getConfiguredProviders(),
|
|
146
|
+
providerWarnings: getProviderWarnings(),
|
|
96
147
|
statsWindow: win.label,
|
|
97
148
|
metrics: {
|
|
98
149
|
requestsTotal: snap.requestsTotal,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Bypass
|
|
3
|
+
*
|
|
4
|
+
* Short-circuits Claude Code CLI housekeeping requests that don't need a real
|
|
5
|
+
* model call:
|
|
6
|
+
* - "Warmup" pings the CLI sends to prime a connection
|
|
7
|
+
* - Topic/title extraction (the CLI asks for {"isNewTopic":..,"title":..})
|
|
8
|
+
* - Single-word "count" / "Warmup" probes
|
|
9
|
+
*
|
|
10
|
+
* Returning a canned response here saves a full provider round-trip (latency
|
|
11
|
+
* and tokens) on every session. Inspired by 9router's bypassHandler.
|
|
12
|
+
*
|
|
13
|
+
* Always on — only ever returns a canned response for unambiguous Claude CLI
|
|
14
|
+
* housekeeping traffic, never for real work.
|
|
15
|
+
*
|
|
16
|
+
* @module orchestrator/bypass
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const logger = require("../logger");
|
|
20
|
+
|
|
21
|
+
/** Flatten Anthropic content (string | block[]) into plain text. */
|
|
22
|
+
function getText(content) {
|
|
23
|
+
if (typeof content === "string") return content;
|
|
24
|
+
if (Array.isArray(content)) {
|
|
25
|
+
return content
|
|
26
|
+
.filter((b) => b && b.type === "text" && typeof b.text === "string")
|
|
27
|
+
.map((b) => b.text)
|
|
28
|
+
.join(" ");
|
|
29
|
+
}
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Flatten the top-level Anthropic `system` field (string | block[]). */
|
|
34
|
+
function getSystemText(system) {
|
|
35
|
+
if (typeof system === "string") return system;
|
|
36
|
+
if (Array.isArray(system)) {
|
|
37
|
+
return system
|
|
38
|
+
.filter((s) => s && s.type === "text" && typeof s.text === "string")
|
|
39
|
+
.map((s) => s.text)
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decide whether a request is a bypassable Claude CLI housekeeping call.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} args
|
|
49
|
+
* @param {object} args.payload - The Anthropic request body.
|
|
50
|
+
* @param {object} [args.headers] - Lowercased request headers.
|
|
51
|
+
* @returns {{kind: string, text: string}|null} bypass descriptor or null.
|
|
52
|
+
*/
|
|
53
|
+
function detectBypass({ payload, headers = {} }) {
|
|
54
|
+
if (!payload || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Only bypass Claude CLI traffic — other clients use these endpoints for
|
|
59
|
+
// real work and must never receive a canned response.
|
|
60
|
+
const ua = String(headers["user-agent"] || "").toLowerCase();
|
|
61
|
+
if (!ua.includes("claude-cli")) return null;
|
|
62
|
+
|
|
63
|
+
const messages = payload.messages;
|
|
64
|
+
const lastMsg = messages[messages.length - 1];
|
|
65
|
+
|
|
66
|
+
// Pattern 1: Title prefill — the CLI seeds an assistant turn with just "{"
|
|
67
|
+
// to coax a JSON object out of the model.
|
|
68
|
+
if (lastMsg?.role === "assistant") {
|
|
69
|
+
const firstBlockText =
|
|
70
|
+
Array.isArray(lastMsg.content) && lastMsg.content[0]?.type === "text"
|
|
71
|
+
? lastMsg.content[0].text
|
|
72
|
+
: typeof lastMsg.content === "string"
|
|
73
|
+
? lastMsg.content
|
|
74
|
+
: "";
|
|
75
|
+
if (firstBlockText.trim() === "{") {
|
|
76
|
+
return { kind: "title_prefill", text: "{}" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Pattern 2: Topic/title extraction — system prompt asks for isNewTopic.
|
|
81
|
+
// Synthesize a title from the first user message instead of calling a model.
|
|
82
|
+
const systemText = getSystemText(payload.system);
|
|
83
|
+
if (systemText.includes("isNewTopic")) {
|
|
84
|
+
const userMsg = messages.find((m) => m.role === "user");
|
|
85
|
+
const userText = getText(userMsg?.content).trim();
|
|
86
|
+
const title = userText.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
|
|
87
|
+
return {
|
|
88
|
+
kind: "title_extraction",
|
|
89
|
+
text: JSON.stringify({ isNewTopic: true, title }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Pattern 3: Warmup / count probes — a single short user message.
|
|
94
|
+
if (messages.length === 1 && messages[0]?.role === "user") {
|
|
95
|
+
const firstText = getText(messages[0].content).trim();
|
|
96
|
+
if (firstText === "Warmup" || firstText === "count") {
|
|
97
|
+
return { kind: firstText.toLowerCase(), text: "OK" };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the processMessage-shaped response for a bypass descriptor.
|
|
106
|
+
* Matches the `{ status, body, terminationReason }` contract the router
|
|
107
|
+
* consumes (same shape as the prompt-cache early returns).
|
|
108
|
+
*
|
|
109
|
+
* @param {{kind: string, text: string}} bypass
|
|
110
|
+
* @param {string} model - Model id to echo back.
|
|
111
|
+
* @returns {{status: number, body: object, terminationReason: string}}
|
|
112
|
+
*/
|
|
113
|
+
function buildBypassResponse(bypass, model) {
|
|
114
|
+
logger.info({ kind: bypass.kind }, "[Bypass] Short-circuiting CLI housekeeping request");
|
|
115
|
+
return {
|
|
116
|
+
status: 200,
|
|
117
|
+
body: {
|
|
118
|
+
id: `msg_bypass_${Date.now()}`,
|
|
119
|
+
type: "message",
|
|
120
|
+
role: "assistant",
|
|
121
|
+
content: [{ type: "text", text: bypass.text }],
|
|
122
|
+
model: model || "claude-3-unknown",
|
|
123
|
+
stop_reason: "end_turn",
|
|
124
|
+
stop_sequence: null,
|
|
125
|
+
usage: { input_tokens: 1, output_tokens: 1 },
|
|
126
|
+
lynkr_bypass: { kind: bypass.kind },
|
|
127
|
+
},
|
|
128
|
+
terminationReason: `bypass_${bypass.kind}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
detectBypass,
|
|
134
|
+
buildBypassResponse,
|
|
135
|
+
};
|