lynkr 7.2.4 → 8.0.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 +2 -2
- package/config/model-tiers.json +89 -0
- package/docs/docs.html +1 -0
- package/docs/index.md +7 -0
- package/docs/toon-integration-spec.md +130 -0
- package/documentation/README.md +3 -2
- package/documentation/claude-code-cli.md +23 -16
- package/documentation/cursor-integration.md +17 -14
- package/documentation/docker.md +11 -4
- package/documentation/embeddings.md +7 -5
- package/documentation/faq.md +66 -12
- package/documentation/features.md +22 -15
- package/documentation/installation.md +66 -14
- package/documentation/production.md +43 -8
- package/documentation/providers.md +145 -42
- package/documentation/routing.md +476 -0
- package/documentation/token-optimization.md +7 -5
- package/documentation/troubleshooting.md +81 -5
- package/install.sh +6 -1
- package/package.json +5 -3
- package/scripts/setup.js +0 -1
- package/src/agents/executor.js +14 -6
- package/src/api/middleware/session.js +15 -2
- package/src/api/openai-router.js +130 -37
- package/src/api/providers-handler.js +15 -1
- package/src/api/router.js +107 -2
- package/src/budget/index.js +4 -3
- package/src/clients/databricks.js +431 -234
- package/src/clients/gpt-utils.js +181 -0
- package/src/clients/ollama-utils.js +66 -140
- package/src/clients/routing.js +0 -1
- package/src/clients/standard-tools.js +82 -5
- package/src/config/index.js +119 -35
- package/src/context/toon.js +173 -0
- package/src/headroom/launcher.js +8 -3
- package/src/logger/index.js +23 -0
- package/src/orchestrator/index.js +765 -212
- package/src/routing/agentic-detector.js +320 -0
- package/src/routing/complexity-analyzer.js +202 -2
- package/src/routing/cost-optimizer.js +305 -0
- package/src/routing/index.js +168 -159
- package/src/routing/model-registry.js +437 -0
- package/src/routing/model-tiers.js +365 -0
- package/src/server.js +2 -2
- package/src/sessions/cleanup.js +3 -3
- package/src/sessions/record.js +10 -1
- package/src/sessions/store.js +7 -2
- package/src/tools/agent-task.js +48 -1
- package/src/tools/index.js +15 -2
- package/src/tools/workspace.js +35 -4
- package/src/workspace/index.js +30 -0
- package/te +11622 -0
- package/test/README.md +1 -1
- package/test/azure-openai-config.test.js +17 -8
- package/test/azure-openai-integration.test.js +7 -1
- package/test/azure-openai-routing.test.js +41 -43
- package/test/bedrock-integration.test.js +18 -32
- package/test/hybrid-routing-integration.test.js +35 -20
- package/test/hybrid-routing-performance.test.js +74 -64
- package/test/llamacpp-integration.test.js +28 -9
- package/test/lmstudio-integration.test.js +20 -8
- package/test/openai-integration.test.js +17 -20
- package/test/performance-tests.js +1 -1
- package/test/routing.test.js +65 -59
- package/test/toon-compression.test.js +131 -0
- package/CLAWROUTER_ROUTING_PLAN.md +0 -910
- package/ROUTER_COMPARISON.md +0 -173
- package/TIER_ROUTING_PLAN.md +0 -771
package/src/config/index.js
CHANGED
|
@@ -62,7 +62,7 @@ function resolveConfigPath(targetPath) {
|
|
|
62
62
|
return path.resolve(normalised);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex"]);
|
|
65
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex", "moonshot"]);
|
|
66
66
|
const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
67
67
|
|
|
68
68
|
// Validate MODEL_PROVIDER early with a clear error message
|
|
@@ -132,16 +132,24 @@ const zaiApiKey = process.env.ZAI_API_KEY?.trim() || null;
|
|
|
132
132
|
const zaiEndpoint = process.env.ZAI_ENDPOINT?.trim() || "https://api.z.ai/api/anthropic/v1/messages";
|
|
133
133
|
const zaiModel = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
|
|
134
134
|
|
|
135
|
+
// Moonshot AI (Kimi) configuration - OpenAI-compatible API
|
|
136
|
+
const moonshotApiKey = process.env.MOONSHOT_API_KEY?.trim() || null;
|
|
137
|
+
const moonshotEndpoint = process.env.MOONSHOT_ENDPOINT?.trim() || "https://api.moonshot.ai/v1/chat/completions";
|
|
138
|
+
const moonshotModel = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview";
|
|
139
|
+
|
|
135
140
|
// Vertex AI (Google Gemini) configuration
|
|
136
141
|
const vertexApiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
|
|
137
142
|
const vertexModel = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash";
|
|
138
143
|
|
|
144
|
+
// Suggestion mode model override
|
|
145
|
+
// Values: "default" (use MODEL_DEFAULT), "none" (skip LLM call), or a model name
|
|
146
|
+
const suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
|
|
147
|
+
|
|
139
148
|
// Hot reload configuration
|
|
140
149
|
const hotReloadEnabled = process.env.HOT_RELOAD_ENABLED !== "false"; // default true
|
|
141
150
|
const hotReloadDebounceMs = Number.parseInt(process.env.HOT_RELOAD_DEBOUNCE_MS ?? "1000", 10);
|
|
142
151
|
|
|
143
|
-
//
|
|
144
|
-
const preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
152
|
+
// Routing configuration
|
|
145
153
|
const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true
|
|
146
154
|
const ollamaMaxToolsForRouting = Number.parseInt(
|
|
147
155
|
process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING ?? "3",
|
|
@@ -200,6 +208,12 @@ const tokenBudgetWarning = Number.parseInt(process.env.TOKEN_BUDGET_WARNING ?? "
|
|
|
200
208
|
const tokenBudgetMax = Number.parseInt(process.env.TOKEN_BUDGET_MAX ?? "180000", 10);
|
|
201
209
|
const tokenBudgetEnforcement = process.env.TOKEN_BUDGET_ENFORCEMENT !== "false"; // default true
|
|
202
210
|
|
|
211
|
+
// TOON payload compression (opt-in)
|
|
212
|
+
const toonEnabled = process.env.TOON_ENABLED === "true"; // default false
|
|
213
|
+
const toonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
|
|
214
|
+
const toonFailOpen = process.env.TOON_FAIL_OPEN !== "false"; // default true
|
|
215
|
+
const toonLogStats = process.env.TOON_LOG_STATS !== "false"; // default true
|
|
216
|
+
|
|
203
217
|
// Smart tool selection configuration (always enabled)
|
|
204
218
|
const smartToolSelectionMode = (process.env.SMART_TOOL_SELECTION_MODE ?? "heuristic").toLowerCase();
|
|
205
219
|
const smartToolSelectionTokenBudget = Number.parseInt(
|
|
@@ -301,37 +315,39 @@ if (modelProvider === "bedrock" && !bedrockApiKey) {
|
|
|
301
315
|
);
|
|
302
316
|
}
|
|
303
317
|
|
|
304
|
-
//
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
if (fallbackEnabled && !SUPPORTED_MODEL_PROVIDERS.has(fallbackProvider)) {
|
|
310
|
-
throw new Error(
|
|
311
|
-
`FALLBACK_PROVIDER must be one of: ${Array.from(SUPPORTED_MODEL_PROVIDERS).join(", ")}`
|
|
312
|
-
);
|
|
313
|
-
}
|
|
318
|
+
// Deprecation warning for PREFER_OLLAMA
|
|
319
|
+
if (process.env.PREFER_OLLAMA) {
|
|
320
|
+
console.warn('[DEPRECATION] PREFER_OLLAMA is removed. Use TIER_* env vars for routing. See documentation/routing.md');
|
|
321
|
+
}
|
|
314
322
|
|
|
315
|
-
|
|
323
|
+
// Warn about misconfigured fallback provider (only when tier routing is active,
|
|
324
|
+
// since that's the only path that triggers provider fallback)
|
|
325
|
+
const tiersConfigured = !!(
|
|
326
|
+
process.env.TIER_SIMPLE?.trim() &&
|
|
327
|
+
process.env.TIER_MEDIUM?.trim() &&
|
|
328
|
+
process.env.TIER_COMPLEX?.trim() &&
|
|
329
|
+
process.env.TIER_REASONING?.trim()
|
|
330
|
+
);
|
|
331
|
+
if (fallbackEnabled && tiersConfigured) {
|
|
316
332
|
const localProviders = ["ollama", "llamacpp", "lmstudio"];
|
|
317
|
-
if (
|
|
333
|
+
if (localProviders.includes(fallbackProvider)) {
|
|
318
334
|
throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, openai, bedrock`);
|
|
319
335
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
336
|
+
let fallbackMisconfigured = false;
|
|
337
|
+
if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
|
|
338
|
+
fallbackMisconfigured = true;
|
|
339
|
+
}
|
|
340
|
+
if (fallbackProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) {
|
|
341
|
+
fallbackMisconfigured = true;
|
|
342
|
+
}
|
|
343
|
+
if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
|
|
344
|
+
fallbackMisconfigured = true;
|
|
345
|
+
}
|
|
346
|
+
if (fallbackProvider === "bedrock" && !bedrockApiKey) {
|
|
347
|
+
fallbackMisconfigured = true;
|
|
348
|
+
}
|
|
349
|
+
if (fallbackMisconfigured) {
|
|
350
|
+
console.warn(`[WARN] FALLBACK_PROVIDER='${fallbackProvider}' is enabled but missing credentials. Fallback will not work until configured.`);
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
353
|
|
|
@@ -589,6 +605,11 @@ var config = {
|
|
|
589
605
|
apiKey: vertexApiKey,
|
|
590
606
|
model: vertexModel,
|
|
591
607
|
},
|
|
608
|
+
moonshot: {
|
|
609
|
+
apiKey: moonshotApiKey,
|
|
610
|
+
endpoint: moonshotEndpoint,
|
|
611
|
+
model: moonshotModel,
|
|
612
|
+
},
|
|
592
613
|
hotReload: {
|
|
593
614
|
enabled: hotReloadEnabled,
|
|
594
615
|
debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs,
|
|
@@ -596,8 +617,7 @@ var config = {
|
|
|
596
617
|
modelProvider: {
|
|
597
618
|
type: modelProvider,
|
|
598
619
|
defaultModel,
|
|
599
|
-
|
|
600
|
-
preferOllama,
|
|
620
|
+
suggestionModeModel,
|
|
601
621
|
fallbackEnabled,
|
|
602
622
|
ollamaMaxToolsForRouting,
|
|
603
623
|
openRouterMaxToolsForRouting,
|
|
@@ -615,6 +635,13 @@ var config = {
|
|
|
615
635
|
},
|
|
616
636
|
logger: {
|
|
617
637
|
level: process.env.LOG_LEVEL ?? "info",
|
|
638
|
+
file: {
|
|
639
|
+
enabled: process.env.LOG_FILE_ENABLED === "true",
|
|
640
|
+
path: process.env.LOG_FILE_PATH ?? path.join(process.cwd(), "logs", "lynkr.log"),
|
|
641
|
+
level: process.env.LOG_FILE_LEVEL ?? "debug", // File captures everything
|
|
642
|
+
frequency: process.env.LOG_FILE_FREQUENCY ?? "daily", // daily | hourly | <milliseconds>
|
|
643
|
+
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES ?? "14", 10),
|
|
644
|
+
},
|
|
618
645
|
},
|
|
619
646
|
sessionStore: {
|
|
620
647
|
dbPath: sessionDbPath,
|
|
@@ -699,8 +726,8 @@ var config = {
|
|
|
699
726
|
semanticCache: {
|
|
700
727
|
enabled: process.env.SEMANTIC_CACHE_ENABLED !== 'false', // Disable via env if needed
|
|
701
728
|
similarityThreshold: parseFloat(process.env.SEMANTIC_CACHE_THRESHOLD || '0.95'), // Higher threshold
|
|
702
|
-
maxEntries: 500
|
|
703
|
-
ttlMs:
|
|
729
|
+
maxEntries: Number.parseInt(process.env.SEMANTIC_CACHE_MAX_ENTRIES ?? "50", 10), // Reduced from 500 to prevent memory bloat
|
|
730
|
+
ttlMs: Number.parseInt(process.env.SEMANTIC_CACHE_TTL_MS ?? "300000", 10), // 5 minutes (was 1 hour)
|
|
704
731
|
},
|
|
705
732
|
agents: {
|
|
706
733
|
enabled: agentsEnabled,
|
|
@@ -760,6 +787,12 @@ var config = {
|
|
|
760
787
|
max: tokenBudgetMax,
|
|
761
788
|
enforcement: tokenBudgetEnforcement,
|
|
762
789
|
},
|
|
790
|
+
toon: {
|
|
791
|
+
enabled: toonEnabled,
|
|
792
|
+
minBytes: Number.isNaN(toonMinBytes) ? 4096 : toonMinBytes,
|
|
793
|
+
failOpen: toonFailOpen,
|
|
794
|
+
logStats: toonLogStats,
|
|
795
|
+
},
|
|
763
796
|
smartToolSelection: {
|
|
764
797
|
enabled: true, // HARDCODED - always enabled
|
|
765
798
|
mode: smartToolSelectionMode,
|
|
@@ -852,6 +885,23 @@ var config = {
|
|
|
852
885
|
taskTimeoutMs: Number.isNaN(workerTaskTimeoutMs) ? 5000 : workerTaskTimeoutMs,
|
|
853
886
|
offloadThresholdBytes: Number.isNaN(workerOffloadThresholdBytes) ? 10000 : workerOffloadThresholdBytes,
|
|
854
887
|
},
|
|
888
|
+
|
|
889
|
+
// Intelligent Routing
|
|
890
|
+
routing: {
|
|
891
|
+
weightedScoring: true,
|
|
892
|
+
costOptimization: true,
|
|
893
|
+
agenticDetection: true,
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
// Model Tier Configuration (REQUIRED)
|
|
897
|
+
// Format: TIER_<LEVEL>=provider:model (e.g., TIER_SIMPLE=ollama:llama3.2)
|
|
898
|
+
modelTiers: {
|
|
899
|
+
enabled: true,
|
|
900
|
+
SIMPLE: process.env.TIER_SIMPLE?.trim() || null,
|
|
901
|
+
MEDIUM: process.env.TIER_MEDIUM?.trim() || null,
|
|
902
|
+
COMPLEX: process.env.TIER_COMPLEX?.trim() || null,
|
|
903
|
+
REASONING: process.env.TIER_REASONING?.trim() || null,
|
|
904
|
+
},
|
|
855
905
|
};
|
|
856
906
|
|
|
857
907
|
/**
|
|
@@ -876,15 +926,23 @@ function reloadConfig() {
|
|
|
876
926
|
config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
|
|
877
927
|
config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
|
|
878
928
|
config.vertex.model = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash";
|
|
929
|
+
config.moonshot.apiKey = process.env.MOONSHOT_API_KEY?.trim() || null;
|
|
930
|
+
config.moonshot.model = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview";
|
|
879
931
|
|
|
880
932
|
// Model provider settings
|
|
881
933
|
const newProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
882
934
|
if (SUPPORTED_MODEL_PROVIDERS.has(newProvider)) {
|
|
883
935
|
config.modelProvider.type = newProvider;
|
|
884
936
|
}
|
|
885
|
-
config.modelProvider.preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
886
937
|
config.modelProvider.fallbackEnabled = process.env.FALLBACK_ENABLED !== "false";
|
|
887
938
|
config.modelProvider.fallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase();
|
|
939
|
+
config.modelProvider.suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
|
|
940
|
+
|
|
941
|
+
config.toon.enabled = process.env.TOON_ENABLED === "true";
|
|
942
|
+
const newToonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
|
|
943
|
+
config.toon.minBytes = Number.isNaN(newToonMinBytes) ? 4096 : newToonMinBytes;
|
|
944
|
+
config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false";
|
|
945
|
+
config.toon.logStats = process.env.TOON_LOG_STATS !== "false";
|
|
888
946
|
|
|
889
947
|
// Log level
|
|
890
948
|
config.logger.level = process.env.LOG_LEVEL ?? "info";
|
|
@@ -896,4 +954,30 @@ function reloadConfig() {
|
|
|
896
954
|
// Make config mutable for hot reload
|
|
897
955
|
config.reloadConfig = reloadConfig;
|
|
898
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Check if any TIER_* value references Ollama (starts with "ollama:")
|
|
959
|
+
* Used by server.js to decide whether to wait for Ollama at startup.
|
|
960
|
+
*/
|
|
961
|
+
config.tiersReferenceOllama = function tiersReferenceOllama() {
|
|
962
|
+
const tiers = config.modelTiers;
|
|
963
|
+
if (!tiers?.enabled) return false;
|
|
964
|
+
return [tiers.SIMPLE, tiers.MEDIUM, tiers.COMPLEX, tiers.REASONING]
|
|
965
|
+
.some(v => typeof v === 'string' && v.startsWith('ollama:'));
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// Validate TIER_* configuration (warn if missing, don't crash)
|
|
969
|
+
const missingTiers = [];
|
|
970
|
+
if (!config.modelTiers.SIMPLE) missingTiers.push('TIER_SIMPLE');
|
|
971
|
+
if (!config.modelTiers.MEDIUM) missingTiers.push('TIER_MEDIUM');
|
|
972
|
+
if (!config.modelTiers.COMPLEX) missingTiers.push('TIER_COMPLEX');
|
|
973
|
+
if (!config.modelTiers.REASONING) missingTiers.push('TIER_REASONING');
|
|
974
|
+
|
|
975
|
+
if (missingTiers.length > 0) {
|
|
976
|
+
config.modelTiers.enabled = false;
|
|
977
|
+
console.warn(
|
|
978
|
+
`[WARN] Missing tier configuration: ${missingTiers.join(', ')} — tiered routing disabled.\n` +
|
|
979
|
+
` Set TIER_<LEVEL>=provider:model to enable (e.g., TIER_SIMPLE=ollama:llama3.2)`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
899
983
|
module.exports = config;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const logger = require("../logger");
|
|
2
|
+
|
|
3
|
+
let cachedEncode;
|
|
4
|
+
let cachedLoadError;
|
|
5
|
+
let warnedMissingDependency = false;
|
|
6
|
+
|
|
7
|
+
function normaliseSettings(settings = {}) {
|
|
8
|
+
const minBytesRaw =
|
|
9
|
+
typeof settings.minBytes === "number" ? settings.minBytes : Number.parseInt(settings.minBytes ?? "4096", 10);
|
|
10
|
+
return {
|
|
11
|
+
enabled: settings.enabled === true,
|
|
12
|
+
minBytes: Number.isFinite(minBytesRaw) && minBytesRaw > 0 ? minBytesRaw : 4096,
|
|
13
|
+
failOpen: settings.failOpen !== false,
|
|
14
|
+
logStats: settings.logStats !== false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveEncodeFn(overrideEncode) {
|
|
19
|
+
if (typeof overrideEncode === "function") return overrideEncode;
|
|
20
|
+
if (cachedEncode !== undefined) return cachedEncode;
|
|
21
|
+
try {
|
|
22
|
+
const toon = require("@toon-format/toon");
|
|
23
|
+
cachedEncode = typeof toon?.encode === "function" ? toon.encode : null;
|
|
24
|
+
cachedLoadError = cachedEncode ? null : new Error("Missing encode() export from @toon-format/toon");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
cachedEncode = null;
|
|
27
|
+
cachedLoadError = err;
|
|
28
|
+
}
|
|
29
|
+
return cachedEncode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function looksLikeJsonObjectOrArray(text) {
|
|
33
|
+
if (typeof text !== "string") return false;
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
if (trimmed.length < 2) return false;
|
|
36
|
+
return (
|
|
37
|
+
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
38
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeJsonParse(text) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toToonString(encodeFn, value) {
|
|
51
|
+
const encoded = encodeFn(value);
|
|
52
|
+
if (typeof encoded === "string") return encoded;
|
|
53
|
+
if (encoded && typeof encoded[Symbol.iterator] === "function") {
|
|
54
|
+
return Array.from(encoded).join("\n");
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function compressStringContent(content, cfg, encodeFn, stats) {
|
|
60
|
+
if (typeof content !== "string") return content;
|
|
61
|
+
|
|
62
|
+
const originalBytes = Buffer.byteLength(content, "utf8");
|
|
63
|
+
if (originalBytes < cfg.minBytes) {
|
|
64
|
+
stats.skippedBySize += 1;
|
|
65
|
+
return content;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
stats.candidateCount += 1;
|
|
69
|
+
if (!looksLikeJsonObjectOrArray(content)) {
|
|
70
|
+
stats.skippedByShape += 1;
|
|
71
|
+
return content;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parsed = safeJsonParse(content);
|
|
75
|
+
if (!parsed || typeof parsed !== "object") {
|
|
76
|
+
stats.skippedByParse += 1;
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const toonText = toToonString(encodeFn, parsed);
|
|
81
|
+
if (typeof toonText !== "string" || toonText.trim().length === 0) {
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const compressedBytes = Buffer.byteLength(toonText, "utf8");
|
|
86
|
+
stats.convertedCount += 1;
|
|
87
|
+
stats.originalBytes += originalBytes;
|
|
88
|
+
stats.compressedBytes += compressedBytes;
|
|
89
|
+
return toonText;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyToonCompression(payload, settings = {}, options = {}) {
|
|
93
|
+
const cfg = normaliseSettings(settings);
|
|
94
|
+
const stats = {
|
|
95
|
+
enabled: cfg.enabled,
|
|
96
|
+
available: true,
|
|
97
|
+
convertedCount: 0,
|
|
98
|
+
candidateCount: 0,
|
|
99
|
+
skippedBySize: 0,
|
|
100
|
+
skippedByShape: 0,
|
|
101
|
+
skippedByParse: 0,
|
|
102
|
+
failureCount: 0,
|
|
103
|
+
originalBytes: 0,
|
|
104
|
+
compressedBytes: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!cfg.enabled) return { payload, stats };
|
|
108
|
+
if (!payload || !Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
109
|
+
return { payload, stats };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const encodeFn = resolveEncodeFn(options.encode);
|
|
113
|
+
if (typeof encodeFn !== "function") {
|
|
114
|
+
stats.available = false;
|
|
115
|
+
const err = cachedLoadError ?? new Error("TOON encoder unavailable");
|
|
116
|
+
if (!cfg.failOpen) throw err;
|
|
117
|
+
if (!warnedMissingDependency) {
|
|
118
|
+
logger.warn(
|
|
119
|
+
{ error: err.message },
|
|
120
|
+
"TOON enabled but encoder dependency is unavailable; falling back to JSON",
|
|
121
|
+
);
|
|
122
|
+
warnedMissingDependency = true;
|
|
123
|
+
}
|
|
124
|
+
return { payload, stats };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const message of payload.messages) {
|
|
128
|
+
if (!message || typeof message !== "object") continue;
|
|
129
|
+
if (message.role === "tool") continue; // Never mutate machine-executed protocol payloads
|
|
130
|
+
try {
|
|
131
|
+
if (typeof message.content === "string") {
|
|
132
|
+
message.content = compressStringContent(message.content, cfg, encodeFn, stats);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!Array.isArray(message.content)) continue;
|
|
137
|
+
for (const block of message.content) {
|
|
138
|
+
if (!block || typeof block !== "object") continue;
|
|
139
|
+
|
|
140
|
+
// Keep protocol blocks untouched. Only compress user-language text fields.
|
|
141
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
142
|
+
block.text = compressStringContent(block.text, cfg, encodeFn, stats);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (block.type === "input_text" && typeof block.input_text === "string") {
|
|
147
|
+
block.input_text = compressStringContent(block.input_text, cfg, encodeFn, stats);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
stats.failureCount += 1;
|
|
152
|
+
if (!cfg.failOpen) throw err;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (cfg.logStats && stats.convertedCount > 0) {
|
|
157
|
+
logger.info(
|
|
158
|
+
{
|
|
159
|
+
convertedCount: stats.convertedCount,
|
|
160
|
+
candidateCount: stats.candidateCount,
|
|
161
|
+
originalBytes: stats.originalBytes,
|
|
162
|
+
compressedBytes: stats.compressedBytes,
|
|
163
|
+
},
|
|
164
|
+
"TOON compression applied to message context",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { payload, stats };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
applyToonCompression,
|
|
173
|
+
};
|
package/src/headroom/launcher.js
CHANGED
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
* Provides automatic container creation, health checking, and graceful shutdown.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
let Docker;
|
|
9
|
+
try {
|
|
10
|
+
Docker = require("dockerode");
|
|
11
|
+
} catch {
|
|
12
|
+
Docker = null;
|
|
13
|
+
}
|
|
9
14
|
const logger = require("../logger");
|
|
10
15
|
const config = require("../config");
|
|
11
16
|
|
|
12
|
-
// Initialize Docker client
|
|
13
|
-
const docker = new Docker();
|
|
17
|
+
// Initialize Docker client (only if dockerode is available)
|
|
18
|
+
const docker = Docker ? new Docker() : null;
|
|
14
19
|
|
|
15
20
|
// Launcher state
|
|
16
21
|
let containerInstance = null;
|
package/src/logger/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const pino = require("pino");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
2
4
|
const config = require("../config");
|
|
3
5
|
const { createOversizedErrorStream } = require("./oversized-error-stream");
|
|
4
6
|
|
|
@@ -64,6 +66,27 @@ streams.push({
|
|
|
64
66
|
: process.stdout,
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
// File rotation stream (if enabled via LOG_FILE_ENABLED=true)
|
|
70
|
+
if (config.logger.file?.enabled) {
|
|
71
|
+
const fileConfig = config.logger.file;
|
|
72
|
+
// Ensure log directory exists
|
|
73
|
+
const logDir = path.dirname(fileConfig.path);
|
|
74
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
75
|
+
|
|
76
|
+
streams.push({
|
|
77
|
+
level: fileConfig.level,
|
|
78
|
+
stream: pino.transport({
|
|
79
|
+
target: "pino-roll",
|
|
80
|
+
options: {
|
|
81
|
+
file: fileConfig.path,
|
|
82
|
+
frequency: fileConfig.frequency,
|
|
83
|
+
limit: { count: fileConfig.maxFiles },
|
|
84
|
+
mkdir: true,
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
// Oversized error stream (if enabled)
|
|
68
91
|
if (config.oversizedErrorLogging?.enabled) {
|
|
69
92
|
streams.push({
|