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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GPT-specific utilities for handling tool calls and responses
|
|
3
|
+
* All settings are hardcoded - no env vars required
|
|
4
|
+
*
|
|
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
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const logger = require("../logger");
|
|
13
|
+
|
|
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
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Calculate string similarity using Jaccard index
|
|
85
|
+
* Used to detect semantically similar tool calls
|
|
86
|
+
*
|
|
87
|
+
* @param {string} s1 - First string
|
|
88
|
+
* @param {string} s2 - Second string
|
|
89
|
+
* @returns {number} - Similarity score between 0 and 1
|
|
90
|
+
*/
|
|
91
|
+
function stringSimilarity(s1, s2) {
|
|
92
|
+
if (!s1 || !s2) return 0;
|
|
93
|
+
if (s1 === s2) return 1;
|
|
94
|
+
|
|
95
|
+
// Tokenize by whitespace and common delimiters
|
|
96
|
+
const tokenize = (s) => new Set(
|
|
97
|
+
s.toLowerCase()
|
|
98
|
+
.split(/[\s\-_\/\.\,\:\;]+/)
|
|
99
|
+
.filter(t => t.length > 0)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const set1 = tokenize(s1);
|
|
103
|
+
const set2 = tokenize(s2);
|
|
104
|
+
|
|
105
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
106
|
+
const union = new Set([...set1, ...set2]);
|
|
107
|
+
|
|
108
|
+
return union.size > 0 ? intersection.size / union.size : 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if two tool calls are semantically similar
|
|
113
|
+
* GPT often retries with slightly different parameters that are functionally equivalent
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} call1 - First tool call {name, arguments}
|
|
116
|
+
* @param {Object} call2 - Second tool call {name, arguments}
|
|
117
|
+
* @returns {boolean} - True if calls are similar enough to be considered duplicates
|
|
118
|
+
*/
|
|
119
|
+
function areSimilarToolCalls(call1, call2) {
|
|
120
|
+
if (!call1 || !call2) return false;
|
|
121
|
+
|
|
122
|
+
// Must be the same tool
|
|
123
|
+
const name1 = call1.function?.name ?? call1.name;
|
|
124
|
+
const name2 = call2.function?.name ?? call2.name;
|
|
125
|
+
if (name1 !== name2) return false;
|
|
126
|
+
|
|
127
|
+
// Get arguments
|
|
128
|
+
const args1 = call1.function?.arguments ?? call1.arguments ?? call1.input ?? {};
|
|
129
|
+
const args2 = call2.function?.arguments ?? call2.arguments ?? call2.input ?? {};
|
|
130
|
+
|
|
131
|
+
// Stringify for comparison
|
|
132
|
+
const argsStr1 = typeof args1 === 'string' ? args1 : JSON.stringify(args1);
|
|
133
|
+
const argsStr2 = typeof args2 === 'string' ? args2 : JSON.stringify(args2);
|
|
134
|
+
|
|
135
|
+
// Exact match
|
|
136
|
+
if (argsStr1 === argsStr2) return true;
|
|
137
|
+
|
|
138
|
+
// For search-related tools, check semantic similarity
|
|
139
|
+
const searchTools = ['grep', 'glob', 'search', 'find', 'read', 'bash', 'shell'];
|
|
140
|
+
const toolName = (name1 || '').toLowerCase();
|
|
141
|
+
const isSearchTool = searchTools.some(t => toolName.includes(t));
|
|
142
|
+
|
|
143
|
+
if (isSearchTool) {
|
|
144
|
+
const similarity = stringSimilarity(argsStr1, argsStr2);
|
|
145
|
+
if (similarity >= GPT_SETTINGS.similarityThreshold) {
|
|
146
|
+
logger.debug({
|
|
147
|
+
tool: name1,
|
|
148
|
+
similarity,
|
|
149
|
+
threshold: GPT_SETTINGS.similarityThreshold,
|
|
150
|
+
args1: argsStr1.substring(0, 100),
|
|
151
|
+
args2: argsStr2.substring(0, 100),
|
|
152
|
+
}, "Similar tool call detected");
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
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
|
+
module.exports = {
|
|
173
|
+
GPT_SETTINGS,
|
|
174
|
+
isGPTProvider,
|
|
175
|
+
getGPTToolLoopThreshold,
|
|
176
|
+
formatToolResultForGPT,
|
|
177
|
+
getGPTSystemPromptAddendum,
|
|
178
|
+
stringSimilarity,
|
|
179
|
+
areSimilarToolCalls,
|
|
180
|
+
getToolCallSignature,
|
|
181
|
+
};
|
|
@@ -10,12 +10,21 @@ const modelCapabilitiesCache = new Map();
|
|
|
10
10
|
const TOOL_CAPABLE_MODELS = new Set([
|
|
11
11
|
"llama3.1",
|
|
12
12
|
"llama3.2",
|
|
13
|
+
"llama3.3",
|
|
13
14
|
"qwen2.5",
|
|
15
|
+
"qwen3",
|
|
14
16
|
"mistral",
|
|
15
17
|
"mistral-nemo",
|
|
16
18
|
"firefunction-v2",
|
|
17
19
|
"kimi-k2.5",
|
|
18
|
-
"nemotron"
|
|
20
|
+
"nemotron",
|
|
21
|
+
"glm-4",
|
|
22
|
+
"glm-4.5",
|
|
23
|
+
"glm-4.7",
|
|
24
|
+
"glm-5",
|
|
25
|
+
"gpt-oss",
|
|
26
|
+
"minimax",
|
|
27
|
+
"deepseek-r1",
|
|
19
28
|
]);
|
|
20
29
|
|
|
21
30
|
/**
|
|
@@ -55,25 +64,60 @@ async function checkOllamaToolSupport(modelName = config.ollama?.model) {
|
|
|
55
64
|
return supportsTools;
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
// --- Endpoint detection: Anthropic (/v1/messages) vs legacy (/api/chat) ---
|
|
68
|
+
|
|
69
|
+
// null = not probed yet, true = Anthropic available, false = use legacy
|
|
70
|
+
let anthropicEndpointAvailable = null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Probe whether Ollama exposes the Anthropic-compatible /v1/messages endpoint (v0.14.0+).
|
|
74
|
+
* Result is cached for the process lifetime.
|
|
75
|
+
*/
|
|
76
|
+
async function hasAnthropicEndpoint(baseUrl) {
|
|
77
|
+
if (anthropicEndpointAvailable !== null) return anthropicEndpointAvailable;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Send a minimal request — we only care about whether the route exists
|
|
81
|
+
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"anthropic-version": "2023-06-01",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
model: "probe",
|
|
89
|
+
max_tokens: 1,
|
|
90
|
+
messages: [{ role: "user", content: "hi" }],
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 404 → endpoint doesn't exist (old Ollama)
|
|
95
|
+
// Any other status (200, 400, 500) → endpoint exists
|
|
96
|
+
anthropicEndpointAvailable = res.status !== 404;
|
|
97
|
+
logger.info(
|
|
98
|
+
{ available: anthropicEndpointAvailable, status: res.status },
|
|
99
|
+
anthropicEndpointAvailable
|
|
100
|
+
? "Ollama Anthropic API detected (/v1/messages) — using native passthrough"
|
|
101
|
+
: "Ollama Anthropic API not available — falling back to legacy /api/chat (upgrade to Ollama v0.14.0+ for best results)"
|
|
102
|
+
);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Network error — assume legacy
|
|
105
|
+
anthropicEndpointAvailable = false;
|
|
106
|
+
logger.warn({ error: err.message }, "Failed to probe Ollama Anthropic endpoint, using legacy /api/chat");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return anthropicEndpointAvailable;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Exposed for tests
|
|
113
|
+
function resetEndpointCache() {
|
|
114
|
+
anthropicEndpointAvailable = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Legacy format conversion (for Ollama < v0.14.0 using /api/chat) ---
|
|
118
|
+
|
|
58
119
|
/**
|
|
59
|
-
* Convert Anthropic tool format to Ollama format
|
|
60
|
-
*
|
|
61
|
-
* Anthropic format:
|
|
62
|
-
* {
|
|
63
|
-
* name: "get_weather",
|
|
64
|
-
* description: "Get weather",
|
|
65
|
-
* input_schema: { type: "object", properties: {...}, required: [...] }
|
|
66
|
-
* }
|
|
67
|
-
*
|
|
68
|
-
* Ollama format:
|
|
69
|
-
* {
|
|
70
|
-
* type: "function",
|
|
71
|
-
* function: {
|
|
72
|
-
* name: "get_weather",
|
|
73
|
-
* description: "Get weather",
|
|
74
|
-
* parameters: { type: "object", properties: {...}, required: [...] }
|
|
75
|
-
* }
|
|
76
|
-
* }
|
|
120
|
+
* Convert Anthropic tool format to Ollama/OpenAI function-calling format
|
|
77
121
|
*/
|
|
78
122
|
function convertAnthropicToolsToOllama(anthropicTools) {
|
|
79
123
|
if (!Array.isArray(anthropicTools) || anthropicTools.length === 0) {
|
|
@@ -93,128 +137,10 @@ function convertAnthropicToolsToOllama(anthropicTools) {
|
|
|
93
137
|
}));
|
|
94
138
|
}
|
|
95
139
|
|
|
96
|
-
/**
|
|
97
|
-
* Convert Ollama tool call response to Anthropic format
|
|
98
|
-
*
|
|
99
|
-
* Ollama format (actual):
|
|
100
|
-
* {
|
|
101
|
-
* message: {
|
|
102
|
-
* role: "assistant",
|
|
103
|
-
* content: "",
|
|
104
|
-
* tool_calls: [{
|
|
105
|
-
* function: {
|
|
106
|
-
* name: "get_weather",
|
|
107
|
-
* arguments: { location: "SF" } // Already parsed object
|
|
108
|
-
* }
|
|
109
|
-
* }]
|
|
110
|
-
* }
|
|
111
|
-
* }
|
|
112
|
-
*
|
|
113
|
-
* Anthropic format:
|
|
114
|
-
* {
|
|
115
|
-
* content: [{
|
|
116
|
-
* type: "tool_use",
|
|
117
|
-
* id: "toolu_123",
|
|
118
|
-
* name: "get_weather",
|
|
119
|
-
* input: { location: "SF" }
|
|
120
|
-
* }],
|
|
121
|
-
* stop_reason: "tool_use"
|
|
122
|
-
* }
|
|
123
|
-
*/
|
|
124
|
-
function convertOllamaToolCallsToAnthropic(ollamaResponse) {
|
|
125
|
-
const message = ollamaResponse?.message || {};
|
|
126
|
-
const toolCalls = message.tool_calls || [];
|
|
127
|
-
const textContent = message.content || "";
|
|
128
|
-
|
|
129
|
-
const contentBlocks = [];
|
|
130
|
-
|
|
131
|
-
// Add text content if present
|
|
132
|
-
if (textContent && textContent.trim()) {
|
|
133
|
-
contentBlocks.push({
|
|
134
|
-
type: "text",
|
|
135
|
-
text: textContent,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Add tool calls
|
|
140
|
-
for (const toolCall of toolCalls) {
|
|
141
|
-
const func = toolCall.function || {};
|
|
142
|
-
let input = {};
|
|
143
|
-
|
|
144
|
-
// Handle arguments - can be string JSON or already parsed object
|
|
145
|
-
if (func.arguments) {
|
|
146
|
-
if (typeof func.arguments === "string") {
|
|
147
|
-
try {
|
|
148
|
-
input = JSON.parse(func.arguments);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
logger.warn({
|
|
151
|
-
error: err.message,
|
|
152
|
-
arguments: func.arguments
|
|
153
|
-
}, "Failed to parse Ollama tool arguments string");
|
|
154
|
-
input = {};
|
|
155
|
-
}
|
|
156
|
-
} else if (typeof func.arguments === "object") {
|
|
157
|
-
// Already an object, use directly
|
|
158
|
-
input = func.arguments;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Generate tool use ID (Ollama may or may not provide one)
|
|
163
|
-
const toolUseId = toolCall.id || `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
164
|
-
|
|
165
|
-
contentBlocks.push({
|
|
166
|
-
type: "tool_use",
|
|
167
|
-
id: toolUseId,
|
|
168
|
-
name: func.name || "unknown",
|
|
169
|
-
input,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Determine stop reason
|
|
174
|
-
const stopReason = toolCalls.length > 0 ? "tool_use" : "end_turn";
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
contentBlocks,
|
|
178
|
-
stopReason,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Build complete Anthropic response from Ollama with tool calls
|
|
184
|
-
*/
|
|
185
|
-
function buildAnthropicResponseFromOllama(ollamaResponse, requestedModel) {
|
|
186
|
-
const { contentBlocks, stopReason } = convertOllamaToolCallsToAnthropic(ollamaResponse);
|
|
187
|
-
|
|
188
|
-
// Ensure at least one content block
|
|
189
|
-
const finalContent = contentBlocks.length > 0
|
|
190
|
-
? contentBlocks
|
|
191
|
-
: [{ type: "text", text: "" }];
|
|
192
|
-
|
|
193
|
-
// Extract token counts
|
|
194
|
-
const inputTokens = ollamaResponse.prompt_eval_count || 0;
|
|
195
|
-
const outputTokens = ollamaResponse.eval_count || 0;
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
id: `msg_${Date.now()}`,
|
|
199
|
-
type: "message",
|
|
200
|
-
role: "assistant",
|
|
201
|
-
model: requestedModel,
|
|
202
|
-
content: finalContent,
|
|
203
|
-
stop_reason: stopReason,
|
|
204
|
-
stop_sequence: null,
|
|
205
|
-
usage: {
|
|
206
|
-
input_tokens: inputTokens,
|
|
207
|
-
output_tokens: outputTokens,
|
|
208
|
-
cache_creation_input_tokens: 0,
|
|
209
|
-
cache_read_input_tokens: 0,
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
140
|
module.exports = {
|
|
215
141
|
checkOllamaToolSupport,
|
|
216
|
-
convertAnthropicToolsToOllama,
|
|
217
|
-
convertOllamaToolCallsToAnthropic,
|
|
218
|
-
buildAnthropicResponseFromOllama,
|
|
219
142
|
modelNameSupportsTools,
|
|
143
|
+
hasAnthropicEndpoint,
|
|
144
|
+
resetEndpointCache,
|
|
145
|
+
convertAnthropicToolsToOllama,
|
|
220
146
|
};
|
package/src/clients/routing.js
CHANGED
|
@@ -14,7 +14,6 @@ const smartRouting = require('../routing');
|
|
|
14
14
|
|
|
15
15
|
// Re-export all functions from smart routing
|
|
16
16
|
module.exports = {
|
|
17
|
-
determineProvider: smartRouting.determineProvider,
|
|
18
17
|
determineProviderSmart: smartRouting.determineProviderSmart,
|
|
19
18
|
isFallbackEnabled: smartRouting.isFallbackEnabled,
|
|
20
19
|
getFallbackProvider: smartRouting.getFallbackProvider,
|
|
@@ -76,7 +76,7 @@ const STANDARD_TOOLS = [
|
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
name: "Bash",
|
|
79
|
-
description: "Executes a bash command in a persistent shell session. Use for terminal operations like git, npm, docker, etc. DO NOT use for file
|
|
79
|
+
description: "Executes a bash command in a persistent shell session. Use for terminal operations like git, npm, docker, listing files (ls), etc. PREFERRED for listing directory contents - use 'ls' command. DO NOT use for reading file contents - use Read tool instead.",
|
|
80
80
|
input_schema: {
|
|
81
81
|
type: "object",
|
|
82
82
|
properties: {
|
|
@@ -98,7 +98,7 @@ const STANDARD_TOOLS = [
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
name: "Glob",
|
|
101
|
-
description: "
|
|
101
|
+
description: "File pattern matching for finding files by name pattern. Use ONLY when you need to find files matching a specific pattern like '**/*.js'. For simple directory listing, use Bash with 'ls' instead.",
|
|
102
102
|
input_schema: {
|
|
103
103
|
type: "object",
|
|
104
104
|
properties: {
|
|
@@ -145,6 +145,66 @@ const STANDARD_TOOLS = [
|
|
|
145
145
|
required: ["pattern"]
|
|
146
146
|
}
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
name: "MultiEdit",
|
|
150
|
+
description: "Makes multiple edits to a single file in one atomic operation. More efficient than calling Edit multiple times. Each edit is an exact string replacement.",
|
|
151
|
+
input_schema: {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
file_path: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "Relative path within workspace. DO NOT use absolute paths."
|
|
157
|
+
},
|
|
158
|
+
edits: {
|
|
159
|
+
type: "array",
|
|
160
|
+
description: "Array of edits to apply to the file",
|
|
161
|
+
items: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
old_string: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "The text to replace"
|
|
167
|
+
},
|
|
168
|
+
new_string: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "The text to replace it with"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: ["old_string", "new_string"]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
required: ["file_path", "edits"]
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "LS",
|
|
182
|
+
description: "Lists files and directories in a given path. Returns a structured listing with file types and sizes. Use for quick directory overview.",
|
|
183
|
+
input_schema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
path: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "The directory to list. Defaults to current working directory."
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
required: []
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "NotebookRead",
|
|
196
|
+
description: "Reads and displays the contents of a Jupyter notebook (.ipynb file), including all cells with their outputs, combining code, text, and visualizations.",
|
|
197
|
+
input_schema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
notebook_path: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "Relative path to the Jupyter notebook (e.g., 'analysis.ipynb'). DO NOT use absolute paths."
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
required: ["notebook_path"]
|
|
206
|
+
}
|
|
207
|
+
},
|
|
148
208
|
{
|
|
149
209
|
name: "TodoWrite",
|
|
150
210
|
description: "Create and manage a structured task list for tracking progress and organizing complex tasks. Use proactively for multi-step tasks or when user provides multiple tasks.",
|
|
@@ -320,6 +380,29 @@ EXAMPLE: User says "explore this project" → Call Task with subagent_type="Expl
|
|
|
320
380
|
required: ["url", "prompt"]
|
|
321
381
|
}
|
|
322
382
|
},
|
|
383
|
+
{
|
|
384
|
+
name: "WebAgent",
|
|
385
|
+
description: "Launches a browser agent to navigate a website and accomplish a goal. Use when you need to interact with dynamic web content (click buttons, fill forms, extract data from JS-rendered pages) beyond what a simple HTTP fetch can do. Returns structured JSON. Takes 10-60 seconds.",
|
|
386
|
+
input_schema: {
|
|
387
|
+
type: "object",
|
|
388
|
+
properties: {
|
|
389
|
+
url: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "Target URL to navigate to"
|
|
392
|
+
},
|
|
393
|
+
goal: {
|
|
394
|
+
type: "string",
|
|
395
|
+
description: "What to accomplish on the page. Be specific about what data to extract or actions to take."
|
|
396
|
+
},
|
|
397
|
+
browser_profile: {
|
|
398
|
+
type: "string",
|
|
399
|
+
enum: ["lite", "stealth"],
|
|
400
|
+
description: "lite (default, faster) or stealth (for bot-protected sites)"
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
required: ["url", "goal"]
|
|
404
|
+
}
|
|
405
|
+
},
|
|
323
406
|
{
|
|
324
407
|
name: "NotebookEdit",
|
|
325
408
|
description: "Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file). Use for editing interactive documents that combine code, text, and visualizations.",
|
|
@@ -354,4 +437,17 @@ EXAMPLE: User says "explore this project" → Call Task with subagent_type="Expl
|
|
|
354
437
|
}
|
|
355
438
|
];
|
|
356
439
|
|
|
357
|
-
|
|
440
|
+
// Pre-computed name list to avoid re-mapping on every log call
|
|
441
|
+
const STANDARD_TOOL_NAMES = STANDARD_TOOLS.map(t => t.name);
|
|
442
|
+
|
|
443
|
+
// Tools that cannot work through a proxy (require bidirectional user interaction).
|
|
444
|
+
// All other tools are safe — per-client filtering via CLIENT_TOOL_MAPPINGS in
|
|
445
|
+
// openai-router.js handles excluding tools that specific clients don't support
|
|
446
|
+
// (e.g. Codex has no equivalent for Task, WebFetch, NotebookEdit).
|
|
447
|
+
const IDE_UNSUPPORTED_TOOLS = new Set(['AskUserQuestion']);
|
|
448
|
+
|
|
449
|
+
// Filtered tool set for IDE clients — excludes tools with no IDE equivalent
|
|
450
|
+
const IDE_SAFE_TOOLS = STANDARD_TOOLS.filter(t => !IDE_UNSUPPORTED_TOOLS.has(t.name));
|
|
451
|
+
const IDE_SAFE_TOOL_NAMES = IDE_SAFE_TOOLS.map(t => t.name);
|
|
452
|
+
|
|
453
|
+
module.exports = { STANDARD_TOOLS, STANDARD_TOOL_NAMES, IDE_SAFE_TOOLS, IDE_SAFE_TOOL_NAMES, IDE_UNSUPPORTED_TOOLS };
|