lynkr 9.7.2 → 9.9.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.
Files changed (80) hide show
  1. package/README.md +29 -19
  2. package/bin/cli.js +11 -0
  3. package/bin/lynkr-init.js +14 -1
  4. package/bin/lynkr-usage.js +78 -0
  5. package/bin/wrap.js +60 -35
  6. package/config/difficulty-anchors.json +22 -0
  7. package/package.json +24 -3
  8. package/scripts/audit-log-reader.js +399 -0
  9. package/scripts/calibrate-thresholds.js +38 -157
  10. package/scripts/compact-dictionary.js +204 -0
  11. package/scripts/test-deduplication.js +448 -0
  12. package/scripts/ws7-anchor-replay.js +108 -0
  13. package/skills/lynkr/SKILL.md +195 -0
  14. package/src/agents/context-manager.js +18 -2
  15. package/src/agents/definitions/loader.js +90 -0
  16. package/src/agents/executor.js +24 -2
  17. package/src/agents/index.js +9 -1
  18. package/src/agents/parallel-coordinator.js +2 -2
  19. package/src/agents/reflector.js +11 -1
  20. package/src/api/middleware/loop-guard.js +87 -0
  21. package/src/api/middleware/request-logging.js +5 -64
  22. package/src/api/middleware/session.js +0 -0
  23. package/src/api/openai-router.js +120 -101
  24. package/src/api/providers-handler.js +27 -2
  25. package/src/api/router.js +450 -125
  26. package/src/budget/index.js +2 -19
  27. package/src/cache/semantic.js +9 -0
  28. package/src/clients/databricks.js +459 -146
  29. package/src/clients/gpt-utils.js +11 -105
  30. package/src/clients/openai-format.js +10 -3
  31. package/src/clients/openrouter-utils.js +49 -24
  32. package/src/clients/prompt-cache-injection.js +1 -0
  33. package/src/clients/provider-capabilities.js +1 -1
  34. package/src/clients/responses-format.js +34 -3
  35. package/src/clients/routing.js +15 -0
  36. package/src/config/index.js +36 -2
  37. package/src/context/gcf.js +275 -0
  38. package/src/context/tool-result-compressor.js +51 -9
  39. package/src/dashboard/api.js +1 -0
  40. package/src/logger/index.js +14 -1
  41. package/src/memory/search.js +12 -40
  42. package/src/memory/tools.js +3 -24
  43. package/src/orchestrator/bypass.js +4 -2
  44. package/src/orchestrator/index.js +144 -88
  45. package/src/routing/affinity-store.js +194 -0
  46. package/src/routing/agentic-detector.js +36 -6
  47. package/src/routing/bandit.js +25 -6
  48. package/src/routing/calibration.js +212 -0
  49. package/src/routing/client-profiles.js +292 -0
  50. package/src/routing/complexity-analyzer.js +48 -11
  51. package/src/routing/deescalator.js +148 -0
  52. package/src/routing/degradation.js +109 -0
  53. package/src/routing/feedback.js +157 -0
  54. package/src/routing/index.js +897 -87
  55. package/src/routing/intent-score.js +339 -0
  56. package/src/routing/interaction.js +3 -0
  57. package/src/routing/knn-router.js +70 -21
  58. package/src/routing/model-registry.js +28 -7
  59. package/src/routing/model-tiers.js +25 -2
  60. package/src/routing/reward-pipeline.js +68 -2
  61. package/src/routing/risk-analyzer.js +30 -1
  62. package/src/routing/risk-classifier.js +6 -2
  63. package/src/routing/session-affinity.js +162 -34
  64. package/src/routing/telemetry.js +298 -10
  65. package/src/routing/verifier.js +267 -0
  66. package/src/server.js +66 -21
  67. package/src/sessions/cleanup.js +17 -0
  68. package/src/tools/index.js +1 -15
  69. package/src/tools/smart-selection.js +10 -0
  70. package/src/tools/web-client.js +3 -3
  71. package/.eslintrc.cjs +0 -12
  72. package/benchmark-configs/litellm_config.yaml +0 -86
  73. package/benchmark-configs/lynkr.env +0 -48
  74. package/benchmark-configs/portkey-config.json +0 -60
  75. package/benchmark-configs/portkey-docker.sh +0 -23
  76. package/benchmark-tier-routing.js +0 -449
  77. package/funding.json +0 -110
  78. package/src/api/middleware/validation.js +0 -261
  79. package/src/routing/drift-monitor.js +0 -113
  80. package/src/workers/helpers.js +0 -185
