lynkr 3.2.1 → 3.3.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.
@@ -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
 
@@ -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
@@ -102,6 +102,17 @@ const llamacppModel = process.env.LLAMACPP_MODEL?.trim() || "default";
102
102
  const llamacppTimeout = Number.parseInt(process.env.LLAMACPP_TIMEOUT_MS ?? "120000", 10);
103
103
  const llamacppApiKey = process.env.LLAMACPP_API_KEY?.trim() || null;
104
104
 
105
+ // LM Studio configuration
106
+ const lmstudioEndpoint = process.env.LMSTUDIO_ENDPOINT?.trim() || "http://localhost:1234";
107
+ const lmstudioModel = process.env.LMSTUDIO_MODEL?.trim() || "default";
108
+ const lmstudioTimeout = Number.parseInt(process.env.LMSTUDIO_TIMEOUT_MS ?? "120000", 10);
109
+ const lmstudioApiKey = process.env.LMSTUDIO_API_KEY?.trim() || null;
110
+
111
+ // AWS Bedrock configuration
112
+ const bedrockRegion = process.env.AWS_BEDROCK_REGION?.trim() || process.env.AWS_REGION?.trim() || "us-east-1";
113
+ const bedrockApiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null; // Bearer token
114
+ const bedrockModelId = process.env.AWS_BEDROCK_MODEL_ID?.trim() || "anthropic.claude-3-5-sonnet-20241022-v2:0";
115
+
105
116
  // Hybrid routing configuration
106
117
  const preferOllama = process.env.PREFER_OLLAMA === "true";
107
118
  const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true
@@ -201,6 +212,22 @@ if (modelProvider === "llamacpp") {
201
212
  }
202
213
  }
203
214
 
215
+ if (modelProvider === "lmstudio") {
216
+ try {
217
+ new URL(lmstudioEndpoint);
218
+ } catch (err) {
219
+ throw new Error("LMSTUDIO_ENDPOINT must be a valid URL (default: http://localhost:1234)");
220
+ }
221
+ }
222
+
223
+ // Validate Bedrock credentials when it's the primary provider
224
+ if (modelProvider === "bedrock" && !bedrockApiKey) {
225
+ throw new Error(
226
+ "AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " +
227
+ "Generate from AWS Console → Bedrock → API Keys, then set AWS_BEDROCK_API_KEY in your .env file."
228
+ );
229
+ }
230
+
204
231
  // Validate hybrid routing configuration
205
232
  if (preferOllama) {
206
233
  if (!ollamaEndpoint) {
@@ -211,8 +238,11 @@ if (preferOllama) {
211
238
  `FALLBACK_PROVIDER must be one of: ${Array.from(SUPPORTED_MODEL_PROVIDERS).join(", ")}`
212
239
  );
213
240
  }
214
- if (fallbackEnabled && fallbackProvider === "ollama") {
215
- throw new Error("FALLBACK_PROVIDER cannot be 'ollama' (circular fallback)");
241
+
242
+ // Prevent local providers from being used as fallback (they can fail just like Ollama)
243
+ const localProviders = ["ollama", "llamacpp", "lmstudio"];
244
+ if (fallbackEnabled && localProviders.includes(fallbackProvider)) {
245
+ 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
246
  }
217
247
 
218
248
  // Ensure fallback provider is properly configured (only if fallback is enabled)
@@ -226,6 +256,9 @@ if (preferOllama) {
226
256
  if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) {
227
257
  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
258
  }
259
+ if (fallbackProvider === "bedrock" && !bedrockApiKey) {
260
+ 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.");
261
+ }
229
262
  }
230
263
  }
231
264
 
@@ -424,6 +457,17 @@ const config = {
424
457
  timeout: Number.isNaN(llamacppTimeout) ? 120000 : llamacppTimeout,
425
458
  apiKey: llamacppApiKey,
426
459
  },
