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.
- package/README.md +29 -19
- package/bin/cli.js +11 -0
- package/bin/lynkr-init.js +14 -1
- package/bin/lynkr-usage.js +78 -0
- package/bin/wrap.js +60 -35
- package/config/difficulty-anchors.json +22 -0
- package/package.json +24 -3
- package/scripts/audit-log-reader.js +399 -0
- package/scripts/calibrate-thresholds.js +38 -157
- package/scripts/compact-dictionary.js +204 -0
- package/scripts/test-deduplication.js +448 -0
- package/scripts/ws7-anchor-replay.js +108 -0
- package/skills/lynkr/SKILL.md +195 -0
- package/src/agents/context-manager.js +18 -2
- package/src/agents/definitions/loader.js +90 -0
- package/src/agents/executor.js +24 -2
- package/src/agents/index.js +9 -1
- package/src/agents/parallel-coordinator.js +2 -2
- package/src/agents/reflector.js +11 -1
- package/src/api/middleware/loop-guard.js +87 -0
- package/src/api/middleware/request-logging.js +5 -64
- package/src/api/middleware/session.js +0 -0
- package/src/api/openai-router.js +120 -101
- package/src/api/providers-handler.js +27 -2
- package/src/api/router.js +450 -125
- package/src/budget/index.js +2 -19
- package/src/cache/semantic.js +9 -0
- package/src/clients/databricks.js +459 -146
- package/src/clients/gpt-utils.js +11 -105
- package/src/clients/openai-format.js +10 -3
- package/src/clients/openrouter-utils.js +49 -24
- package/src/clients/prompt-cache-injection.js +1 -0
- package/src/clients/provider-capabilities.js +1 -1
- package/src/clients/responses-format.js +34 -3
- package/src/clients/routing.js +15 -0
- package/src/config/index.js +36 -2
- package/src/context/gcf.js +275 -0
- package/src/context/tool-result-compressor.js +51 -9
- package/src/dashboard/api.js +1 -0
- package/src/logger/index.js +14 -1
- package/src/memory/search.js +12 -40
- package/src/memory/tools.js +3 -24
- package/src/orchestrator/bypass.js +4 -2
- package/src/orchestrator/index.js +144 -88
- package/src/routing/affinity-store.js +194 -0
- package/src/routing/agentic-detector.js +36 -6
- package/src/routing/bandit.js +25 -6
- package/src/routing/calibration.js +212 -0
- package/src/routing/client-profiles.js +292 -0
- package/src/routing/complexity-analyzer.js +48 -11
- package/src/routing/deescalator.js +148 -0
- package/src/routing/degradation.js +109 -0
- package/src/routing/feedback.js +157 -0
- package/src/routing/index.js +897 -87
- package/src/routing/intent-score.js +339 -0
- package/src/routing/interaction.js +3 -0
- package/src/routing/knn-router.js +70 -21
- package/src/routing/model-registry.js +28 -7
- package/src/routing/model-tiers.js +25 -2
- package/src/routing/reward-pipeline.js +68 -2
- package/src/routing/risk-analyzer.js +30 -1
- package/src/routing/risk-classifier.js +6 -2
- package/src/routing/session-affinity.js +162 -34
- package/src/routing/telemetry.js +298 -10
- package/src/routing/verifier.js +267 -0
- package/src/server.js +66 -21
- package/src/sessions/cleanup.js +17 -0
- package/src/tools/index.js +1 -15
- package/src/tools/smart-selection.js +10 -0
- package/src/tools/web-client.js +3 -3
- package/.eslintrc.cjs +0 -12
- package/benchmark-configs/litellm_config.yaml +0 -86
- package/benchmark-configs/lynkr.env +0 -48
- package/benchmark-configs/portkey-config.json +0 -60
- package/benchmark-configs/portkey-docker.sh +0 -23
- package/benchmark-tier-routing.js +0 -449
- package/funding.json +0 -110
- package/src/api/middleware/validation.js +0 -261
- package/src/routing/drift-monitor.js +0 -113
- package/src/workers/helpers.js +0 -185
package/src/clients/gpt-utils.js
CHANGED
|
@@ -1,89 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* All settings are hardcoded - no env vars required
|
|
2
|
+
* Detection of semantically-similar tool calls.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
//
|
|
15
|
-
const
|
|
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
|
-
//
|
|
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 >=
|
|
70
|
+
if (similarity >= SIMILARITY_THRESHOLD) {
|
|
146
71
|
logger.debug({
|
|
147
72
|
tool: name1,
|
|
148
73
|
similarity,
|
|
149
|
-
threshold:
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
289
|
+
tools: chatTools,
|
|
259
290
|
tool_choice: tool_choice,
|
|
260
291
|
stream: stream || false
|
|
261
292
|
};
|
package/src/clients/routing.js
CHANGED
|
@@ -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,
|
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", "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;
|