@@ -1,89 +1,19 @@
1
1
  /**
2
- * GPT-specific utilities for handling tool calls and responses
3
- * All settings are hardcoded - no env vars required
2
+ * Detection of semantically-similar tool calls.
4
3
  *
5
- * This module addresses GPT model compatibility issues when using Azure OpenAI
6
- * through Lynkr proxy with Claude Code:
7
- * - GPT doesn't interpret "0 files found" as a final answer
8
- * - GPT retries the same tool expecting different results
9
- * - GPT needs explicit guidance on tool result interpretation
4
+ * GPT-family models often retry a tool with slightly different but
5
+ * functionally equivalent parameters instead of accepting the result;
6
+ * the orchestrator uses areSimilarToolCalls to treat those retries as
7
+ * duplicates.
10
8
  */
11
9
 
12
10
  const logger = require("../logger");
13
11
 
14
- // Hardcoded GPT settings - optimized for GPT model behavior
15
- const GPT_SETTINGS = {
16
- toolLoopThreshold: 2, // Lower than Claude's 3 to catch loops earlier
17
- enhancedFormatting: true, // Always format results explicitly for GPT
18
- similarityThreshold: 0.8, // For detecting similar (not just identical) tool calls
19
- };
20
-
21
- // Provider identifiers that use GPT models
22
- const GPT_PROVIDERS = ['azure-openai', 'openai'];
23
-
24
- /**
25
- * Check if a provider uses GPT models
26
- * @param {string} provider - Provider type (e.g., 'azure-openai', 'databricks')
27
- * @returns {boolean} - True if provider uses GPT models
28
- */
29
- function isGPTProvider(provider) {
30
- if (!provider) return false;
31
- return GPT_PROVIDERS.includes(provider.toLowerCase());
32
- }
33
-
34
- /**
35
- * Get the tool loop threshold for GPT models
36
- * @returns {number} - Threshold (2 for GPT, lower than Claude's 3)
37
- */
38
- function getGPTToolLoopThreshold() {
39
- return GPT_SETTINGS.toolLoopThreshold;
40
- }
41
-
42
- /**
43
- * Format tool result with explicit structure for GPT models
44
- * GPT models need clear, unambiguous formatting to understand tool results
45
- *
46
- * @param {string} toolName - Name of the tool that was called
47
- * @param {string} content - The tool result content
48
- * @param {Object} args - The arguments passed to the tool
49
- * @returns {string} - Formatted result with explicit status and instructions
50
- */
51
- function formatToolResultForGPT(toolName, content, args) {
52
- // Handle empty/no results explicitly - add clear messaging to prevent retries
53
- const isEmpty = !content ||
54
- content.trim() === '' ||
55
- content.includes('0 files found') ||
56
- content.includes('No matches found') ||
57
- content.includes('No results') ||
58
- content.includes('Found 0') ||
59
- /^Found \d+ files?\.$/.test(content.trim()) && content.includes('Found 0');
60
-
61
- if (isEmpty) {
62
- // Only format empty results - add explicit "don't retry" instruction
63
- return `Tool "${toolName}" completed with no results found.
64
- Query: ${JSON.stringify(args)}
65
-
66
- This is a FINAL result - do not retry this query. Respond to the user based on this outcome.`;
67
- }
68
-
69
- // For successful results, return content as-is (don't add markers that might confuse GPT)
70
- return content;
71
- }
72
-
73
- /**
74
- * Get system prompt addendum for GPT models
75
- * This teaches GPT how to properly interpret and use tools
76
- *
77
- * @returns {string} - System prompt instructions for GPT
78
- */
79
- function getGPTSystemPromptAddendum() {
80
- return `Use the Bash tool with ls command for listing files. After any tool returns results, respond to the user.`;
81
- }
12
+ // Jaccard similarity above this counts two search-tool calls as duplicates
13
+ const SIMILARITY_THRESHOLD = 0.8;
82
14
 