460
+ lmstudio: {
461
+ endpoint: lmstudioEndpoint,
462
+ model: lmstudioModel,
463
+ timeout: Number.isNaN(lmstudioTimeout) ? 120000 : lmstudioTimeout,
464
+ apiKey: lmstudioApiKey,
465
+ },
466
+ bedrock: {
467
+ region: bedrockRegion,
468
+ apiKey: bedrockApiKey,
469
+ modelId: bedrockModelId,
470
+ },
427
471
  modelProvider: {
428
472
  type: modelProvider,
429
473
  defaultModel,
@@ -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 === "azure-anthropic") {
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;
@@ -0,0 +1,471 @@
1
+ const assert = require("assert");
2
+ const { describe, it, beforeEach, afterEach } = require("node:test");
3
+
4
+ describe("AWS Bedrock Integration", () => {
5
+ let originalEnv;
6
+
7
+ beforeEach(() => {
8
+ originalEnv = { ...process.env };
9
+
10
+ // Clear module cache
11
+ delete require.cache[require.resolve("../src/config")];
12
+ delete require.cache[require.resolve("../src/clients/routing")];
13
+ delete require.cache[require.resolve("../src/clients/bedrock-utils")];
14
+ });
15
+
16
+ afterEach(() => {
17
+ process.env = originalEnv;
18
+ });
19
+
20
+ describe("Configuration", () => {
21
+ it("should accept bedrock as a valid MODEL_PROVIDER", () => {
22
+ process.env.MODEL_PROVIDER = "bedrock";
23
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
24
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
25
+
26
+ const config = require("../src/config");
27
+ assert.strictEqual(config.modelProvider.type, "bedrock");
28
+ });
29
+
30
+ it("should throw error when AWS credentials are missing", () => {
31
+ process.env.MODEL_PROVIDER = "bedrock";
32
+ // Set to empty string to override .env file values
33
+ process.env.AWS_ACCESS_KEY_ID = "";
34
+ process.env.AWS_SECRET_ACCESS_KEY = "";
35
+
36
+ assert.throws(
37
+ () => require("../src/config"),
38
+ /AWS Bedrock requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY/
39
+ );
40
+ });
41
+
42
+ it("should use default region (us-east-1)", () => {
43
+ process.env.MODEL_PROVIDER = "bedrock";
44
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
45
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
46
+ process.env.AWS_BEDROCK_REGION = ""; // Override .env value
47
+ process.env.AWS_REGION = ""; // Override .env value
48
+
49
+ const config = require("../src/config");
50
+ assert.strictEqual(config.bedrock.region, "us-east-1");
51
+ });
52
+
53
+ it("should use custom region when AWS_BEDROCK_REGION is set", () => {
54
+ process.env.MODEL_PROVIDER = "bedrock";
55
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
56
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
57
+ process.env.AWS_BEDROCK_REGION = "us-west-2";
58
+
59
+ const config = require("../src/config");
60
+ assert.strictEqual(config.bedrock.region, "us-west-2");
61
+ });
62
+
63
+ it("should use AWS_REGION as fallback for region", () => {
64
+ process.env.MODEL_PROVIDER = "bedrock";
65
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
66
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
67
+ process.env.AWS_BEDROCK_REGION = ""; // Override .env value
68
+ process.env.AWS_REGION = "ap-southeast-1";
69
+
70
+ const config = require("../src/config");
71
+ assert.strictEqual(config.bedrock.region, "ap-southeast-1");
72
+ });
73
+
74
+ it("should use default model ID", () => {
75
+ process.env.MODEL_PROVIDER = "bedrock";
76
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
77
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
78
+ process.env.AWS_BEDROCK_MODEL_ID = ""; // Override .env value
79
+
80
+ const config = require("../src/config");
81
+ assert.strictEqual(config.bedrock.modelId, "anthropic.claude-3-5-sonnet-20241022-v2:0");
82
+ });
83
+
84
+ it("should use custom model ID when AWS_BEDROCK_MODEL_ID is set", () => {
85
+ process.env.MODEL_PROVIDER = "bedrock";
86
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
87
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
88
+ process.env.AWS_BEDROCK_MODEL_ID = "anthropic.claude-3-opus-20240229-v1:0";
89
+
90
+ const config = require("../src/config");
91
+ assert.strictEqual(config.bedrock.modelId, "anthropic.claude-3-opus-20240229-v1:0");
92
+ });
93
+ });
94
+
95
+ describe("Model Family Detection", () => {
96
+ it("should detect claude family", () => {
97
+ process.env.MODEL_PROVIDER = "databricks";
98
+ process.env.DATABRICKS_API_KEY = "test";
99
+ process.env.DATABRICKS_API_BASE = "http://test.com";
100
+
101
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
102
+ assert.strictEqual(
103
+ detectModelFamily("anthropic.claude-3-5-sonnet-20241022-v2:0"),
104
+ "claude"
105
+ );
106
+ });
107
+
108
+ it("should detect claude family from global inference profile", () => {
109
+ process.env.MODEL_PROVIDER = "databricks";
110
+ process.env.DATABRICKS_API_KEY = "test";
111
+ process.env.DATABRICKS_API_BASE = "http://test.com";
112
+
113
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
114
+ assert.strictEqual(
115
+ detectModelFamily("global.anthropic.claude-sonnet-4-5-20250929-v1:0"),
116
+ "claude"
117
+ );
118
+ });
119
+
120
+ it("should detect claude family from US inference profile", () => {
121
+ process.env.MODEL_PROVIDER = "databricks";
122
+ process.env.DATABRICKS_API_KEY = "test";
123
+ process.env.DATABRICKS_API_BASE = "http://test.com";
124
+
125
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
126
+ assert.strictEqual(
127
+ detectModelFamily("us.anthropic.claude-sonnet-4-5-20250929-v1:0"),
128
+ "claude"
129
+ );
130
+ });
131
+
132
+ it("should detect titan family", () => {
133
+ process.env.MODEL_PROVIDER = "databricks";
134
+ process.env.DATABRICKS_API_KEY = "test";
135
+ process.env.DATABRICKS_API_BASE = "http://test.com";
136
+
137
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
138
+ assert.strictEqual(
139
+ detectModelFamily("amazon.titan-text-express-v1"),
140
+ "titan"
141
+ );
142
+ });
143
+
144
+ it("should detect llama family", () => {
145
+ process.env.MODEL_PROVIDER = "databricks";
146
+ process.env.DATABRICKS_API_KEY = "test";
147
+ process.env.DATABRICKS_API_BASE = "http://test.com";
148
+
149
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
150
+ assert.strictEqual(
151
+ detectModelFamily("meta.llama3-70b-instruct-v1:0"),
152
+ "llama"
153
+ );
154
+ });
155
+
156
+ it("should detect jurassic family", () => {
157
+ process.env.MODEL_PROVIDER = "databricks";
158
+ process.env.DATABRICKS_API_KEY = "test";
159
+ process.env.DATABRICKS_API_BASE = "http://test.com";
160
+
161
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
162
+ assert.strictEqual(
163
+ detectModelFamily("ai21.j2-ultra-v1"),
164
+ "jurassic"
165
+ );
166
+ });
167
+
168
+ it("should detect cohere family", () => {
169
+ process.env.MODEL_PROVIDER = "databricks";
170
+ process.env.DATABRICKS_API_KEY = "test";
171
+ process.env.DATABRICKS_API_BASE = "http://test.com";
172
+
173
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
174
+ assert.strictEqual(
175
+ detectModelFamily("cohere.command-text-v14"),
176
+ "cohere"
177
+ );
178
+ });
179
+
180
+ it("should detect mistral family", () => {
181
+ process.env.MODEL_PROVIDER = "databricks";
182
+ process.env.DATABRICKS_API_KEY = "test";
183
+ process.env.DATABRICKS_API_BASE = "http://test.com";
184
+
185
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
186
+ assert.strictEqual(
187
+ detectModelFamily("mistral.mistral-7b-instruct-v0:2"),
188
+ "mistral"
189
+ );
190
+ });
191
+
192
+ it("should throw error for unsupported model", () => {
193
+ process.env.MODEL_PROVIDER = "databricks";
194
+ process.env.DATABRICKS_API_KEY = "test";
195
+ process.env.DATABRICKS_API_BASE = "http://test.com";
196
+
197
+ const { detectModelFamily } = require("../src/clients/bedrock-utils");
198
+ assert.throws(
199
+ () => detectModelFamily("unknown.model-v1"),
200
+ /Unsupported Bedrock model/
201
+ );
202
+ });
203
+ });
204
+
205
+ describe("Format Conversion - Request", () => {
206
+ it("should keep Claude requests in Anthropic format", () => {
207
+ process.env.MODEL_PROVIDER = "databricks";
208
+ process.env.DATABRICKS_API_KEY = "test";
209
+ process.env.DATABRICKS_API_BASE = "http://test.com";
210
+
211
+ const { convertAnthropicToBedrockFormat } = require("../src/clients/bedrock-utils");
212
+
213
+ const anthropicBody = {
214
+ messages: [{ role: "user", content: "Hello" }],
215
+ max_tokens: 1024,
216
+ temperature: 0.7,
217
+ };
218
+
219
+ const result = convertAnthropicToBedrockFormat(anthropicBody, "claude");
220
+
221
+ assert.strictEqual(result.anthropic_version, "bedrock-2023-05-31");
222
+ assert.strictEqual(result.max_tokens, 1024);
223
+ assert.strictEqual(result.temperature, 0.7);
224
+ assert.deepStrictEqual(result.messages, anthropicBody.messages);
225
+ });
226
+
227
+ it("should convert to Titan format", () => {
228
+ process.env.MODEL_PROVIDER = "databricks";
229
+ process.env.DATABRICKS_API_KEY = "test";
230
+ process.env.DATABRICKS_API_BASE = "http://test.com";
231
+
232
+ const { convertAnthropicToBedrockFormat } = require("../src/clients/bedrock-utils");
233
+
234
+ const anthropicBody = {
235
+ messages: [{ role: "user", content: "Hello" }],
236
+ max_tokens: 1024,
237
+ temperature: 0.8,
238
+ };
239
+
240
+ const result = convertAnthropicToBedrockFormat(anthropicBody, "titan");
241
+
242
+ assert.strictEqual(result.textGenerationConfig.maxTokenCount, 1024);
243
+ assert.strictEqual(result.textGenerationConfig.temperature, 0.8);
244
+ assert.ok(result.inputText.includes("Human: Hello"));
245
+ });
246
+
247
+ it("should convert to Llama format", () => {
248
+ process.env.MODEL_PROVIDER = "databricks";
249
+ process.env.DATABRICKS_API_KEY = "test";
250
+ process.env.DATABRICKS_API_BASE = "http://test.com";
251
+
252
+ const { convertAnthropicToBedrockFormat } = require("../src/clients/bedrock-utils");
253
+
254
+ const anthropicBody = {
255
+ messages: [{ role: "user", content: "Test" }],
256
+ max_tokens: 512,
257
+ temperature: 0.9,
258
+ };
259
+
260
+ const result = convertAnthropicToBedrockFormat(anthropicBody, "llama");
261
+
262
+ assert.strictEqual(result.max_gen_len, 512);
263
+ assert.strictEqual(result.temperature, 0.9);
264
+ assert.ok(result.prompt.includes("Human: Test"));
265
+ });
266
+
267
+ it("should convert to Jurassic format", () => {
268
+ process.env.MODEL_PROVIDER = "databricks";
269
+ process.env.DATABRICKS_API_KEY = "test";
270
+ process.env.DATABRICKS_API_BASE = "http://test.com";
271
+
272
+ const { convertAnthropicToBedrockFormat } = require("../src/clients/bedrock-utils");
273
+
274
+ const anthropicBody = {
275
+ messages: [{ role: "user", content: "Test" }],
276
+ max_tokens: 200,
277
+ temperature: 0.7,
278
+ };
279
+
280
+ const result = convertAnthropicToBedrockFormat(anthropicBody, "jurassic");
281
+
282
+ assert.strictEqual(result.maxTokens, 200);
283
+ assert.strictEqual(result.temperature, 0.7);
284
+ assert.ok(result.prompt.includes("Human: Test"));
285
+ });
286
+ });
287
+
288
+ describe("Format Conversion - Response", () => {
289
+ it("should parse Claude responses (native Anthropic)", () => {
290
+ process.env.MODEL_PROVIDER = "databricks";
291
+ process.env.DATABRICKS_API_KEY = "test";
292
+ process.env.DATABRICKS_API_BASE = "http://test.com";
293
+
294
+ const { convertBedrockResponseToAnthropic } = require("../src/clients/bedrock-utils");
295
+
296
+ const claudeResponse = {
297
+ id: "msg_123",
298
+ type: "message",
299
+ role: "assistant",
300
+ content: [{ type: "text", text: "Hello!" }],
301
+ stop_reason: "end_turn",
302
+ usage: { input_tokens: 10, output_tokens: 5 },
303
+ };
304
+
305
+ const result = convertBedrockResponseToAnthropic(
306
+ claudeResponse,
307
+ "claude",
308
+ "anthropic.claude-3-5-sonnet-20241022-v2:0"
309
+ );
310
+
311
+ assert.deepStrictEqual(result, claudeResponse);
312
+ });
313
+
314
+ it("should convert Titan responses to Anthropic format", () => {
315
+ process.env.MODEL_PROVIDER = "databricks";
316
+ process.env.DATABRICKS_API_KEY = "test";
317
+ process.env.DATABRICKS_API_BASE = "http://test.com";
318
+
319
+ const { convertBedrockResponseToAnthropic } = require("../src/clients/bedrock-utils");
320
+
321
+ const titanResponse = {
322
+ results: [{
323
+ outputText: "Response text",
324
+ tokenCount: 50,
325
+ completionReason: "FINISH",
326
+ }],
327
+ inputTextTokenCount: 20,
328
+ };
329
+
330
+ const result = convertBedrockResponseToAnthropic(
331
+ titanResponse,
332
+ "titan",
333
+ "amazon.titan-text-express-v1"
334
+ );
335
+
336
+ assert.strictEqual(result.role, "assistant");
337
+ assert.strictEqual(result.content[0].type, "text");
338
+ assert.strictEqual(result.content[0].text, "Response text");
339
+ assert.strictEqual(result.stop_reason, "end_turn");
340
+ assert.strictEqual(result.usage.input_tokens, 20);
341
+ assert.strictEqual(result.usage.output_tokens, 50);
342
+ });
343
+
344
+ it("should convert Llama responses to Anthropic format", () => {
345
+ process.env.MODEL_PROVIDER = "databricks";
346
+ process.env.DATABRICKS_API_KEY = "test";
347
+ process.env.DATABRICKS_API_BASE = "http://test.com";
348
+
349
+ const { convertBedrockResponseToAnthropic } = require("../src/clients/bedrock-utils");
350
+
351
+ const llamaResponse = {
352
+ generation: "Llama response",
353
+ prompt_token_count: 15,
354
+ generation_token_count: 30,
355
+ stop_reason: "stop",
356
+ };
357
+
358
+ const result = convertBedrockResponseToAnthropic(
359
+ llamaResponse,
360
+ "llama",
361
+ "meta.llama3-70b-instruct-v1:0"
362
+ );
363
+
364
+ assert.strictEqual(result.role, "assistant");
365
+ assert.strictEqual(result.content[0].text, "Llama response");
366
+ assert.strictEqual(result.stop_reason, "end_turn");
367
+ assert.strictEqual(result.usage.input_tokens, 15);
368
+ assert.strictEqual(result.usage.output_tokens, 30);
369
+ });
370
+ });
371
+
372
+ describe("Routing", () => {
373
+ it("should route to bedrock when MODEL_PROVIDER is bedrock", () => {
374
+ process.env.MODEL_PROVIDER = "bedrock";
375
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
376
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
377
+ process.env.PREFER_OLLAMA = "false";
378
+
379
+ const config = require("../src/config");
380
+ const routing = require("../src/clients/routing");
381
+
382
+ const payload = { messages: [{ role: "user", content: "test" }] };
383
+ const provider = routing.determineProvider(payload);
384
+
385
+ // When not in hybrid mode, should use primary provider
386
+ assert.strictEqual(provider, "bedrock");
387
+ });
388
+
389
+ it("should route to bedrock in hybrid mode for moderate tool counts", () => {
390
+ process.env.MODEL_PROVIDER = "ollama";
391
+ process.env.PREFER_OLLAMA = "true";
392
+ process.env.OLLAMA_MODEL = "llama3.1";
393
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
394
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
395
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
396
+ process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING = "2";
397
+ process.env.FALLBACK_ENABLED = "true";
398
+ process.env.FALLBACK_PROVIDER = "bedrock";
399
+
400
+ // Clear other providers to ensure bedrock is chosen
401
+ delete process.env.OPENROUTER_API_KEY;
402
+ delete process.env.OPENAI_API_KEY;
403
+ delete process.env.AZURE_OPENAI_API_KEY;
404
+ delete process.env.AZURE_OPENAI_ENDPOINT;
405
+ delete process.env.LLAMACPP_ENDPOINT;
406
+ delete process.env.LMSTUDIO_ENDPOINT;
407
+ delete process.env.DATABRICKS_API_KEY;
408
+ delete process.env.DATABRICKS_API_BASE;
409
+
410
+ const config = require("../src/config");
411
+ const routing = require("../src/clients/routing");
412
+
413
+ // 20 tools should exceed both Ollama and OpenRouter limits, routing to fallback provider (bedrock)
414
+ const payload = {
415
+ messages: [{ role: "user", content: "test" }],
416
+ tools: Array(20).fill({ name: "tool" }),
417
+ };
418
+ const provider = routing.determineProvider(payload);
419
+
420
+ assert.strictEqual(provider, "bedrock");
421
+ });
422
+ });
423
+
424
+ describe("Fallback Provider", () => {
425
+ it("should allow bedrock as fallback provider", () => {
426
+ process.env.MODEL_PROVIDER = "ollama";
427
+ process.env.PREFER_OLLAMA = "true";
428
+ process.env.OLLAMA_MODEL = "llama3.1";
429
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
430
+ process.env.FALLBACK_PROVIDER = "bedrock";
431
+ process.env.AWS_ACCESS_KEY_ID = "AKIATEST123";
432
+ process.env.AWS_SECRET_ACCESS_KEY = "testSecretKey123";
433
+ process.env.FALLBACK_ENABLED = "true";
434
+
435
+ // Should not throw
436
+ const config = require("../src/config");
437
+ assert.strictEqual(config.modelProvider.fallbackProvider, "bedrock");
438
+ });
439
+
440
+ it("should validate bedrock credentials when used as fallback", () => {
441
+ process.env.MODEL_PROVIDER = "ollama";
442
+ process.env.PREFER_OLLAMA = "true";
443
+ process.env.OLLAMA_MODEL = "llama3.1";
444
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
445
+ process.env.FALLBACK_PROVIDER = "bedrock";
446
+ process.env.FALLBACK_ENABLED = "true";
447
+ // Set to empty string to override .env file values
448
+ process.env.AWS_ACCESS_KEY_ID = "";
449
+ process.env.AWS_SECRET_ACCESS_KEY = "";
450
+
451
+ assert.throws(
452
+ () => require("../src/config"),
453
+ /FALLBACK_PROVIDER is set to 'bedrock' but AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are not configured/
454
+ );
455
+ });
456
+
457
+ it("should not allow local providers as fallback", () => {
458
+ process.env.MODEL_PROVIDER = "ollama";
459
+ process.env.PREFER_OLLAMA = "true";
460
+ process.env.OLLAMA_MODEL = "llama3.1";
461
+ process.env.OLLAMA_ENDPOINT = "http://localhost:11434";
462
+ process.env.FALLBACK_PROVIDER = "llamacpp";
463
+ process.env.FALLBACK_ENABLED = "true";
464
+
465
+ assert.throws(
466
+ () => require("../src/config"),
467
+ /FALLBACK_PROVIDER cannot be 'llamacpp'/
468
+ );
469
+ });
470
+ });
471
+ });