lynkr 7.2.5 → 8.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/README.md +3 -3
- package/config/model-tiers.json +89 -0
- 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 +162 -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 +99 -3
- package/src/config/index.js +133 -35
- package/src/context/toon.js +173 -0
- package/src/logger/index.js +23 -0
- package/src/orchestrator/index.js +688 -213
- 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 +4 -14
- 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 +19 -2
- package/src/tools/lazy-loader.js +7 -0
- package/src/tools/tinyfish.js +358 -0
- package/src/tools/truncate.js +1 -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/CLAWROUTER_ROUTING_PLAN.md +0 -910
- 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/ROUTER_COMPARISON.md +0 -173
- package/TIER_ROUTING_PLAN.md +0 -771
- package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
- package/docs/BingSiteAuth.xml +0 -4
- package/docs/docs-style.css +0 -478
- package/docs/docs.html +0 -197
- package/docs/google5be250e608e6da39.html +0 -1
- package/docs/index.html +0 -577
- package/docs/index.md +0 -577
- package/docs/robots.txt +0 -4
- package/docs/sitemap.xml +0 -44
- package/docs/style.css +0 -1223
- package/documentation/README.md +0 -100
- package/documentation/api.md +0 -806
- package/documentation/claude-code-cli.md +0 -672
- package/documentation/codex-cli.md +0 -397
- package/documentation/contributing.md +0 -571
- package/documentation/cursor-integration.md +0 -731
- package/documentation/docker.md +0 -867
- package/documentation/embeddings.md +0 -760
- package/documentation/faq.md +0 -659
- package/documentation/features.md +0 -396
- package/documentation/headroom.md +0 -519
- package/documentation/installation.md +0 -706
- package/documentation/memory-system.md +0 -476
- package/documentation/production.md +0 -601
- package/documentation/providers.md +0 -906
- package/documentation/testing.md +0 -629
- package/documentation/token-optimization.md +0 -323
- package/documentation/tools.md +0 -697
- package/documentation/troubleshooting.md +0 -893
- 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/test/README.md +0 -212
- package/test/azure-openai-config.test.js +0 -204
- 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 -281
- package/test/azure-openai-routing.test.js +0 -177
- package/test/azure-openai-streaming.test.js +0 -171
- package/test/bedrock-integration.test.js +0 -471
- 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 -254
- package/test/hybrid-routing-performance.test.js +0 -418
- package/test/llamacpp-integration.test.js +0 -863
- package/test/lmstudio-integration.test.js +0 -335
- 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 -686
- 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 -219
- 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/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
|
|
|
@@ -385,6 +397,14 @@ const webFetchBodyPreviewMax = Number.parseInt(process.env.WEB_FETCH_BODY_PREVIE
|
|
|
385
397
|
const webSearchRetryEnabled = process.env.WEB_SEARCH_RETRY_ENABLED !== "false"; // default true
|
|
386
398
|
const webSearchMaxRetries = Number.parseInt(process.env.WEB_SEARCH_MAX_RETRIES ?? "2", 10);
|
|
387
399
|
|
|
400
|
+
// TinyFish AI Browser Automation configuration
|
|
401
|
+
const tinyfishApiKey = process.env.TINYFISH_API_KEY?.trim() || null;
|
|
402
|
+
const tinyfishEndpoint = process.env.TINYFISH_ENDPOINT?.trim() || "https://agent.tinyfish.ai/v1/automation/run-sse";
|
|
403
|
+
const tinyfishBrowserProfile = process.env.TINYFISH_BROWSER_PROFILE?.trim() || "lite";
|
|
404
|
+
const tinyfishTimeoutMs = parseInt(process.env.TINYFISH_TIMEOUT_MS ?? "120000", 10);
|
|
405
|
+
const tinyfishProxyEnabled = process.env.TINYFISH_PROXY_ENABLED === "true";
|
|
406
|
+
const tinyfishProxyCountry = process.env.TINYFISH_PROXY_COUNTRY?.trim() || "US";
|
|
407
|
+
|
|
388
408
|
const policyMaxSteps = Number.parseInt(process.env.POLICY_MAX_STEPS ?? "8", 10);
|
|
389
409
|
const policyMaxToolCalls = Number.parseInt(process.env.POLICY_MAX_TOOL_CALLS ?? "12", 10);
|
|
390
410
|
const policyToolLoopThreshold = Number.parseInt(process.env.POLICY_TOOL_LOOP_THRESHOLD ?? "10", 10);
|
|
@@ -593,6 +613,11 @@ var config = {
|
|
|
593
613
|
apiKey: vertexApiKey,
|
|
594
614
|
model: vertexModel,
|
|
595
615
|
},
|
|
616
|
+
moonshot: {
|
|
617
|
+
apiKey: moonshotApiKey,
|
|
618
|
+
endpoint: moonshotEndpoint,
|
|
619
|
+
model: moonshotModel,
|
|
620
|
+
},
|
|
596
621
|
hotReload: {
|
|
597
622
|
enabled: hotReloadEnabled,
|
|
598
623
|
debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs,
|
|
@@ -601,8 +626,6 @@ var config = {
|
|
|
601
626
|
type: modelProvider,
|
|
602
627
|
defaultModel,
|
|
603
628
|
suggestionModeModel,
|
|
604
|
-
// Hybrid routing settings
|
|
605
|
-
preferOllama,
|
|
606
629
|
fallbackEnabled,
|
|
607
630
|
ollamaMaxToolsForRouting,
|
|
608
631
|
openRouterMaxToolsForRouting,
|
|
@@ -620,6 +643,13 @@ var config = {
|
|
|
620
643
|
},
|
|
621
644
|
logger: {
|
|
622
645
|
level: process.env.LOG_LEVEL ?? "info",
|
|
646
|
+
file: {
|
|
647
|
+
enabled: process.env.LOG_FILE_ENABLED === "true",
|
|
648
|
+
path: process.env.LOG_FILE_PATH ?? path.join(process.cwd(), "logs", "lynkr.log"),
|
|
649
|
+
level: process.env.LOG_FILE_LEVEL ?? "debug", // File captures everything
|
|
650
|
+
frequency: process.env.LOG_FILE_FREQUENCY ?? "daily", // daily | hourly | <milliseconds>
|
|
651
|
+
maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES ?? "14", 10),
|
|
652
|
+
},
|
|
623
653
|
},
|
|
624
654
|
sessionStore: {
|
|
625
655
|
dbPath: sessionDbPath,
|
|
@@ -638,6 +668,14 @@ var config = {
|
|
|
638
668
|
retryEnabled: webSearchRetryEnabled,
|
|
639
669
|
maxRetries: Number.isNaN(webSearchMaxRetries) ? 2 : webSearchMaxRetries,
|
|
640
670
|
},
|
|
671
|
+
tinyfish: {
|
|
672
|
+
apiKey: tinyfishApiKey,
|
|
673
|
+
endpoint: tinyfishEndpoint,
|
|
674
|
+
browserProfile: tinyfishBrowserProfile,
|
|
675
|
+
timeoutMs: Number.isNaN(tinyfishTimeoutMs) ? 120000 : tinyfishTimeoutMs,
|
|
676
|
+
proxyEnabled: tinyfishProxyEnabled,
|
|
677
|
+
proxyCountry: tinyfishProxyCountry,
|
|
678
|
+
},
|
|
641
679
|
policy: {
|
|
642
680
|
maxStepsPerTurn: Number.isNaN(policyMaxSteps) ? 8 : policyMaxSteps,
|
|
643
681
|
maxToolCallsPerTurn: Number.isNaN(policyMaxToolCalls) ? 12 : policyMaxToolCalls,
|
|
@@ -704,8 +742,8 @@ var config = {
|
|
|
704
742
|
semanticCache: {
|
|
705
743
|
enabled: process.env.SEMANTIC_CACHE_ENABLED !== 'false', // Disable via env if needed
|
|
706
744
|
similarityThreshold: parseFloat(process.env.SEMANTIC_CACHE_THRESHOLD || '0.95'), // Higher threshold
|
|
707
|
-
maxEntries: 500
|
|
708
|
-
ttlMs:
|
|
745
|
+
maxEntries: Number.parseInt(process.env.SEMANTIC_CACHE_MAX_ENTRIES ?? "50", 10), // Reduced from 500 to prevent memory bloat
|
|
746
|
+
ttlMs: Number.parseInt(process.env.SEMANTIC_CACHE_TTL_MS ?? "300000", 10), // 5 minutes (was 1 hour)
|
|
709
747
|
},
|
|
710
748
|
agents: {
|
|
711
749
|
enabled: agentsEnabled,
|
|
@@ -765,6 +803,12 @@ var config = {
|
|
|
765
803
|
max: tokenBudgetMax,
|
|
766
804
|
enforcement: tokenBudgetEnforcement,
|
|
767
805
|
},
|
|
806
|
+
toon: {
|
|
807
|
+
enabled: toonEnabled,
|
|
808
|
+
minBytes: Number.isNaN(toonMinBytes) ? 4096 : toonMinBytes,
|
|
809
|
+
failOpen: toonFailOpen,
|
|
810
|
+
logStats: toonLogStats,
|
|
811
|
+
},
|
|
768
812
|
smartToolSelection: {
|
|
769
813
|
enabled: true, // HARDCODED - always enabled
|
|
770
814
|
mode: smartToolSelectionMode,
|
|
@@ -857,6 +901,23 @@ var config = {
|
|
|
857
901
|
taskTimeoutMs: Number.isNaN(workerTaskTimeoutMs) ? 5000 : workerTaskTimeoutMs,
|
|
858
902
|
offloadThresholdBytes: Number.isNaN(workerOffloadThresholdBytes) ? 10000 : workerOffloadThresholdBytes,
|
|
859
903
|
},
|
|
904
|
+
|
|
905
|
+
// Intelligent Routing
|
|
906
|
+
routing: {
|
|
907
|
+
weightedScoring: true,
|
|
908
|
+
costOptimization: true,
|
|
909
|
+
agenticDetection: true,
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
// Model Tier Configuration (REQUIRED)
|
|
913
|
+
// Format: TIER_<LEVEL>=provider:model (e.g., TIER_SIMPLE=ollama:llama3.2)
|
|
914
|
+
modelTiers: {
|
|
915
|
+
enabled: true,
|
|
916
|
+
SIMPLE: process.env.TIER_SIMPLE?.trim() || null,
|
|
917
|
+
MEDIUM: process.env.TIER_MEDIUM?.trim() || null,
|
|
918
|
+
COMPLEX: process.env.TIER_COMPLEX?.trim() || null,
|
|
919
|
+
REASONING: process.env.TIER_REASONING?.trim() || null,
|
|
920
|
+
},
|
|
860
921
|
};
|
|
861
922
|
|
|
862
923
|
/**
|
|
@@ -881,17 +942,28 @@ function reloadConfig() {
|
|
|
881
942
|
config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7";
|
|
882
943
|
config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null;
|
|
883
944
|
config.vertex.model = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash";
|
|
945
|
+
config.moonshot.apiKey = process.env.MOONSHOT_API_KEY?.trim() || null;
|
|
946
|
+
config.moonshot.model = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview";
|
|
884
947
|
|
|
885
948
|
// Model provider settings
|
|
886
949
|
const newProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
887
950
|
if (SUPPORTED_MODEL_PROVIDERS.has(newProvider)) {
|
|
888
951
|
config.modelProvider.type = newProvider;
|
|
889
952
|
}
|
|
890
|
-
config.modelProvider.preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
891
953
|
config.modelProvider.fallbackEnabled = process.env.FALLBACK_ENABLED !== "false";
|
|
892
954
|
config.modelProvider.fallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase();
|
|
893
955
|
config.modelProvider.suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim();
|
|
894
956
|
|
|
957
|
+
// TinyFish config reload
|
|
958
|
+
config.tinyfish.apiKey = process.env.TINYFISH_API_KEY?.trim() || null;
|
|
959
|
+
config.tinyfish.browserProfile = process.env.TINYFISH_BROWSER_PROFILE?.trim() || "lite";
|
|
960
|
+
|
|
961
|
+
config.toon.enabled = process.env.TOON_ENABLED === "true";
|
|
962
|
+
const newToonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
|
|
963
|
+
config.toon.minBytes = Number.isNaN(newToonMinBytes) ? 4096 : newToonMinBytes;
|
|
964
|
+
config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false";
|
|
965
|
+
config.toon.logStats = process.env.TOON_LOG_STATS !== "false";
|
|
966
|
+
|
|
895
967
|
// Log level
|
|
896
968
|
config.logger.level = process.env.LOG_LEVEL ?? "info";
|
|
897
969
|
|
|
@@ -902,4 +974,30 @@ function reloadConfig() {
|
|
|
902
974
|
// Make config mutable for hot reload
|
|
903
975
|
config.reloadConfig = reloadConfig;
|
|
904
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Check if any TIER_* value references Ollama (starts with "ollama:")
|
|
979
|
+
* Used by server.js to decide whether to wait for Ollama at startup.
|
|
980
|
+
*/
|
|
981
|
+
config.tiersReferenceOllama = function tiersReferenceOllama() {
|
|
982
|
+
const tiers = config.modelTiers;
|
|
983
|
+
if (!tiers?.enabled) return false;
|
|
984
|
+
return [tiers.SIMPLE, tiers.MEDIUM, tiers.COMPLEX, tiers.REASONING]
|
|
985
|
+
.some(v => typeof v === 'string' && v.startsWith('ollama:'));
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// Validate TIER_* configuration (warn if missing, don't crash)
|
|
989
|
+
const missingTiers = [];
|
|
990
|
+
if (!config.modelTiers.SIMPLE) missingTiers.push('TIER_SIMPLE');
|
|
991
|
+
if (!config.modelTiers.MEDIUM) missingTiers.push('TIER_MEDIUM');
|
|
992
|
+
if (!config.modelTiers.COMPLEX) missingTiers.push('TIER_COMPLEX');
|
|
993
|
+
if (!config.modelTiers.REASONING) missingTiers.push('TIER_REASONING');
|
|
994
|
+
|
|
995
|
+
if (missingTiers.length > 0) {
|
|
996
|
+
config.modelTiers.enabled = false;
|
|
997
|
+
console.warn(
|
|
998
|
+
`[WARN] Missing tier configuration: ${missingTiers.join(', ')} — tiered routing disabled.\n` +
|
|
999
|
+
` Set TIER_<LEVEL>=provider:model to enable (e.g., TIER_SIMPLE=ollama:llama3.2)`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
905
1003
|
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({
|