lynkr 7.2.5 → 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 +4 -2
- 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 +76 -3
- package/src/config/index.js +113 -35
- package/src/context/toon.js +173 -0
- package/src/logger/index.js +23 -0
- package/src/orchestrator/index.js +686 -211
- 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-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/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,6 +132,11 @@ 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";
|
|
@@ -144,8 +149,7 @@ const suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").tri
|
|
|
144
149
|
const hotReloadEnabled = process.env.HOT_RELOAD_ENABLED !== "false"; // default true
|
|
145
150
|
const hotReloadDebounceMs = Number.parseInt(process.env.HOT_RELOAD_DEBOUNCE_MS ?? "1000", 10);
|
|
146
151
|
|
|
147
|
-
//
|
|
148
|
-
const preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
152
|
+
// Routing configuration
|
|
149
153
|
const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true
|
|
150
154
|
const ollamaMaxToolsForRouting = Number.parseInt(
|
|
151
155
|
process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING ?? "3",
|
|
@@ -204,6 +208,12 @@ const tokenBudgetWarning = Number.parseInt(process.env.TOKEN_BUDGET_WARNING ?? "
|
|
|
204
208
|
const tokenBudgetMax = Number.parseInt(process.env.TOKEN_BUDGET_MAX ?? "180000", 10);
|
|
205
209
|
const tokenBudgetEnforcement = process.env.TOKEN_BUDGET_ENFORCEMENT !== "false"; // default true
|
|
206
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
|
+
|
|
207
217
|
// Smart tool selection configuration (always enabled)
|
|
208
218
|
const smartToolSelectionMode = (process.env.SMART_TOOL_SELECTION_MODE ?? "heuristic").toLowerCase();
|
|
209
219
|
const smartToolSelectionTokenBudget = Number.parseInt(
|
|
@@ -305,37 +315,39 @@ if (modelProvider === "bedrock" && !bedrockApiKey) {
|
|
|
305
315
|
);
|
|
306
316
|
}
|
|
307
317
|
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
if (fallbackEnabled && !SUPPORTED_MODEL_PROVIDERS.has(fallbackProvider)) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`FALLBACK_PROVIDER must be one of: ${Array.from(SUPPORTED_MODEL_PROVIDERS).join(", ")}`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
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
|
+
}
|
|
318
322
|
|
|
319
|
-
|
|
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) {
|
|
320
332
|
const localProviders = ["ollama", "llamacpp", "lmstudio"];
|
|
321
|
-
if (
|
|
333
|
+
if (localProviders.includes(fallbackProvider)) {
|
|
322
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`);
|
|
323
335
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
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.`);
|
|
339
351
|
}
|
|
340
352
|
}
|
|
341
353
|
|
|
@@ -593,6 +605,11 @@ var config = {
|
|
|
593
605
|
apiKey: vertexApiKey,
|
|
594
606
|
model: vertexModel,
|
|
595
607
|
},
|
|
608
|
+
moonshot: {
|
|
609
|
+
apiKey: moonshotApiKey,
|
|
610
|
+
endpoint: moonshotEndpoint,
|
|
611
|
+
model: moonshotModel,
|
|
612
|
+
},
|
|
596
613
|
hotReload: {
|
|
597
614
|
enabled: hotReloadEnabled,
|
|
598
615
|
debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs,
|
|
@@ -601,8 +618,6 @@ var config = {
|
|
|
601
618
|
type: modelProvider,
|
|
602
619
|
defaultModel,
|
|
603
620
|
suggestionModeModel,
|
|
604
|
-
// Hybrid routing settings
|
|
605
|
-
preferOllama,
|
|
606
621
|
fallbackEnabled,
|
|
607
622
|
ollamaMaxToolsForRouting,
|
|
608
623
|
openRouterMaxToolsForRouting,
|
|
@@ -620,6 +635,13 @@ var config = {
|
|
|
620
635
|
},
|
|
621
636
|
logger: {
|
|
622
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
|
+
},
|
|
623
645
|
},
|
|
624
646
|
sessionStore: {
|
|
625
647
|
dbPath: sessionDbPath,
|
|
@@ -704,8 +726,8 @@ var config = {
|
|
|
704
726
|
semanticCache: {
|
|
705
727
|
enabled: process.env.SEMANTIC_CACHE_ENABLED !== 'false', // Disable via env if needed
|
|
706
728
|
similarityThreshold: parseFloat(process.env.SEMANTIC_CACHE_THRESHOLD || '0.95'), // Higher threshold
|
|
707
|
-
maxEntries: 500
|
|
708
|
-
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)
|
|
709
731
|
},
|
|
710
732
|
agents: {
|
|
711
733
|
enabled: agentsEnabled,
|
|
@@ -765,6 +787,12 @@ var config = {
|
|
|
765
787
|
max: tokenBudgetMax,
|
|
766
788
|
enforcement: tokenBudgetEnforcement,
|
|
767
789
|
},
|
|
790
|
+
toon: {
|
|
791
|
+
enabled: toonEnabled,
|
|
792
|
+
minBytes: Number.isNaN(toonMinBytes) ? 4096 : toonMinBytes,
|
|
793
|
+
failOpen: toonFailOpen,
|
|
794
|
+
logStats: toonLogStats,
|
|
795
|
+
},
|
|
768
796
|
smartToolSelection: {
|
|
769
797
|
enabled: true, // HARDCODED - always enabled
|
|
770
798
|
mode: smartToolSelectionMode,
|
|
@@ -857,6 +885,23 @@ var config = {
|
|
|
857
885
|
taskTimeoutMs: Number.isNaN(workerTaskTimeoutMs) ? 5000 : workerTaskTimeoutMs,
|
|
858
886
|
offloadThresholdBytes: Number.isNaN(workerOffloadThresholdBytes) ? 10000 : workerOffloadThresholdBytes,
|
|
859
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
|
+
},
|
|
860
905
|
};
|
|
861
906
|
|
|
862
907
|
/**
|
|
@@ -881,17 +926,24 @@ function reloadConfig() {
|
|
|
881
926
|
config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
|
|
882
927
|
config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
|
|
883
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";
|
|
884
931
|
|
|
885
932
|
// Model provider settings
|
|
886
933
|
const newProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
887
934
|
if (SUPPORTED_MODEL_PROVIDERS.has(newProvider)) {
|
|
888
935
|
config.modelProvider.type = newProvider;
|
|
889
936
|
}
|
|
890
|
-
config.modelProvider.preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
891
937
|
config.modelProvider.fallbackEnabled = process.env.FALLBACK_ENABLED !== "false";
|
|
892
938
|
config.modelProvider.fallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase();
|
|
893
939
|
config.modelProvider.suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
|
|
894
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";
|
|
946
|
+
|
|
895
947
|
// Log level
|
|
896
948
|
config.logger.level = process.env.LOG_LEVEL ?? "info";
|
|
897
949
|
|
|
@@ -902,4 +954,30 @@ function reloadConfig() {
|
|
|
902
954
|
// Make config mutable for hot reload
|
|
903
955
|
config.reloadConfig = reloadConfig;
|
|
904
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
|
+
|
|
905
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/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({
|