83
15
  /**
84
16
  * Calculate string similarity using Jaccard index
85
- * Used to detect semantically similar tool calls
86
- *
87
17
  * @param {string} s1 - First string
88
18
  * @param {string} s2 - Second string
89
19
  * @returns {number} - Similarity score between 0 and 1
@@ -110,8 +40,6 @@ function stringSimilarity(s1, s2) {
110
40
 
111
41
  /**
112
42
  * Check if two tool calls are semantically similar
113
- * GPT often retries with slightly different parameters that are functionally equivalent
114
- *
115
43
  * @param {Object} call1 - First tool call {name, arguments}
116
44
  * @param {Object} call2 - Second tool call {name, arguments}
117
45
  * @returns {boolean} - True if calls are similar enough to be considered duplicates
@@ -119,34 +47,31 @@ function stringSimilarity(s1, s2) {
119
47
  function areSimilarToolCalls(call1, call2) {
120
48
  if (!call1 || !call2) return false;
121
49
 
122
- // Must be the same tool
123
50
  const name1 = call1.function?.name ?? call1.name;
124
51
  const name2 = call2.function?.name ?? call2.name;
125
52
  if (name1 !== name2) return false;
126
53
 
127
- // Get arguments
128
54
  const args1 = call1.function?.arguments ?? call1.arguments ?? call1.input ?? {};
129
55
  const args2 = call2.function?.arguments ?? call2.arguments ?? call2.input ?? {};
130
56
 
131
- // Stringify for comparison
132
57
  const argsStr1 = typeof args1 === 'string' ? args1 : JSON.stringify(args1);
133
58
  const argsStr2 = typeof args2 === 'string' ? args2 : JSON.stringify(args2);
134
59
 
135
- // Exact match
136
60
  if (argsStr1 === argsStr2) return true;
137
61
 
138
- // For search-related tools, check semantic similarity
62
+ // Only search-style tools get fuzzy matching; mutating tools with
63
+ // near-identical args may be intentional repeats.
139
64
  const searchTools = ['grep', 'glob', 'search', 'find', 'read', 'bash', 'shell'];
140
65
  const toolName = (name1 || '').toLowerCase();
141
66
  const isSearchTool = searchTools.some(t => toolName.includes(t));
142
67
 
143
68
  if (isSearchTool) {
144
69
  const similarity = stringSimilarity(argsStr1, argsStr2);
145
- if (similarity >= GPT_SETTINGS.similarityThreshold) {
70
+ if (similarity >= SIMILARITY_THRESHOLD) {
146
71
  logger.debug({
147
72
  tool: name1,
148
73
  similarity,
149
- threshold: GPT_SETTINGS.similarityThreshold,
74
+ threshold: SIMILARITY_THRESHOLD,
150
75
  args1: argsStr1.substring(0, 100),
151
76
  args2: argsStr2.substring(0, 100),
152
77
  }, "Similar tool call detected");
@@ -157,25 +82,6 @@ function areSimilarToolCalls(call1, call2) {
157
82
  return false;
158
83
  }
159
84
 
160
- /**
161
- * Get a signature for a tool call (for tracking in history)
162
- * @param {Object} call - Tool call object
163
- * @returns {string} - Unique signature for the call
164
- */
165
- function getToolCallSignature(call) {
166
- const name = call.function?.name ?? call.name ?? 'unknown';
167
- const args = call.function?.arguments ?? call.arguments ?? call.input ?? {};
168
- const argsStr = typeof args === 'string' ? args : JSON.stringify(args);
169
- return `${name}:${argsStr}`;
170
- }
171
-
172
85
  module.exports = {
173
- GPT_SETTINGS,
174
- isGPTProvider,
175
- getGPTToolLoopThreshold,
176
- formatToolResultForGPT,
177
- getGPTSystemPromptAddendum,
178
- stringSimilarity,
179
86
  areSimilarToolCalls,
180
- getToolCallSignature,
181
87
  };
