lynkr 3.2.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +770 -25
- package/ROUTER_COMPARISON.md +173 -0
- package/TIER_ROUTING_PLAN.md +771 -0
- package/docs/GSD_LEARNINGS.md +1116 -0
- package/docs/LOCAL_EMBEDDINGS_PLAN.md +1024 -0
- package/docs/index.md +49 -5
- package/final-test.js +33 -0
- package/package.json +2 -2
- package/src/api/openai-router.js +755 -0
- package/src/api/router.js +4 -0
- package/src/clients/bedrock-utils.js +298 -0
- package/src/clients/databricks.js +265 -0
- package/src/clients/databricks.js.backup +1036 -0
- package/src/clients/openai-format.js +393 -0
- package/src/clients/routing.js +12 -0
- package/src/config/index.js +55 -3
- package/src/orchestrator/index.js +8 -1
- package/src/tools/smart-selection.js +1 -1
- package/test/bedrock-integration.test.js +471 -0
- package/test/cursor-integration.test.js +484 -0
- package/test/llamacpp-integration.test.js +13 -34
- package/test/lmstudio-integration.test.js +335 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI ↔ Anthropic Format Conversion Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts between OpenAI's /v1/chat/completions format and Anthropic's /v1/messages format.
|
|
5
|
+
* Used for Cursor IDE compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module clients/openai-format
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require("../logger");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert OpenAI chat completion request to Anthropic messages format
|
|
14
|
+
* @param {Object} openaiRequest - OpenAI format request
|
|
15
|
+
* @returns {Object} Anthropic format request
|
|
16
|
+
*/
|
|
17
|
+
function convertOpenAIToAnthropic(openaiRequest) {
|
|
18
|
+
const { messages, model, temperature, max_tokens, top_p, stream, tools, tool_choice } = openaiRequest;
|
|
19
|
+
|
|
20
|
+
// Extract system message if present
|
|
21
|
+
let system = null;
|
|
22
|
+
const anthropicMessages = [];
|
|
23
|
+
|
|
24
|
+
for (const msg of messages) {
|
|
25
|
+
if (msg.role === "system") {
|
|
26
|
+
// Anthropic uses a separate system field
|
|
27
|
+
system = msg.content;
|
|
28
|
+
} else if (msg.role === "user" || msg.role === "assistant") {
|
|
29
|
+
// Convert content format
|
|
30
|
+
let content;
|
|
31
|
+
if (typeof msg.content === "string") {
|
|
32
|
+
content = msg.content;
|
|
33
|
+
} else if (Array.isArray(msg.content)) {
|
|
34
|
+
// OpenAI content parts format
|
|
35
|
+
content = msg.content.map(part => {
|
|
36
|
+
if (part.type === "text") {
|
|
37
|
+
return { type: "text", text: part.text };
|
|
38
|
+
} else if (part.type === "image_url") {
|
|
39
|
+
return {
|
|
40
|
+
type: "image",
|
|
41
|
+
source: {
|
|
42
|
+
type: "url",
|
|
43
|
+
url: part.image_url.url
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return part;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle tool calls in assistant messages (OpenAI format)
|
|
52
|
+
if (msg.role === "assistant" && msg.tool_calls) {
|
|
53
|
+
// Convert OpenAI tool_calls to Anthropic tool_use blocks
|
|
54
|
+
const contentBlocks = [];
|
|
55
|
+
|
|
56
|
+
// Add text content if present
|
|
57
|
+
if (msg.content) {
|
|
58
|
+
contentBlocks.push({ type: "text", text: msg.content });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add tool use blocks
|
|
62
|
+
for (const toolCall of msg.tool_calls) {
|
|
63
|
+
contentBlocks.push({
|
|
64
|
+
type: "tool_use",
|
|
65
|
+
id: toolCall.id,
|
|
66
|
+
name: toolCall.function.name,
|
|
67
|
+
input: JSON.parse(toolCall.function.arguments)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
anthropicMessages.push({
|
|
72
|
+
role: "assistant",
|
|
73
|
+
content: contentBlocks
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
anthropicMessages.push({
|
|
77
|
+
role: msg.role,
|
|
78
|
+
content
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} else if (msg.role === "tool") {
|
|
82
|
+
// OpenAI tool response → Anthropic tool_result
|
|
83
|
+
const previousMsg = anthropicMessages[anthropicMessages.length - 1];
|
|
84
|
+
|
|
85
|
+
// Tool results must follow assistant message with tool_use
|
|
86
|
+
// Add as separate user message with tool_result
|
|
87
|
+
anthropicMessages.push({
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "tool_result",
|
|
92
|
+
tool_use_id: msg.tool_call_id,
|
|
93
|
+
content: msg.content
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Convert tools format (OpenAI → Anthropic)
|
|
101
|
+
let anthropicTools = null;
|
|
102
|
+
if (tools && tools.length > 0) {
|
|
103
|
+
anthropicTools = tools.map(tool => ({
|
|
104
|
+
name: tool.function.name,
|
|
105
|
+
description: tool.function.description || "",
|
|
106
|
+
input_schema: tool.function.parameters || {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {},
|
|
109
|
+
required: []
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Build Anthropic request
|
|
115
|
+
const anthropicRequest = {
|
|
116
|
+
model: model || "claude-3-5-sonnet-20241022",
|
|
117
|
+
messages: anthropicMessages,
|
|
118
|
+
max_tokens: max_tokens || 4096,
|
|
119
|
+
stream: stream || false
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (system) {
|
|
123
|
+
anthropicRequest.system = system;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (temperature !== undefined) {
|
|
127
|
+
anthropicRequest.temperature = temperature;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (top_p !== undefined) {
|
|
131
|
+
anthropicRequest.top_p = top_p;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (anthropicTools) {
|
|
135
|
+
anthropicRequest.tools = anthropicTools;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle tool_choice
|
|
139
|
+
if (tool_choice) {
|
|
140
|
+
if (tool_choice === "auto") {
|
|
141
|
+
anthropicRequest.tool_choice = { type: "auto" };
|
|
142
|
+
} else if (tool_choice === "none") {
|
|
143
|
+
anthropicRequest.tool_choice = { type: "none" };
|
|
144
|
+
} else if (typeof tool_choice === "object" && tool_choice.function) {
|
|
145
|
+
anthropicRequest.tool_choice = {
|
|
146
|
+
type: "tool",
|
|
147
|
+
name: tool_choice.function.name
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
logger.debug({
|
|
153
|
+
openaiMessageCount: messages.length,
|
|
154
|
+
anthropicMessageCount: anthropicMessages.length,
|
|
155
|
+
hasSystem: !!system,
|
|
156
|
+
hasTools: !!anthropicTools,
|
|
157
|
+
toolCount: anthropicTools?.length || 0
|
|
158
|
+
}, "Converted OpenAI request to Anthropic format");
|
|
159
|
+
|
|
160
|
+
return anthropicRequest;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert Anthropic messages response to OpenAI chat completion format
|
|
165
|
+
* @param {Object} anthropicResponse - Anthropic format response
|
|
166
|
+
* @param {string} model - Model name to include in response
|
|
167
|
+
* @returns {Object} OpenAI format response
|
|
168
|
+
*/
|
|
169
|
+
function convertAnthropicToOpenAI(anthropicResponse, model = "claude-3-5-sonnet-20241022") {
|
|
170
|
+
const { id, content, stop_reason, usage } = anthropicResponse;
|
|
171
|
+
|
|
172
|
+
// Convert content blocks to OpenAI format
|
|
173
|
+
let messageContent = "";
|
|
174
|
+
const toolCalls = [];
|
|
175
|
+
|
|
176
|
+
for (const block of content) {
|
|
177
|
+
if (block.type === "text") {
|
|
178
|
+
messageContent += block.text;
|
|
179
|
+
} else if (block.type === "tool_use") {
|
|
180
|
+
toolCalls.push({
|
|
181
|
+
id: block.id,
|
|
182
|
+
type: "function",
|
|
183
|
+
function: {
|
|
184
|
+
name: block.name,
|
|
185
|
+
arguments: JSON.stringify(block.input)
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Build OpenAI response
|
|
192
|
+
const openaiResponse = {
|
|
193
|
+
id: id || `chatcmpl-${Date.now()}`,
|
|
194
|
+
object: "chat.completion",
|
|
195
|
+
created: Math.floor(Date.now() / 1000),
|
|
196
|
+
model: model,
|
|
197
|
+
choices: [
|
|
198
|
+
{
|
|
199
|
+
index: 0,
|
|
200
|
+
message: {
|
|
201
|
+
role: "assistant",
|
|
202
|
+
content: messageContent || null
|
|
203
|
+
},
|
|
204
|
+
finish_reason: mapStopReason(stop_reason)
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
usage: {
|
|
208
|
+
prompt_tokens: usage?.input_tokens || 0,
|
|
209
|
+
completion_tokens: usage?.output_tokens || 0,
|
|
210
|
+
total_tokens: (usage?.input_tokens || 0) + (usage?.output_tokens || 0)
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Add tool_calls if present
|
|
215
|
+
if (toolCalls.length > 0) {
|
|
216
|
+
openaiResponse.choices[0].message.tool_calls = toolCalls;
|
|
217
|
+
openaiResponse.choices[0].finish_reason = "tool_calls";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
logger.debug({
|
|
221
|
+
anthropicStopReason: stop_reason,
|
|
222
|
+
openaiFinishReason: openaiResponse.choices[0].finish_reason,
|
|
223
|
+
hasToolCalls: toolCalls.length > 0,
|
|
224
|
+
messageLength: messageContent.length
|
|
225
|
+
}, "Converted Anthropic response to OpenAI format");
|
|
226
|
+
|
|
227
|
+
return openaiResponse;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Convert Anthropic streaming chunk to OpenAI streaming format
|
|
232
|
+
* @param {Object} chunk - Anthropic SSE event
|
|
233
|
+
* @param {string} model - Model name
|
|
234
|
+
* @returns {string} OpenAI format SSE line (data: {...})
|
|
235
|
+
*/
|
|
236
|
+
function convertAnthropicStreamChunkToOpenAI(chunk, model = "claude-3-5-sonnet-20241022") {
|
|
237
|
+
const eventType = chunk.type;
|
|
238
|
+
|
|
239
|
+
if (eventType === "message_start") {
|
|
240
|
+
// Initial message metadata
|
|
241
|
+
return {
|
|
242
|
+
id: chunk.message?.id || `chatcmpl-${Date.now()}`,
|
|
243
|
+
object: "chat.completion.chunk",
|
|
244
|
+
created: Math.floor(Date.now() / 1000),
|
|
245
|
+
model: model,
|
|
246
|
+
choices: [
|
|
247
|
+
{
|
|
248
|
+
index: 0,
|
|
249
|
+
delta: { role: "assistant", content: "" },
|
|
250
|
+
finish_reason: null
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
} else if (eventType === "content_block_start") {
|
|
255
|
+
// Start of content block (text or tool_use)
|
|
256
|
+
const contentBlock = chunk.content_block;
|
|
257
|
+
|
|
258
|
+
if (contentBlock?.type === "tool_use") {
|
|
259
|
+
return {
|
|
260
|
+
id: `chatcmpl-${Date.now()}`,
|
|
261
|
+
object: "chat.completion.chunk",
|
|
262
|
+
created: Math.floor(Date.now() / 1000),
|
|
263
|
+
model: model,
|
|
264
|
+
choices: [
|
|
265
|
+
{
|
|
266
|
+
index: 0,
|
|
267
|
+
delta: {
|
|
268
|
+
tool_calls: [
|
|
269
|
+
{
|
|
270
|
+
index: chunk.index,
|
|
271
|
+
id: contentBlock.id,
|
|
272
|
+
type: "function",
|
|
273
|
+
function: {
|
|
274
|
+
name: contentBlock.name,
|
|
275
|
+
arguments: ""
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
finish_reason: null
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
} else if (eventType === "content_block_delta") {
|
|
286
|
+
// Incremental content
|
|
287
|
+
const delta = chunk.delta;
|
|
288
|
+
|
|
289
|
+
if (delta?.type === "text_delta") {
|
|
290
|
+
return {
|
|
291
|
+
id: `chatcmpl-${Date.now()}`,
|
|
292
|
+
object: "chat.completion.chunk",
|
|
293
|
+
created: Math.floor(Date.now() / 1000),
|
|
294
|
+
model: model,
|
|
295
|
+
choices: [
|
|
296
|
+
{
|
|
297
|
+
index: 0,
|
|
298
|
+
delta: { content: delta.text },
|
|
299
|
+
finish_reason: null
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
} else if (delta?.type === "input_json_delta") {
|
|
304
|
+
// Tool call arguments streaming
|
|
305
|
+
return {
|
|
306
|
+
id: `chatcmpl-${Date.now()}`,
|
|
307
|
+
object: "chat.completion.chunk",
|
|
308
|
+
created: Math.floor(Date.now() / 1000),
|
|
309
|
+
model: model,
|
|
310
|
+
choices: [
|
|
311
|
+
{
|
|
312
|
+
index: 0,
|
|
313
|
+
delta: {
|
|
314
|
+
tool_calls: [
|
|
315
|
+
{
|
|
316
|
+
index: chunk.index,
|
|
317
|
+
function: {
|
|
318
|
+
arguments: delta.partial_json
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
finish_reason: null
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
} else if (eventType === "message_delta") {
|
|
329
|
+
// Final message metadata (stop reason, usage)
|
|
330
|
+
const stopReason = chunk.delta?.stop_reason;
|
|
331
|
+
const usage = chunk.usage;
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
id: `chatcmpl-${Date.now()}`,
|
|
335
|
+
object: "chat.completion.chunk",
|
|
336
|
+
created: Math.floor(Date.now() / 1000),
|
|
337
|
+
model: model,
|
|
338
|
+
choices: [
|
|
339
|
+
{
|
|
340
|
+
index: 0,
|
|
341
|
+
delta: {},
|
|
342
|
+
finish_reason: mapStopReason(stopReason)
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
usage: usage ? {
|
|
346
|
+
prompt_tokens: 0, // Not available in streaming
|
|
347
|
+
completion_tokens: usage.output_tokens || 0,
|
|
348
|
+
total_tokens: usage.output_tokens || 0
|
|
349
|
+
} : undefined
|
|
350
|
+
};
|
|
351
|
+
} else if (eventType === "message_stop") {
|
|
352
|
+
// End of stream
|
|
353
|
+
return {
|
|
354
|
+
id: `chatcmpl-${Date.now()}`,
|
|
355
|
+
object: "chat.completion.chunk",
|
|
356
|
+
created: Math.floor(Date.now() / 1000),
|
|
357
|
+
model: model,
|
|
358
|
+
choices: [
|
|
359
|
+
{
|
|
360
|
+
index: 0,
|
|
361
|
+
delta: {},
|
|
362
|
+
finish_reason: "stop"
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Unknown event type, return empty chunk
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Map Anthropic stop_reason to OpenAI finish_reason
|
|
374
|
+
* @param {string} stopReason - Anthropic stop reason
|
|
375
|
+
* @returns {string} OpenAI finish reason
|
|
376
|
+
*/
|
|
377
|
+
function mapStopReason(stopReason) {
|
|
378
|
+
const mapping = {
|
|
379
|
+
"end_turn": "stop",
|
|
380
|
+
"max_tokens": "length",
|
|
381
|
+
"stop_sequence": "stop",
|
|
382
|
+
"tool_use": "tool_calls"
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return mapping[stopReason] || "stop";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
convertOpenAIToAnthropic,
|
|
390
|
+
convertAnthropicToOpenAI,
|
|
391
|
+
convertAnthropicStreamChunkToOpenAI,
|
|
392
|
+
mapStopReason
|
|
393
|
+
};
|
package/src/clients/routing.js
CHANGED
|
@@ -78,6 +78,18 @@ function determineProvider(payload) {
|
|
|
78
78
|
"Routing to llama.cpp (moderate tools)"
|
|
79
79
|
);
|
|
80
80
|
return "llamacpp";
|
|
81
|
+
} else if (config.lmstudio?.endpoint) {
|
|
82
|
+
logger.debug(
|
|
83
|
+
{ toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "lmstudio" },
|
|
84
|
+
"Routing to LM Studio (moderate tools)"
|
|
85
|
+
);
|
|
86
|
+
return "lmstudio";
|
|
87
|
+
} else if (config.bedrock?.apiKey) {
|
|
88
|
+
logger.debug(
|
|
89
|
+
{ toolCount, maxToolsForOllama, maxToolsForOpenRouter, decision: "bedrock" },
|
|
90
|
+
"Routing to AWS Bedrock (moderate tools)"
|
|
91
|
+
);
|
|
92
|
+
return "bedrock";
|
|
81
93
|
}
|
|
82
94
|
}
|
|
83
95
|
|
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"]);
|
|
65
|
+
const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock"]);
|
|
66
66
|
const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase();
|
|
67
67
|
const modelProvider = SUPPORTED_MODEL_PROVIDERS.has(rawModelProvider)
|
|
68
68
|
? rawModelProvider
|
|
@@ -78,10 +78,13 @@ const azureAnthropicVersion = process.env.AZURE_ANTHROPIC_VERSION ?? "2023-06-01
|
|
|
78
78
|
const ollamaEndpoint = process.env.OLLAMA_ENDPOINT ?? "http://localhost:11434";
|
|
79
79
|
const ollamaModel = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b";
|
|
80
80
|
const ollamaTimeout = Number.parseInt(process.env.OLLAMA_TIMEOUT_MS ?? "120000", 10);
|
|
81
|
+
const ollamaEmbeddingsEndpoint = process.env.OLLAMA_EMBEDDINGS_ENDPOINT ?? `${ollamaEndpoint}/api/embeddings`;
|
|
82
|
+
const ollamaEmbeddingsModel = process.env.OLLAMA_EMBEDDINGS_MODEL ?? "nomic-embed-text";
|
|
81
83
|
|
|
82
84
|
// OpenRouter configuration
|
|
83
85
|
const openRouterApiKey = process.env.OPENROUTER_API_KEY ?? null;
|
|
84
86
|
const openRouterModel = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini";
|
|
87
|
+
const openRouterEmbeddingsModel = process.env.OPENROUTER_EMBEDDINGS_MODEL ?? "openai/text-embedding-ada-002";
|
|
85
88
|
const openRouterEndpoint = process.env.OPENROUTER_ENDPOINT ?? "https://openrouter.ai/api/v1/chat/completions";
|
|
86
89
|
|
|
87
90
|
// Azure OpenAI configuration
|
|
@@ -101,6 +104,18 @@ const llamacppEndpoint = process.env.LLAMACPP_ENDPOINT?.trim() || "http://localh
|
|
|
101
104
|
const llamacppModel = process.env.LLAMACPP_MODEL?.trim() || "default";
|
|
102
105
|
const llamacppTimeout = Number.parseInt(process.env.LLAMACPP_TIMEOUT_MS ?? "120000", 10);
|
|
103
106
|
const llamacppApiKey = process.env.LLAMACPP_API_KEY?.trim() || null;
|
|
107
|
+
const llamacppEmbeddingsEndpoint = process.env.LLAMACPP_EMBEDDINGS_ENDPOINT?.trim() || `${llamacppEndpoint}/embeddings`;
|
|
108
|
+
|
|
109
|
+
// LM Studio configuration
|
|
110
|
+
const lmstudioEndpoint = process.env.LMSTUDIO_ENDPOINT?.trim() || "http://localhost:1234";
|
|
111
|
+
const lmstudioModel = process.env.LMSTUDIO_MODEL?.trim() || "default";
|
|
112
|
+
const lmstudioTimeout = Number.parseInt(process.env.LMSTUDIO_TIMEOUT_MS ?? "120000", 10);
|
|
113
|
+
const lmstudioApiKey = process.env.LMSTUDIO_API_KEY?.trim() || null;
|
|
114
|
+
|
|
115
|
+
// AWS Bedrock configuration
|
|
116
|
+
const bedrockRegion = process.env.AWS_BEDROCK_REGION?.trim() || process.env.AWS_REGION?.trim() || "us-east-1";
|
|
117
|
+
const bedrockApiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null; // Bearer token
|
|
118
|
+
const bedrockModelId = process.env.AWS_BEDROCK_MODEL_ID?.trim() || "anthropic.claude-3-5-sonnet-20241022-v2:0";
|
|
104
119
|
|
|
105
120
|
// Hybrid routing configuration
|
|
106
121
|
const preferOllama = process.env.PREFER_OLLAMA === "true";
|
|
@@ -201,6 +216,22 @@ if (modelProvider === "llamacpp") {
|
|
|
201
216
|
}
|
|
202
217
|
}
|
|
203
218
|
|
|
219
|
+
if (modelProvider === "lmstudio") {
|
|
220
|
+
try {
|
|
221
|
+
new URL(lmstudioEndpoint);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
throw new Error("LMSTUDIO_ENDPOINT must be a valid URL (default: http://localhost:1234)");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Validate Bedrock credentials when it's the primary provider
|
|
228
|
+
if (modelProvider === "bedrock" && !bedrockApiKey) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
"AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " +
|
|
231
|
+
"Generate from AWS Console → Bedrock → API Keys, then set AWS_BEDROCK_API_KEY in your .env file."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
204
235
|
// Validate hybrid routing configuration
|
|
205
236
|
if (preferOllama) {
|
|
206
237
|
if (!ollamaEndpoint) {
|
|
@@ -211,8 +242,11 @@ if (preferOllama) {
|
|
|
211
242
|
`FALLBACK_PROVIDER must be one of: ${Array.from(SUPPORTED_MODEL_PROVIDERS).join(", ")}`
|
|
212
243
|
);
|
|
213
244
|
}
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
|
|
246
|
+
// Prevent local providers from being used as fallback (they can fail just like Ollama)
|
|
247
|
+
const localProviders = ["ollama", "llamacpp", "lmstudio"];
|
|
248
|
+
if (fallbackEnabled && localProviders.includes(fallbackProvider)) {
|
|
249
|
+
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`);
|
|
216
250
|
}
|
|
217
251
|
|
|
218
252
|
// Ensure fallback provider is properly configured (only if fallback is enabled)
|
|
@@ -226,6 +260,9 @@ if (preferOllama) {
|
|
|
226
260
|
if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
|
|
227
261
|
throw new Error("FALLBACK_PROVIDER is set to 'azure-openai' but AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY are not configured. Please set these environment variables or choose a different fallback provider.");
|
|
228
262
|
}
|
|
263
|
+
if (fallbackProvider === "bedrock" && !bedrockApiKey) {
|
|
264
|
+
throw new Error("FALLBACK_PROVIDER is set to 'bedrock' but AWS_BEDROCK_API_KEY is not configured. Please set this environment variable or choose a different fallback provider.");
|
|
265
|
+
}
|
|
229
266
|
}
|
|
230
267
|
}
|
|
231
268
|
|
|
@@ -400,10 +437,13 @@ const config = {
|
|
|
400
437
|
endpoint: ollamaEndpoint,
|
|
401
438
|
model: ollamaModel,
|
|
402
439
|
timeout: Number.isNaN(ollamaTimeout) ? 120000 : ollamaTimeout,
|
|
440
|
+
embeddingsEndpoint: ollamaEmbeddingsEndpoint,
|
|
441
|
+
embeddingsModel: ollamaEmbeddingsModel,
|
|
403
442
|
},
|
|
404
443
|
openrouter: {
|
|
405
444
|
apiKey: openRouterApiKey,
|
|
406
445
|
model: openRouterModel,
|
|
446
|
+
embeddingsModel: openRouterEmbeddingsModel,
|
|
407
447
|
endpoint: openRouterEndpoint,
|
|
408
448
|
},
|
|
409
449
|
azureOpenAI: {
|
|
@@ -423,6 +463,18 @@ const config = {
|
|
|
423
463
|
model: llamacppModel,
|
|
424
464
|
timeout: Number.isNaN(llamacppTimeout) ? 120000 : llamacppTimeout,
|
|
425
465
|
apiKey: llamacppApiKey,
|
|
466
|
+
embeddingsEndpoint: llamacppEmbeddingsEndpoint,
|
|
467
|
+
},
|
|
468
|
+
lmstudio: {
|
|
469
|
+
endpoint: lmstudioEndpoint,
|
|
470
|
+
model: lmstudioModel,
|
|
471
|
+
timeout: Number.isNaN(lmstudioTimeout) ? 120000 : lmstudioTimeout,
|
|
472
|
+
apiKey: lmstudioApiKey,
|
|
473
|
+
},
|
|
474
|
+
bedrock: {
|
|
475
|
+
region: bedrockRegion,
|
|
476
|
+
apiKey: bedrockApiKey,
|
|
477
|
+
modelId: bedrockModelId,
|
|
426
478
|
},
|
|
427
479
|
modelProvider: {
|
|
428
480
|
type: modelProvider,
|
|
@@ -1994,7 +1994,14 @@ async function runAgentLoop({
|
|
|
1994
1994
|
// Use actualProvider from invokeModel for hybrid routing support
|
|
1995
1995
|
const actualProvider = databricksResponse.actualProvider || providerType;
|
|
1996
1996
|
|
|
1997
|
-
if (actualProvider === "
|
|
1997
|
+
if (actualProvider === "bedrock") {
|
|
1998
|
+
// Bedrock with Claude models returns native Anthropic format
|
|
1999
|
+
// Other models are already converted by bedrock-utils
|
|
2000
|
+
anthropicPayload = databricksResponse.json;
|
|
2001
|
+
if (Array.isArray(anthropicPayload?.content)) {
|
|
2002
|
+
anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
|
|
2003
|
+
}
|
|
2004
|
+
} else if (actualProvider === "azure-anthropic") {
|
|
1998
2005
|
anthropicPayload = databricksResponse.json;
|
|
1999
2006
|
if (Array.isArray(anthropicPayload?.content)) {
|
|
2000
2007
|
anthropicPayload.content = policy.sanitiseContent(anthropicPayload.content);
|
|
@@ -16,7 +16,7 @@ const TECHNICAL_KEYWORDS = /code|function|class|file|module|import|export|async|
|
|
|
16
16
|
const EXPLANATION_PATTERN = /explain|describe|summarize|what does|how does|tell me about|give me an overview|clarify|elaborate/i;
|
|
17
17
|
const WEB_PATTERN = /search|lookup|find info|google|documentation|docs|website|url|link|online|internet|browse/i;
|
|
18
18
|
const READ_PATTERN = /read|show|display|view|cat|check|inspect|look at|see|examine|review|print|output/i;
|
|
19
|
-
const WRITE_PATTERN = /write|create|add|update|modify|change|fix|delete|remove|insert|append|replace|save/i;
|
|
19
|
+
const WRITE_PATTERN = /write|create|add|update|modify|change|fix|delete|remove|insert|append|replace|save|put|make|generate|produce/i;
|
|
20
20
|
const EDIT_PATTERN = /edit|refactor|rename|move|reorganize|restructure|rewrite/i;
|
|
21
21
|
const EXECUTION_PATTERN = /run|execute|test|compile|build|deploy|start|install|launch|boot|fire up|npm|git|python|node|docker|bash|sh|cmd/i;
|
|
22
22
|
const COMPLEX_PATTERN = /implement|build|create|develop|design|architect|plan|strategy|approach|help with|work on|improve|optimize|enhance|refactor|migrate/i;
|