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
package/src/api/router.js
CHANGED
|
@@ -3,6 +3,7 @@ const { processMessage } = require("../orchestrator");
|
|
|
3
3
|
const { getSession } = require("../sessions");
|
|
4
4
|
const metrics = require("../metrics");
|
|
5
5
|
const { createRateLimiter } = require("./middleware/rate-limiter");
|
|
6
|
+
const openaiRouter = require("./openai-router");
|
|
6
7
|
|
|
7
8
|
const router = express.Router();
|
|
8
9
|
|
|
@@ -383,4 +384,7 @@ router.get("/api/tokens/stats", (req, res) => {
|
|
|
383
384
|
}
|
|
384
385
|
});
|
|
385
386
|
|
|
387
|
+
// Mount OpenAI-compatible endpoints for Cursor IDE support
|
|
388
|
+
router.use("/v1", openaiRouter);
|
|
389
|
+
|
|
386
390
|
module.exports = router;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Bedrock Model Format Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles format conversion between Anthropic format and various Bedrock model families.
|
|
5
|
+
* Supports: Claude, Titan, Llama, Jurassic, Cohere, Mistral
|
|
6
|
+
*
|
|
7
|
+
* @module clients/bedrock-utils
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const logger = require("../logger");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert Anthropic messages array to a simple text prompt
|
|
14
|
+
* @param {Array} messages - Anthropic messages array
|
|
15
|
+
* @returns {string} Combined prompt text
|
|
16
|
+
*/
|
|
17
|
+
function messagesToPrompt(messages) {
|
|
18
|
+
return messages
|
|
19
|
+
.map((msg) => {
|
|
20
|
+
const role = msg.role === "user" ? "Human" : "Assistant";
|
|
21
|
+
let content = "";
|
|
22
|
+
|
|
23
|
+
if (typeof msg.content === "string") {
|
|
24
|
+
content = msg.content;
|
|
25
|
+
} else if (Array.isArray(msg.content)) {
|
|
26
|
+
content = msg.content
|
|
27
|
+
.filter((block) => block.type === "text")
|
|
28
|
+
.map((block) => block.text)
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `${role}: ${content}`;
|
|
33
|
+
})
|
|
34
|
+
.join("\n\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect which model family a Bedrock model ID belongs to
|
|
39
|
+
* @param {string} modelId - AWS Bedrock model ID or inference profile ID
|
|
40
|
+
* Examples: "anthropic.claude-3-5-sonnet-20241022-v2:0", "us.deepseek.r1-v1:0", "qwen.qwen3-235b-v1:0"
|
|
41
|
+
* @returns {string} Model family identifier
|
|
42
|
+
*/
|
|
43
|
+
function detectModelFamily(modelId) {
|
|
44
|
+
// Handle inference profiles (e.g., "global.anthropic.claude-..." or "us.deepseek.r1-...")
|
|
45
|
+
if (modelId.includes(".anthropic.claude")) return "claude";
|
|
46
|
+
if (modelId.includes(".amazon.titan")) return "titan";
|
|
47
|
+
if (modelId.includes(".amazon.nova")) return "nova";
|
|
48
|
+
if (modelId.includes(".meta.llama")) return "llama";
|
|
49
|
+
if (modelId.includes(".ai21.jamba")) return "jamba";
|
|
50
|
+
if (modelId.includes(".cohere.command")) return "cohere";
|
|
51
|
+
if (modelId.includes(".mistral")) return "mistral";
|
|
52
|
+
if (modelId.includes(".deepseek")) return "deepseek";
|
|
53
|
+
if (modelId.includes(".qwen")) return "qwen";
|
|
54
|
+
if (modelId.includes(".openai")) return "openai";
|
|
55
|
+
if (modelId.includes(".google.gemma")) return "gemma";
|
|
56
|
+
if (modelId.includes(".minimax")) return "minimax";
|
|
57
|
+
if (modelId.includes(".writer")) return "writer";
|
|
58
|
+
if (modelId.includes(".kimi")) return "kimi";
|
|
59
|
+
if (modelId.includes(".luma")) return "luma";
|
|
60
|
+
if (modelId.includes(".twelvelabs")) return "twelvelabs";
|
|
61
|
+
|
|
62
|
+
// Handle direct model IDs (standard format)
|
|
63
|
+
if (modelId.startsWith("anthropic.claude")) return "claude";
|
|
64
|
+
if (modelId.startsWith("amazon.titan")) return "titan";
|
|
65
|
+
if (modelId.startsWith("amazon.nova")) return "nova";
|
|
66
|
+
if (modelId.startsWith("meta.llama")) return "llama";
|
|
67
|
+
if (modelId.startsWith("ai21.j2")) return "jurassic";
|
|
68
|
+
if (modelId.startsWith("ai21.jamba")) return "jamba";
|
|
69
|
+
if (modelId.startsWith("cohere.command")) return "cohere";
|
|
70
|
+
if (modelId.startsWith("mistral.")) return "mistral";
|
|
71
|
+
if (modelId.startsWith("deepseek.")) return "deepseek";
|
|
72
|
+
if (modelId.startsWith("qwen.")) return "qwen";
|
|
73
|
+
if (modelId.startsWith("openai.")) return "openai";
|
|
74
|
+
if (modelId.startsWith("google.gemma")) return "gemma";
|
|
75
|
+
if (modelId.startsWith("minimax.")) return "minimax";
|
|
76
|
+
if (modelId.startsWith("writer.")) return "writer";
|
|
77
|
+
if (modelId.startsWith("kimi.")) return "kimi";
|
|
78
|
+
if (modelId.startsWith("luma.")) return "luma";
|
|
79
|
+
if (modelId.startsWith("twelvelabs.")) return "twelvelabs";
|
|
80
|
+
|
|
81
|
+
// If we can't detect, assume it works with Converse API (Bedrock's unified API)
|
|
82
|
+
logger.info({ modelId }, "Unknown Bedrock model family - assuming Converse API compatibility");
|
|
83
|
+
return "converse";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert Anthropic format request to Bedrock-specific format
|
|
88
|
+
* @param {Object} body - Request body in Anthropic format
|
|
89
|
+
* @param {string} modelFamily - Model family from detectModelFamily()
|
|
90
|
+
* @returns {Object} Request body in Bedrock model-specific format
|
|
91
|
+
*/
|
|
92
|
+
function convertAnthropicToBedrockFormat(body, modelFamily) {
|
|
93
|
+
switch (modelFamily) {
|
|
94
|
+
case "claude":
|
|
95
|
+
// Claude models use native Anthropic Messages API format
|
|
96
|
+
// Only need to add anthropic_version field for Bedrock
|
|
97
|
+
return {
|
|
98
|
+
anthropic_version: "bedrock-2023-05-31",
|
|
99
|
+
max_tokens: body.max_tokens || 4096,
|
|
100
|
+
messages: body.messages,
|
|
101
|
+
system: body.system,
|
|
102
|
+
temperature: body.temperature,
|
|
103
|
+
top_p: body.top_p,
|
|
104
|
+
tools: body.tools,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
case "titan":
|
|
108
|
+
// Amazon Titan format
|
|
109
|
+
return {
|
|
110
|
+
inputText: messagesToPrompt(body.messages),
|
|
111
|
+
textGenerationConfig: {
|
|
112
|
+
maxTokenCount: body.max_tokens || 4096,
|
|
113
|
+
temperature: body.temperature || 0.7,
|
|
114
|
+
topP: body.top_p || 1.0,
|
|
115
|
+
stopSequences: [],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
case "llama":
|
|
120
|
+
// Meta Llama format
|
|
121
|
+
const llamaPrompt = messagesToPrompt(body.messages);
|
|
122
|
+
return {
|
|
123
|
+
prompt: llamaPrompt,
|
|
124
|
+
max_gen_len: body.max_tokens || 2048,
|
|
125
|
+
temperature: body.temperature || 0.7,
|
|
126
|
+
top_p: body.top_p || 0.9,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
case "jurassic":
|
|
130
|
+
// AI21 Jurassic format
|
|
131
|
+
return {
|
|
132
|
+
prompt: messagesToPrompt(body.messages),
|
|
133
|
+
maxTokens: body.max_tokens || 200,
|
|
134
|
+
temperature: body.temperature || 0.7,
|
|
135
|
+
topP: body.top_p || 1,
|
|
136
|
+
stopSequences: [],
|
|
137
|
+
countPenalty: {
|
|
138
|
+
scale: 0,
|
|
139
|
+
},
|
|
140
|
+
presencePenalty: {
|
|
141
|
+
scale: 0,
|
|
142
|
+
},
|
|
143
|
+
frequencyPenalty: {
|
|
144
|
+
scale: 0,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
case "cohere":
|
|
149
|
+
// Cohere Command format
|
|
150
|
+
return {
|
|
151
|
+
prompt: messagesToPrompt(body.messages),
|
|
152
|
+
max_tokens: body.max_tokens || 400,
|
|
153
|
+
temperature: body.temperature || 0.75,
|
|
154
|
+
p: body.top_p || 1.0,
|
|
155
|
+
k: 0,
|
|
156
|
+
stop_sequences: [],
|
|
157
|
+
return_likelihoods: "NONE",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
case "mistral":
|
|
161
|
+
// Mistral format (similar to OpenAI)
|
|
162
|
+
return {
|
|
163
|
+
prompt: messagesToPrompt(body.messages),
|
|
164
|
+
max_tokens: body.max_tokens || 2048,
|
|
165
|
+
temperature: body.temperature || 0.7,
|
|
166
|
+
top_p: body.top_p || 1.0,
|
|
167
|
+
stop: [],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`Unsupported model family: ${modelFamily}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Convert Bedrock response to Anthropic format
|
|
177
|
+
* @param {Object} response - Raw response from Bedrock model
|
|
178
|
+
* @param {string} modelFamily - Model family from detectModelFamily()
|
|
179
|
+
* @param {string} modelId - Full Bedrock model ID
|
|
180
|
+
* @returns {Object} Response in Anthropic format
|
|
181
|
+
*/
|
|
182
|
+
function convertBedrockResponseToAnthropic(response, modelFamily, modelId) {
|
|
183
|
+
switch (modelFamily) {
|
|
184
|
+
case "claude":
|
|
185
|
+
// Claude models return native Anthropic format
|
|
186
|
+
// No conversion needed - pass through directly
|
|
187
|
+
return response;
|
|
188
|
+
|
|
189
|
+
case "titan":
|
|
190
|
+
// Convert Titan response to Anthropic format
|
|
191
|
+
return {
|
|
192
|
+
id: `bedrock-titan-${Date.now()}`,
|
|
193
|
+
type: "message",
|
|
194
|
+
role: "assistant",
|
|
195
|
+
model: modelId,
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: response.results?.[0]?.outputText || "",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
stop_reason: response.results?.[0]?.completionReason === "FINISH" ? "end_turn" : "max_tokens",
|
|
203
|
+
usage: {
|
|
204
|
+
input_tokens: response.inputTextTokenCount || 0,
|
|
205
|
+
output_tokens: response.results?.[0]?.tokenCount || 0,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
case "llama":
|
|
210
|
+
// Convert Llama response to Anthropic format
|
|
211
|
+
return {
|
|
212
|
+
id: `bedrock-llama-${Date.now()}`,
|
|
213
|
+
type: "message",
|
|
214
|
+
role: "assistant",
|
|
215
|
+
model: modelId,
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: response.generation || "",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
stop_reason: response.stop_reason === "stop" ? "end_turn" : "max_tokens",
|
|
223
|
+
usage: {
|
|
224
|
+
input_tokens: response.prompt_token_count || 0,
|
|
225
|
+
output_tokens: response.generation_token_count || 0,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
case "jurassic":
|
|
230
|
+
// Convert Jurassic response to Anthropic format
|
|
231
|
+
return {
|
|
232
|
+
id: `bedrock-jurassic-${Date.now()}`,
|
|
233
|
+
type: "message",
|
|
234
|
+
role: "assistant",
|
|
235
|
+
model: modelId,
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text",
|
|
239
|
+
text: response.completions?.[0]?.data?.text || "",
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
stop_reason: response.completions?.[0]?.finishReason?.reason === "endoftext" ? "end_turn" : "max_tokens",
|
|
243
|
+
usage: {
|
|
244
|
+
input_tokens: 0, // Jurassic doesn't provide input token count
|
|
245
|
+
output_tokens: response.completions?.[0]?.data?.tokens?.length || 0,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
case "cohere":
|
|
250
|
+
// Convert Cohere response to Anthropic format
|
|
251
|
+
return {
|
|
252
|
+
id: `bedrock-cohere-${Date.now()}`,
|
|
253
|
+
type: "message",
|
|
254
|
+
role: "assistant",
|
|
255
|
+
model: modelId,
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: response.generations?.[0]?.text || "",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
stop_reason: response.generations?.[0]?.finish_reason === "COMPLETE" ? "end_turn" : "max_tokens",
|
|
263
|
+
usage: {
|
|
264
|
+
input_tokens: 0, // Cohere doesn't provide token counts in basic response
|
|
265
|
+
output_tokens: 0,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
case "mistral":
|
|
270
|
+
// Convert Mistral response to Anthropic format
|
|
271
|
+
return {
|
|
272
|
+
id: `bedrock-mistral-${Date.now()}`,
|
|
273
|
+
type: "message",
|
|
274
|
+
role: "assistant",
|
|
275
|
+
model: modelId,
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: response.outputs?.[0]?.text || "",
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
stop_reason: response.outputs?.[0]?.stop_reason === "stop" ? "end_turn" : "max_tokens",
|
|
283
|
+
usage: {
|
|
284
|
+
input_tokens: 0, // Mistral doesn't provide token counts
|
|
285
|
+
output_tokens: 0,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
default:
|
|
290
|
+
throw new Error(`Unsupported model family: ${modelFamily}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
detectModelFamily,
|
|
296
|
+
convertAnthropicToBedrockFormat,
|
|
297
|
+
convertBedrockResponseToAnthropic,
|
|
298
|
+
};
|
|
@@ -7,6 +7,11 @@ const { getMetricsCollector } = require("../observability/metrics");
|
|
|
7
7
|
const logger = require("../logger");
|
|
8
8
|
const { STANDARD_TOOLS } = require("./standard-tools");
|
|
9
9
|
const { convertAnthropicToolsToOpenRouter } = require("./openrouter-utils");
|
|
10
|
+
const {
|
|
11
|
+
detectModelFamily,
|
|
12
|
+
convertAnthropicToBedrockFormat,
|
|
13
|
+
convertBedrockResponseToAnthropic
|
|
14
|
+
} = require("./bedrock-utils");
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
|
|
@@ -574,6 +579,262 @@ async function invokeLlamaCpp(body) {
|
|
|
574
579
|
return performJsonRequest(endpoint, { headers, body: llamacppBody }, "llama.cpp");
|
|
575
580
|
}
|
|
576
581
|
|
|
582
|
+
async function invokeLMStudio(body) {
|
|
583
|
+
if (!config.lmstudio?.endpoint) {
|
|
584
|
+
throw new Error("LM Studio endpoint is not configured.");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const {
|
|
588
|
+
convertAnthropicToolsToOpenRouter,
|
|
589
|
+
convertAnthropicMessagesToOpenRouter
|
|
590
|
+
} = require("./openrouter-utils");
|
|
591
|
+
|
|
592
|
+
const endpoint = `${config.lmstudio.endpoint}/v1/chat/completions`;
|
|
593
|
+
const headers = {
|
|
594
|
+
"Content-Type": "application/json",
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// Add API key if configured (for secured LM Studio servers)
|
|
598
|
+
if (config.lmstudio.apiKey) {
|
|
599
|
+
headers["Authorization"] = `Bearer ${config.lmstudio.apiKey}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Convert messages to OpenAI format
|
|
603
|
+
const messages = convertAnthropicMessagesToOpenRouter(body.messages || []);
|
|
604
|
+
|
|
605
|
+
// Handle system message
|
|
606
|
+
if (body.system) {
|
|
607
|
+
messages.unshift({ role: "system", content: body.system });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const lmstudioBody = {
|
|
611
|
+
messages,
|
|
612
|
+
temperature: body.temperature ?? 0.7,
|
|
613
|
+
max_tokens: body.max_tokens ?? 4096,
|
|
614
|
+
top_p: body.top_p ?? 1.0,
|
|
615
|
+
stream: body.stream ?? false
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// Inject standard tools if client didn't send any
|
|
619
|
+
let toolsToSend = body.tools;
|
|
620
|
+
let toolsInjected = false;
|
|
621
|
+
|
|
622
|
+
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
623
|
+
toolsToSend = STANDARD_TOOLS;
|
|
624
|
+
toolsInjected = true;
|
|
625
|
+
logger.info({
|
|
626
|
+
injectedToolCount: STANDARD_TOOLS.length,
|
|
627
|
+
injectedToolNames: STANDARD_TOOLS.map(t => t.name),
|
|
628
|
+
reason: "Client did not send tools (passthrough mode)"
|
|
629
|
+
}, "=== INJECTING STANDARD TOOLS (LM Studio) ===");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (Array.isArray(toolsToSend) && toolsToSend.length > 0) {
|
|
633
|
+
lmstudioBody.tools = convertAnthropicToolsToOpenRouter(toolsToSend);
|
|
634
|
+
lmstudioBody.tool_choice = "auto";
|
|
635
|
+
logger.info({
|
|
636
|
+
toolCount: toolsToSend.length,
|
|
637
|
+
toolNames: toolsToSend.map(t => t.name),
|
|
638
|
+
toolsInjected
|
|
639
|
+
}, "=== SENDING TOOLS TO LM STUDIO ===");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
logger.info({
|
|
643
|
+
endpoint,
|
|
644
|
+
hasTools: !!lmstudioBody.tools,
|
|
645
|
+
toolCount: lmstudioBody.tools?.length || 0,
|
|
646
|
+
temperature: lmstudioBody.temperature,
|
|
647
|
+
max_tokens: lmstudioBody.max_tokens,
|
|
648
|
+
}, "=== LM STUDIO REQUEST ===");
|
|
649
|
+
|
|
650
|
+
return performJsonRequest(endpoint, { headers, body: lmstudioBody }, "LM Studio");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function invokeBedrock(body) {
|
|
654
|
+
// 1. Validate Bearer token
|
|
655
|
+
if (!config.bedrock?.apiKey) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
"AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " +
|
|
658
|
+
"Generate from AWS Console → Bedrock → API Keys, then set AWS_BEDROCK_API_KEY in your .env file."
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const bearerToken = config.bedrock.apiKey;
|
|
663
|
+
logger.info({ authMethod: "Bearer Token" }, "=== BEDROCK AUTH ===");
|
|
664
|
+
|
|
665
|
+
// 2. Inject standard tools if needed
|
|
666
|
+
let toolsToSend = body.tools;
|
|
667
|
+
let toolsInjected = false;
|
|
668
|
+
|
|
669
|
+
if (!Array.isArray(toolsToSend) || toolsToSend.length === 0) {
|
|
670
|
+
toolsToSend = STANDARD_TOOLS;
|
|
671
|
+
toolsInjected = true;
|
|
672
|
+
logger.info({
|
|
673
|
+
injectedToolCount: STANDARD_TOOLS.length,
|
|
674
|
+
injectedToolNames: STANDARD_TOOLS.map(t => t.name),
|
|
675
|
+
reason: "Client did not send tools (passthrough mode)"
|
|
676
|
+
}, "=== INJECTING STANDARD TOOLS (Bedrock) ===");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const bedrockBody = { ...body, tools: toolsToSend };
|
|
680
|
+
|
|
681
|
+
// 4. Detect model family and convert format
|
|
682
|
+
const modelId = config.bedrock.modelId;
|
|
683
|
+
const modelFamily = detectModelFamily(modelId);
|
|
684
|
+
|
|
685
|
+
logger.info({
|
|
686
|
+
modelId,
|
|
687
|
+
modelFamily,
|
|
688
|
+
hasTools: !!bedrockBody.tools,
|
|
689
|
+
toolCount: bedrockBody.tools?.length || 0,
|
|
690
|
+
streaming: body.stream || false,
|
|
691
|
+
}, "=== BEDROCK REQUEST (FETCH) ===");
|
|
692
|
+
|
|
693
|
+
// 5. Convert to Bedrock Converse API format (simpler, more universal)
|
|
694
|
+
// Bedrock Converse API only allows 'user' and 'assistant' roles in messages array
|
|
695
|
+
|
|
696
|
+
// Extract system messages from messages array (if any)
|
|
697
|
+
const systemMessages = bedrockBody.messages.filter(msg => msg.role === 'system');
|
|
698
|
+
|
|
699
|
+
const converseBody = {
|
|
700
|
+
messages: bedrockBody.messages
|
|
701
|
+
.filter(msg => msg.role !== 'system') // Filter out system messages
|
|
702
|
+
.map(msg => ({
|
|
703
|
+
role: msg.role,
|
|
704
|
+
content: Array.isArray(msg.content)
|
|
705
|
+
? msg.content.map(c => ({ text: c.text || c.content || "" }))
|
|
706
|
+
: [{ text: msg.content }]
|
|
707
|
+
}))
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// Add system prompt (from Anthropic system field OR extracted from messages)
|
|
711
|
+
if (bedrockBody.system) {
|
|
712
|
+
converseBody.system = [{ text: bedrockBody.system }];
|
|
713
|
+
} else if (systemMessages.length > 0) {
|
|
714
|
+
// If system messages were in the messages array, use the first one
|
|
715
|
+
const systemContent = Array.isArray(systemMessages[0].content)
|
|
716
|
+
? systemMessages[0].content.map(c => c.text || c.content || "").join("\n")
|
|
717
|
+
: systemMessages[0].content;
|
|
718
|
+
converseBody.system = [{ text: systemContent }];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Add inference config
|
|
722
|
+
if (bedrockBody.max_tokens) {
|
|
723
|
+
converseBody.inferenceConfig = {
|
|
724
|
+
maxTokens: bedrockBody.max_tokens,
|
|
725
|
+
temperature: bedrockBody.temperature,
|
|
726
|
+
topP: bedrockBody.top_p,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Add tools if present
|
|
731
|
+
if (bedrockBody.tools && bedrockBody.tools.length > 0) {
|
|
732
|
+
converseBody.toolConfig = {
|
|
733
|
+
tools: bedrockBody.tools.map(tool => ({
|
|
734
|
+
toolSpec: {
|
|
735
|
+
name: tool.name,
|
|
736
|
+
description: tool.description,
|
|
737
|
+
inputSchema: {
|
|
738
|
+
json: tool.input_schema
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}))
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 6. Construct Bedrock Converse API endpoint
|
|
746
|
+
const path = `/model/${modelId}/converse`;
|
|
747
|
+
const host = `bedrock-runtime.${config.bedrock.region}.amazonaws.com`;
|
|
748
|
+
const endpoint = `https://${host}${path}`;
|
|
749
|
+
|
|
750
|
+
logger.info({
|
|
751
|
+
endpoint,
|
|
752
|
+
authMethod: "Bearer Token",
|
|
753
|
+
hasSystem: !!converseBody.system,
|
|
754
|
+
hasTools: !!converseBody.toolConfig,
|
|
755
|
+
messageCount: converseBody.messages.length
|
|
756
|
+
}, "=== BEDROCK CONVERSE API REQUEST ===");
|
|
757
|
+
|
|
758
|
+
// 7. Prepare request headers with Bearer token
|
|
759
|
+
const requestHeaders = {
|
|
760
|
+
"Content-Type": "application/json",
|
|
761
|
+
"Authorization": `Bearer ${bearerToken}`
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// 8. Make the Converse API request
|
|
765
|
+
try {
|
|
766
|
+
const response = await performJsonRequest(endpoint, {
|
|
767
|
+
headers: requestHeaders,
|
|
768
|
+
body: converseBody // Pass object, performJsonRequest will stringify it
|
|
769
|
+
}, "Bedrock"); // Add provider label for logging
|
|
770
|
+
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
const errorText = response.text; // Use property, not method
|
|
773
|
+
logger.error({
|
|
774
|
+
status: response.status,
|
|
775
|
+
error: errorText
|
|
776
|
+
}, "=== BEDROCK CONVERSE API ERROR ===");
|
|
777
|
+
throw new Error(`Bedrock Converse API failed: ${response.status} ${errorText}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Parse Converse API response (already parsed by performJsonRequest)
|
|
781
|
+
const converseResponse = response.json; // Use property, not method
|
|
782
|
+
|
|
783
|
+
logger.info({
|
|
784
|
+
stopReason: converseResponse.stopReason,
|
|
785
|
+
inputTokens: converseResponse.usage?.inputTokens || 0,
|
|
786
|
+
outputTokens: converseResponse.usage?.outputTokens || 0,
|
|
787
|
+
hasToolUse: !!converseResponse.output?.message?.content?.some(c => c.toolUse)
|
|
788
|
+
}, "=== BEDROCK CONVERSE API RESPONSE ===");
|
|
789
|
+
|
|
790
|
+
// Convert Converse API response to Anthropic format
|
|
791
|
+
const message = converseResponse.output.message;
|
|
792
|
+
const anthropicResponse = {
|
|
793
|
+
id: `bedrock-${Date.now()}`,
|
|
794
|
+
type: "message",
|
|
795
|
+
role: message.role,
|
|
796
|
+
model: modelId,
|
|
797
|
+
content: message.content.map(item => {
|
|
798
|
+
if (item.text) {
|
|
799
|
+
return { type: "text", text: item.text };
|
|
800
|
+
} else if (item.toolUse) {
|
|
801
|
+
return {
|
|
802
|
+
type: "tool_use",
|
|
803
|
+
id: item.toolUse.toolUseId,
|
|
804
|
+
name: item.toolUse.name,
|
|
805
|
+
input: item.toolUse.input
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return item;
|
|
809
|
+
}),
|
|
810
|
+
stop_reason: converseResponse.stopReason === "end_turn" ? "end_turn" :
|
|
811
|
+
converseResponse.stopReason === "tool_use" ? "tool_use" :
|
|
812
|
+
converseResponse.stopReason === "max_tokens" ? "max_tokens" : "end_turn",
|
|
813
|
+
usage: {
|
|
814
|
+
input_tokens: converseResponse.usage?.inputTokens || 0,
|
|
815
|
+
output_tokens: converseResponse.usage?.outputTokens || 0,
|
|
816
|
+
},
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
ok: true,
|
|
821
|
+
status: 200,
|
|
822
|
+
json: anthropicResponse,
|
|
823
|
+
actualProvider: "bedrock",
|
|
824
|
+
modelFamily,
|
|
825
|
+
};
|
|
826
|
+
} catch (e) {
|
|
827
|
+
logger.error({
|
|
828
|
+
error: e.message,
|
|
829
|
+
modelId,
|
|
830
|
+
region: config.bedrock.region,
|
|
831
|
+
endpoint,
|
|
832
|
+
stack: e.stack
|
|
833
|
+
}, "=== BEDROCK CONVERSE API ERROR ===");
|
|
834
|
+
throw e;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
577
838
|
async function invokeModel(body, options = {}) {
|
|
578
839
|
const { determineProvider, isFallbackEnabled, getFallbackProvider } = require("./routing");
|
|
579
840
|
const metricsCollector = getMetricsCollector();
|
|
@@ -617,6 +878,10 @@ async function invokeModel(body, options = {}) {
|
|
|
617
878
|
return await invokeOpenAI(body);
|
|
618
879
|
} else if (initialProvider === "llamacpp") {
|
|
619
880
|
return await invokeLlamaCpp(body);
|
|
881
|
+
} else if (initialProvider === "lmstudio") {
|
|
882
|
+
return await invokeLMStudio(body);
|
|
883
|
+
} else if (initialProvider === "bedrock") {
|
|
884
|
+
return await invokeBedrock(body);
|
|
620
885
|
}
|
|
621
886
|
return await invokeDatabricks(body);
|
|
622
887
|
});
|