@@ -46,9 +46,16 @@ function convertOpenAIToAnthropic(openaiRequest) {
46
46
  const anthropicMessages = [];
47
47
 
48
48
  for (const msg of messageArray) {
49
- if (msg.role === "system") {
50
- // Anthropic uses a separate system field
51
- system = msg.content;
49
+ if (msg.role === "system" || msg.role === "developer") {
50
+ // Anthropic uses a separate system field. OpenAI's `developer` role
51
+ // (Codex sends its sandbox/permissions rules this way) is system-level
52
+ // content — append rather than overwrite when both are present.
53
+ const sysText = typeof msg.content === "string"
54
+ ? msg.content
55
+ : Array.isArray(msg.content)
56
+ ? msg.content.filter((p) => p?.type === "text" || typeof p?.text === "string").map((p) => p.text || "").join("\n")
57
+ : "";
58
+ system = system ? `${system}\n\n${sysText}` : sysText;
52
59
  } else if (msg.role === "user" || msg.role === "assistant") {
53
60
  // Convert content format
54
61
  let content;
@@ -26,18 +26,25 @@ function convertAnthropicToolsToOpenRouter(anthropicTools) {
26
26
  return [];
27
27
  }
28
28
 
29
- return anthropicTools.map(tool => ({
30
- type: "function",
31
- function: {
32
- name: tool.name,
33
- description: tool.description || "",
34
- parameters: tool.input_schema || {
35
- type: "object",
36
- properties: {},
37
- required: []
29
+ return anthropicTools.map(tool => {
30
+ // Shape-detect: already OpenAI-format tools pass through untouched —
31
+ // the orchestrator's build step may have normalised for a different
32
+ // provider before routing picked this one, and re-wrapping strips the
33
+ // name ({type:"function", function:{name:undefined}}).
34
+ if (tool?.type === "function" && tool.function) return tool;
35
+ return {
36
+ type: "function",
37
+ function: {
38
+ name: tool.name,
39
+ description: tool.description || "",
40
+ parameters: tool.input_schema || {
41
+ type: "object",
42
+ properties: {},
43
+ required: []
44
+ }
38
45
  }
39
- }
40
- }));
46
+ };
47
+ });
41
48
  }
42
49
 
43
50
  /**
@@ -62,6 +69,13 @@ function convertAnthropicMessagesToOpenRouter(anthropicMessages) {
62
69
  const textBlocks = content.filter(block => block.type === 'text');
63
70
  const toolUseBlocks = content.filter(block => block.type === 'tool_use');
64
71
  const toolResultBlocks = content.filter(block => block.type === 'tool_result');
72
+ // Thinking models (Kimi, MiniMax) require prior reasoning passed back
73
+ // as reasoning_content on replayed assistant turns — dropping it
74
+ // severs the model's chain across a tool loop.
75
+ const thinkingText = content
76
+ .filter(block => block.type === 'thinking' && typeof block.thinking === 'string')
77
+ .map(block => block.thinking)
78
+ .join('\n');
65
79
 
66
80
  logger.debug({
67
81
  role: msg.role,
@@ -98,20 +112,18 @@ function convertAnthropicMessagesToOpenRouter(anthropicMessages) {
98
112
  message.content = null;
99
113
  }
100
114
 
115
+ if (thinkingText) {
116
+ message.reasoning_content = thinkingText;
117
+ }
118
+
101
119
  converted.push(message);
102
120
  }
103
- // User message with tool results
121
+ // User message with tool results. ORDER MATTERS: tool messages must
122
+ // directly follow the assistant message carrying their tool_calls —
123
+ // a user-text message in between orphans the results, and strict
124
+ // providers 400 the unpaired function_call.
104
125
  else if (msg.role === 'user' && toolResultBlocks.length > 0) {
105
- // Add text content as user message first if present
106
- const textContent = textBlocks.map(block => block.text || '').join('\n');
107
- if (textContent) {
108
- converted.push({
109
- role: 'user',
110
- content: textContent
111
- });
112
- }
113
-
114
- // Add each tool result as a separate tool message
126
+ // Tool results first adjacent to their assistant tool_calls.
115
127
  for (const toolResult of toolResultBlocks) {
116
128
  converted.push({
117
129
  role: 'tool',
@@ -121,14 +133,27 @@ function convertAnthropicMessagesToOpenRouter(anthropicMessages) {
121
133
  : JSON.stringify(toolResult.content || {})
122
134
  });
123
135
  }
136
+
137
+ // Then any user-authored/injected text as a user message.
138
+ const textContent = textBlocks.map(block => block.text || '').join('\n');
139
+ if (textContent) {
140
+ converted.push({
141
+ role: 'user',
142
+ content: textContent
143
+ });
144
+ }
124
145
  }
125
146
  // Regular message with just text
126
147
  else {
127
148
  const textContent = textBlocks.map(block => block.text || '').join('\n');
128
- converted.push({
149
+ const message = {
129
150
  role: msg.role,
130
151
  content: textContent || ''
131
- });
152
+ };
153
+ if (msg.role === 'assistant' && thinkingText) {
154
+ message.reasoning_content = thinkingText;
155
+ }
156
+ converted.push(message);
132
157
  }
133
158
  }
134
159
  // Simple string content
@@ -114,6 +114,7 @@ function needsCacheInjection(provider) {
114
114
  'bedrock',
115
115
  'databricks', // Databricks routes to Claude which supports caching
116
116
  'openrouter', // OpenRouter forwards cache_control to underlying provider
117
+ 'edenai', // Eden AI forwards cache_control to underlying provider
117
118
  ]);
118
119
 
119
120
  return EXPLICIT_CACHE_PROVIDERS.has(provider);
@@ -11,7 +11,7 @@ const NATIVE_THINKING_BEDROCK_MODELS = [
11
11
  "claude-haiku",
12
12
  ];
13
13
 
14
- const REASONING_CONTENT_PROVIDERS = new Set(["moonshot", "openrouter", "openai", "azure-openai"]);
14
+ const REASONING_CONTENT_PROVIDERS = new Set(["moonshot", "openrouter", "edenai", "openai", "azure-openai"]);
15
15
 
16
16
  function supportsNativeThinking(providerType, model) {
17
17
  if (NATIVE_THINKING_PROVIDERS.has(providerType)) return true;
@@ -71,7 +71,7 @@ function mapClientToolToLynkr(clientToolName) {
71
71
  * @returns {Object} Chat Completions format request
72
72
  */
73
73
  function convertResponsesToChat(responsesRequest) {
74
- const { input, model, max_tokens, temperature, top_p, tools, tool_choice, stream } = responsesRequest;
74
+ const { input, instructions, model, max_tokens, temperature, top_p, tools, tool_choice, stream } = responsesRequest;
75
75
 
76
76
  logger.info({
77
77
  inputType: typeof input,
@@ -173,7 +173,9 @@ function convertResponsesToChat(responsesRequest) {
173
173
  if (Array.isArray(content)) {
174
174
  // Extract text from array of content parts
175
175
  const textParts = content
176
- .filter(part => part && (part.type === 'text' || part.type === 'input_text'))
176
+ // output_text is how prior ASSISTANT replies arrive in input[]
177
+ // dropping it made the model re-answer every previous turn.
178
+ .filter(part => part && (part.type === 'text' || part.type === 'input_text' || part.type === 'output_text'))
177
179
  .map(part => part.text || part.input_text || '')
178
180
  .filter(text => text.length > 0);
179
181
 
@@ -249,13 +251,42 @@ function convertResponsesToChat(responsesRequest) {
249
251
  messages = [{ role: "user", content: String(input || "") }];
250
252
  }
251
253
 
254
+ // Responses API carries the system prompt in `instructions`, not as an
255
+ // input message — Codex's entire harness prompt lives here. Dropping it
256
+ // leaves the model with no tool-usage conventions.
257
+ if (typeof instructions === "string" && instructions.trim()) {
258
+ messages = [{ role: "system", content: instructions }, ...messages];
259
+ }
260
+
261
+ // Responses API declares function tools with a top-level name/parameters;
262
+ // downstream converters expect Chat Completions shape ({function:{name}})
263
+ // and silently drop anything else. Convert function tools; exotic types
264
+ // (custom/namespace/tool_search/web_search/image_generation) have no
265
+ // Chat/Anthropic equivalent and are dropped.
266
+ let chatTools;
267
+ if (Array.isArray(tools)) {
268
+ chatTools = tools
269
+ .map((t) => {
270
+ if (t?.type === "function" && t.function) return t; // already Chat-shaped
271
+ if (t?.type === "function" && t.name && t.parameters) {
272
+ return {
273
+ type: "function",
274
+ function: { name: t.name, description: t.description || "", parameters: t.parameters },
275
+ };
276
+ }
277
+ return null;
278
+ })
279
+ .filter(Boolean);
280
+ if (chatTools.length === 0) chatTools = undefined;
281
+ }
282
+
252
283
  const result = {
253
284
  model: model || "gpt-4o",
254
285
  messages: messages,
255
286
  max_tokens: max_tokens || 4096,
256
287
  temperature: temperature,
257
288
  top_p: top_p,
258
- tools: tools,
289
+ tools: chatTools,
259
290
  tool_choice: tool_choice,
260
291
  stream: stream || false
261
292
  };
@@ -11,10 +11,25 @@
11
11
  */
12
12
 
13
13
  const smartRouting = require('../routing');
14
+ const config = require('../config');
15
+
16
+ // Synchronous version for benchmarking/tests
17
+ // (when tiers are disabled, routing is purely static)
18
+ function determineProviderSync(payload) {
19
+ const primaryProvider = config.modelProvider?.type || 'databricks';
20
+ const defaultModel = config.modelProvider?.defaultModel || 'databricks-claude-sonnet-4-5';
21
+
22
+ return {
23
+ provider: primaryProvider,
24
+ model: defaultModel,
25
+ reason: 'static_provider'
26
+ };
27
+ }
14
28
 
15
29
  // Re-export all functions from smart routing
16
30
  module.exports = {
17
31
  determineProviderSmart: smartRouting.determineProviderSmart,
32
+ determineProviderSync,
18
33
  isFallbackEnabled: smartRouting.isFallbackEnabled,
19
34
  getFallbackProvider: smartRouting.getFallbackProvider,
20
35
  getRoutingHeaders: smartRouting.getRoutingHeaders,
@@ -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", "moonshot"]);
65
+ const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "edenai", "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
@@ -97,6 +97,12 @@ const openRouterModel = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini";
97
97
  const openRouterEmbeddingsModel = process.env.OPENROUTER_EMBEDDINGS_MODEL ?? "openai/text-embedding-ada-002";
98
98
  const openRouterEndpoint = process.env.OPENROUTER_ENDPOINT ?? "https://openrouter.ai/api/v1/chat/completions";
99
99
 
100
+ // Eden AI configuration (OpenAI-compatible v3 gateway — provider/model naming, EU/GDPR)
101
+ const edenAIApiKey = process.env.EDENAI_API_KEY ?? null;
102
+ const edenAIModel = process.env.EDENAI_MODEL ?? "openai/gpt-4o-mini";
103
+ const edenAIEmbeddingsModel = process.env.EDENAI_EMBEDDINGS_MODEL ?? "openai/text-embedding-ada-002";
104
+ const edenAIEndpoint = process.env.EDENAI_ENDPOINT ?? "https://api.edenai.run/v3/chat/completions";
105
+
100
106
  // Azure OpenAI configuration
101
107
  const azureOpenAIEndpoint = process.env.AZURE_OPENAI_ENDPOINT?.trim() || null;
102
108
  const azureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null;
@@ -219,6 +225,13 @@ const toonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10);
219
225
  const toonFailOpen = process.env.TOON_FAIL_OPEN !== "false"; // default true
220
226
  const toonLogStats = process.env.TOON_LOG_STATS !== "false"; // default true
221
227
 
228
+ // GCF payload compression (opt-in; drop-in alternative to TOON)
229
+ const gcfEnabled = process.env.GCF_ENABLED === "true"; // default false
230
+ const gcfMinBytes = Number.parseInt(process.env.GCF_MIN_BYTES ?? "4096", 10);
231
+ const gcfFailOpen = process.env.GCF_FAIL_OPEN !== "false"; // default true
232
+ const gcfLogStats = process.env.GCF_LOG_STATS !== "false"; // default true
233
+ const gcfVerify = process.env.GCF_VERIFY !== "false"; // default true (round-trip check)
234
+
222
235
  // Smart tool selection configuration (always enabled)
223
236
  const smartToolSelectionMode = (process.env.SMART_TOOL_SELECTION_MODE ?? "heuristic").toLowerCase();
224
237
  const smartToolSelectionTokenBudget = Number.parseInt(
@@ -336,7 +349,7 @@ const tiersConfigured = !!(
336
349
  if (fallbackEnabled && tiersConfigured) {
337
350
  const localProviders = ["ollama", "llamacpp", "lmstudio"];
338
351
  if (localProviders.includes(fallbackProvider)) {
339
- 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`);
352
+ throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, edenai, openai, bedrock`);
340
353
  }
341
354
  let fallbackMisconfigured = false;
342
355
  if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) {
@@ -585,6 +598,12 @@ var config = {
585
598
  embeddingsModel: openRouterEmbeddingsModel,
586
599
  endpoint: openRouterEndpoint,
587
600
  },
601
+ edenai: {
602
+ apiKey: edenAIApiKey,
603
+ model: edenAIModel,
604
+ embeddingsModel: edenAIEmbeddingsModel,
605
+ endpoint: edenAIEndpoint,
606
+ },
588
607
  azureOpenAI: {
589
608
  endpoint: azureOpenAIEndpoint,
590
609
  apiKey: azureOpenAIApiKey,
@@ -855,6 +874,13 @@ var config = {
855
874
  failOpen: toonFailOpen,
856
875
  logStats: toonLogStats,
857
876
  },
877
+ gcf: {
878
+ enabled: gcfEnabled,
879
+ minBytes: Number.isNaN(gcfMinBytes) ? 4096 : gcfMinBytes,
880
+ failOpen: gcfFailOpen,
881
+ logStats: gcfLogStats,
882
+ verify: gcfVerify,
883
+ },
858
884
  smartToolSelection: {
859
885
  enabled: true, // HARDCODED - always enabled
860
886
  mode: smartToolSelectionMode,
@@ -1013,6 +1039,8 @@ function reloadConfig() {
1013
1039
  config.ollama.model = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b";
1014
1040
  config.openrouter.apiKey = process.env.OPENROUTER_API_KEY ?? null;
1015
1041
  config.openrouter.model = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini";
1042
+ config.edenai.apiKey = process.env.EDENAI_API_KEY ?? null;
1043
+ config.edenai.model = process.env.EDENAI_MODEL ?? "openai/gpt-4o-mini";
1016
1044
  config.azureOpenAI.apiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null;
1017
1045
  config.openai.apiKey = process.env.OPENAI_API_KEY?.trim() || null;
1018
1046
  config.bedrock.apiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null;
@@ -1041,6 +1069,12 @@ function reloadConfig() {
1041
1069
  config.toon.minBytes = Number.isNaN(newToonMinBytes) ? 4096 : newToonMinBytes;
1042
1070
  config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false";
1043
1071
  config.toon.logStats = process.env.TOON_LOG_STATS !== "false";
1072
+ config.gcf.enabled = process.env.GCF_ENABLED === "true";
1073
+ const newGcfMinBytes = Number.parseInt(process.env.GCF_MIN_BYTES ?? "4096", 10);
1074
+ config.gcf.minBytes = Number.isNaN(newGcfMinBytes) ? 4096 : newGcfMinBytes;
1075
+ config.gcf.failOpen = process.env.GCF_FAIL_OPEN !== "false";
1076
+ config.gcf.logStats = process.env.GCF_LOG_STATS !== "false";
1077
+ config.gcf.verify = process.env.GCF_VERIFY !== "false";
1044
1078
 
1045
1079
  // Tier routing (critical for fixing model name issues without restart)
1046
1080
  config.modelTiers.SIMPLE = process.env.TIER_SIMPLE?.trim() || null;