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.
- package/README.md +66 -17
- package/ROUTER_COMPARISON.md +173 -0
- package/TIER_ROUTING_PLAN.md +771 -0
- package/docs/index.md +49 -5
- package/final-test.js +33 -0
- package/package.json +2 -2
- 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/routing.js +12 -0
- package/src/config/index.js +47 -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/llamacpp-integration.test.js +13 -34
- package/test/lmstudio-integration.test.js +335 -0
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
|
|
@@ -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
|
-
|
|
215
|
-
|
|
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 === "
|
|
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
|
+
});
|