mcp-rubber-duck 1.1.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/.dockerignore +19 -0
- package/.env.desktop.example +145 -0
- package/.env.example +45 -0
- package/.env.pi.example +106 -0
- package/.env.template +165 -0
- package/.eslintrc.json +40 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +65 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +58 -0
- package/.github/ISSUE_TEMPLATE/question.md +67 -0
- package/.github/pull_request_template.md +111 -0
- package/.github/workflows/docker-build.yml +138 -0
- package/.github/workflows/release.yml +182 -0
- package/.github/workflows/security.yml +141 -0
- package/.github/workflows/semantic-release.yml +89 -0
- package/.prettierrc +10 -0
- package/.releaserc.json +66 -0
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +242 -0
- package/Dockerfile +62 -0
- package/LICENSE +21 -0
- package/README.md +803 -0
- package/audit-ci.json +8 -0
- package/config/claude_desktop.json +14 -0
- package/config/config.example.json +91 -0
- package/dist/config/config.d.ts +51 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +301 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/types.d.ts +356 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +41 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +29 -0
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -0
- package/dist/providers/duck-provider-enhanced.js +230 -0
- package/dist/providers/duck-provider-enhanced.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +54 -0
- package/dist/providers/enhanced-manager.d.ts.map +1 -0
- package/dist/providers/enhanced-manager.js +217 -0
- package/dist/providers/enhanced-manager.js.map +1 -0
- package/dist/providers/manager.d.ts +28 -0
- package/dist/providers/manager.d.ts.map +1 -0
- package/dist/providers/manager.js +204 -0
- package/dist/providers/manager.js.map +1 -0
- package/dist/providers/provider.d.ts +29 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +179 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/providers/types.d.ts +69 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +501 -0
- package/dist/server.js.map +1 -0
- package/dist/services/approval.d.ts +44 -0
- package/dist/services/approval.d.ts.map +1 -0
- package/dist/services/approval.js +159 -0
- package/dist/services/approval.js.map +1 -0
- package/dist/services/cache.d.ts +21 -0
- package/dist/services/cache.d.ts.map +1 -0
- package/dist/services/cache.js +63 -0
- package/dist/services/cache.js.map +1 -0
- package/dist/services/conversation.d.ts +24 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +108 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/function-bridge.d.ts +41 -0
- package/dist/services/function-bridge.d.ts.map +1 -0
- package/dist/services/function-bridge.js +259 -0
- package/dist/services/function-bridge.js.map +1 -0
- package/dist/services/health.d.ts +17 -0
- package/dist/services/health.d.ts.map +1 -0
- package/dist/services/health.js +77 -0
- package/dist/services/health.js.map +1 -0
- package/dist/services/mcp-client-manager.d.ts +49 -0
- package/dist/services/mcp-client-manager.d.ts.map +1 -0
- package/dist/services/mcp-client-manager.js +279 -0
- package/dist/services/mcp-client-manager.js.map +1 -0
- package/dist/tools/approve-mcp-request.d.ts +9 -0
- package/dist/tools/approve-mcp-request.d.ts.map +1 -0
- package/dist/tools/approve-mcp-request.js +111 -0
- package/dist/tools/approve-mcp-request.js.map +1 -0
- package/dist/tools/ask-duck.d.ts +9 -0
- package/dist/tools/ask-duck.d.ts.map +1 -0
- package/dist/tools/ask-duck.js +43 -0
- package/dist/tools/ask-duck.js.map +1 -0
- package/dist/tools/chat-duck.d.ts +9 -0
- package/dist/tools/chat-duck.d.ts.map +1 -0
- package/dist/tools/chat-duck.js +57 -0
- package/dist/tools/chat-duck.js.map +1 -0
- package/dist/tools/clear-conversations.d.ts +8 -0
- package/dist/tools/clear-conversations.d.ts.map +1 -0
- package/dist/tools/clear-conversations.js +17 -0
- package/dist/tools/clear-conversations.js.map +1 -0
- package/dist/tools/compare-ducks.d.ts +8 -0
- package/dist/tools/compare-ducks.d.ts.map +1 -0
- package/dist/tools/compare-ducks.js +49 -0
- package/dist/tools/compare-ducks.js.map +1 -0
- package/dist/tools/duck-council.d.ts +8 -0
- package/dist/tools/duck-council.d.ts.map +1 -0
- package/dist/tools/duck-council.js +69 -0
- package/dist/tools/duck-council.js.map +1 -0
- package/dist/tools/get-pending-approvals.d.ts +15 -0
- package/dist/tools/get-pending-approvals.d.ts.map +1 -0
- package/dist/tools/get-pending-approvals.js +74 -0
- package/dist/tools/get-pending-approvals.js.map +1 -0
- package/dist/tools/list-ducks.d.ts +9 -0
- package/dist/tools/list-ducks.d.ts.map +1 -0
- package/dist/tools/list-ducks.js +47 -0
- package/dist/tools/list-ducks.js.map +1 -0
- package/dist/tools/list-models.d.ts +8 -0
- package/dist/tools/list-models.d.ts.map +1 -0
- package/dist/tools/list-models.js +72 -0
- package/dist/tools/list-models.js.map +1 -0
- package/dist/tools/mcp-status.d.ts +17 -0
- package/dist/tools/mcp-status.d.ts.map +1 -0
- package/dist/tools/mcp-status.js +100 -0
- package/dist/tools/mcp-status.js.map +1 -0
- package/dist/utils/ascii-art.d.ts +19 -0
- package/dist/utils/ascii-art.d.ts.map +1 -0
- package/dist/utils/ascii-art.js +73 -0
- package/dist/utils/ascii-art.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/safe-logger.d.ts +23 -0
- package/dist/utils/safe-logger.d.ts.map +1 -0
- package/dist/utils/safe-logger.js +145 -0
- package/dist/utils/safe-logger.js.map +1 -0
- package/docker-compose.yml +161 -0
- package/jest.config.js +26 -0
- package/package.json +65 -0
- package/scripts/build-multiarch.sh +290 -0
- package/scripts/deploy-raspbian.sh +410 -0
- package/scripts/deploy.sh +322 -0
- package/scripts/gh-deploy.sh +343 -0
- package/scripts/setup-docker-raspbian.sh +530 -0
- package/server.json +8 -0
- package/src/config/config.ts +357 -0
- package/src/config/types.ts +89 -0
- package/src/index.ts +114 -0
- package/src/providers/duck-provider-enhanced.ts +294 -0
- package/src/providers/enhanced-manager.ts +290 -0
- package/src/providers/manager.ts +257 -0
- package/src/providers/provider.ts +207 -0
- package/src/providers/types.ts +78 -0
- package/src/server.ts +603 -0
- package/src/services/approval.ts +225 -0
- package/src/services/cache.ts +79 -0
- package/src/services/conversation.ts +146 -0
- package/src/services/function-bridge.ts +329 -0
- package/src/services/health.ts +107 -0
- package/src/services/mcp-client-manager.ts +362 -0
- package/src/tools/approve-mcp-request.ts +126 -0
- package/src/tools/ask-duck.ts +74 -0
- package/src/tools/chat-duck.ts +82 -0
- package/src/tools/clear-conversations.ts +24 -0
- package/src/tools/compare-ducks.ts +67 -0
- package/src/tools/duck-council.ts +88 -0
- package/src/tools/get-pending-approvals.ts +90 -0
- package/src/tools/list-ducks.ts +65 -0
- package/src/tools/list-models.ts +101 -0
- package/src/tools/mcp-status.ts +117 -0
- package/src/utils/ascii-art.ts +85 -0
- package/src/utils/logger.ts +116 -0
- package/src/utils/safe-logger.ts +165 -0
- package/systemd/mcp-rubber-duck-with-ollama.service +55 -0
- package/systemd/mcp-rubber-duck.service +58 -0
- package/test-functionality.js +147 -0
- package/test-mcp-interface.js +221 -0
- package/tests/ascii-art.test.ts +36 -0
- package/tests/config.test.ts +239 -0
- package/tests/conversation.test.ts +308 -0
- package/tests/mcp-bridge.test.ts +291 -0
- package/tests/providers.test.ts +269 -0
- package/tests/tools/clear-conversations.test.ts +163 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Test script for MCP interface (simulates Claude Desktop communication)
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { Readable, Writable } from 'stream';
|
|
7
|
+
|
|
8
|
+
console.log('🦆 Testing MCP Rubber Duck via MCP Interface\n');
|
|
9
|
+
|
|
10
|
+
class MCPClient {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.messageId = 0;
|
|
13
|
+
this.responses = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
start() {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
// Spawn the MCP server as Claude Desktop would
|
|
19
|
+
this.process = spawn('node', ['dist/index.js'], {
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
MCP_SERVER: 'true',
|
|
23
|
+
LOG_LEVEL: 'error'
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.process.stderr.on('data', (data) => {
|
|
28
|
+
console.error(`Server error: ${data}`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
this.process.on('error', reject);
|
|
32
|
+
this.process.on('exit', (code) => {
|
|
33
|
+
console.log(`Server exited with code ${code}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Set up JSON-RPC communication
|
|
37
|
+
this.setupCommunication();
|
|
38
|
+
|
|
39
|
+
// Initialize the connection
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
this.sendRequest('initialize', {
|
|
42
|
+
protocolVersion: '2024-11-05',
|
|
43
|
+
capabilities: {},
|
|
44
|
+
clientInfo: {
|
|
45
|
+
name: 'test-client',
|
|
46
|
+
version: '1.0.0'
|
|
47
|
+
}
|
|
48
|
+
}).then(resolve).catch(reject);
|
|
49
|
+
}, 100);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setupCommunication() {
|
|
54
|
+
let buffer = '';
|
|
55
|
+
|
|
56
|
+
this.process.stdout.on('data', (chunk) => {
|
|
57
|
+
buffer += chunk.toString();
|
|
58
|
+
const lines = buffer.split('\n');
|
|
59
|
+
buffer = lines.pop() || '';
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.trim()) {
|
|
63
|
+
try {
|
|
64
|
+
const message = JSON.parse(line);
|
|
65
|
+
this.handleMessage(message);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('Failed to parse message:', line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
handleMessage(message) {
|
|
75
|
+
if (message.id && this.responses.has(message.id)) {
|
|
76
|
+
const { resolve, reject } = this.responses.get(message.id);
|
|
77
|
+
this.responses.delete(message.id);
|
|
78
|
+
|
|
79
|
+
if (message.error) {
|
|
80
|
+
reject(new Error(message.error.message));
|
|
81
|
+
} else {
|
|
82
|
+
resolve(message.result);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sendRequest(method, params = {}) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const id = `msg_${++this.messageId}`;
|
|
90
|
+
const request = {
|
|
91
|
+
jsonrpc: '2.0',
|
|
92
|
+
id,
|
|
93
|
+
method,
|
|
94
|
+
params
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.responses.set(id, { resolve, reject });
|
|
98
|
+
this.process.stdin.write(JSON.stringify(request) + '\n');
|
|
99
|
+
|
|
100
|
+
// Timeout after 10 seconds
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
if (this.responses.has(id)) {
|
|
103
|
+
this.responses.delete(id);
|
|
104
|
+
reject(new Error('Request timeout'));
|
|
105
|
+
}
|
|
106
|
+
}, 10000);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async callTool(toolName, args) {
|
|
111
|
+
return this.sendRequest('tools/call', {
|
|
112
|
+
name: toolName,
|
|
113
|
+
arguments: args
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listTools() {
|
|
118
|
+
return this.sendRequest('tools/list');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stop() {
|
|
122
|
+
if (this.process) {
|
|
123
|
+
this.process.kill();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runMCPTests() {
|
|
129
|
+
const client = new MCPClient();
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Initialize connection
|
|
133
|
+
console.log('📡 Initializing MCP connection...');
|
|
134
|
+
const initResult = await client.start();
|
|
135
|
+
console.log('✅ Connected to MCP server');
|
|
136
|
+
console.log(` Protocol version: ${initResult.protocolVersion}`);
|
|
137
|
+
console.log(` Server: ${initResult.serverInfo?.name || 'Unknown'} v${initResult.serverInfo?.version || 'Unknown'}\n`);
|
|
138
|
+
|
|
139
|
+
// Test 1: List available tools
|
|
140
|
+
console.log('📋 Test 1: List available tools');
|
|
141
|
+
const toolsResult = await client.listTools();
|
|
142
|
+
console.log(`Found ${toolsResult.tools.length} tools:`);
|
|
143
|
+
for (const tool of toolsResult.tools) {
|
|
144
|
+
console.log(` - ${tool.name}: ${tool.description}`);
|
|
145
|
+
}
|
|
146
|
+
console.log();
|
|
147
|
+
|
|
148
|
+
// Test 2: List ducks
|
|
149
|
+
console.log('🦆 Test 2: List ducks via MCP');
|
|
150
|
+
const ducksResult = await client.callTool('list_ducks', {
|
|
151
|
+
check_health: false
|
|
152
|
+
});
|
|
153
|
+
console.log('Response received (truncated):',
|
|
154
|
+
ducksResult.content[0].text.substring(0, 200) + '...\n');
|
|
155
|
+
|
|
156
|
+
// Test 3: Ask a duck
|
|
157
|
+
console.log('💬 Test 3: Ask OpenAI via MCP');
|
|
158
|
+
const askResult = await client.callTool('ask_duck', {
|
|
159
|
+
prompt: 'What is MCP? Answer in one sentence.',
|
|
160
|
+
provider: 'openai'
|
|
161
|
+
});
|
|
162
|
+
console.log('Response:', askResult.content[0].text.split('\n')[0] + '\n');
|
|
163
|
+
|
|
164
|
+
// Test 4: List models
|
|
165
|
+
console.log('📚 Test 4: List models via MCP');
|
|
166
|
+
const modelsResult = await client.callTool('list_models', {
|
|
167
|
+
provider: 'openai'
|
|
168
|
+
});
|
|
169
|
+
const modelLines = modelsResult.content[0].text.split('\n').slice(0, 10);
|
|
170
|
+
console.log('First few models:', modelLines.join('\n') + '...\n');
|
|
171
|
+
|
|
172
|
+
// Test 5: Compare ducks
|
|
173
|
+
console.log('🔍 Test 5: Compare ducks via MCP');
|
|
174
|
+
const compareResult = await client.callTool('compare_ducks', {
|
|
175
|
+
prompt: 'Is water wet? Answer yes or no.'
|
|
176
|
+
});
|
|
177
|
+
console.log('Comparison started (truncated):',
|
|
178
|
+
compareResult.content[0].text.substring(0, 300) + '...\n');
|
|
179
|
+
|
|
180
|
+
// Test 6: Error handling
|
|
181
|
+
console.log('❌ Test 6: Error handling');
|
|
182
|
+
try {
|
|
183
|
+
await client.callTool('nonexistent_tool', {});
|
|
184
|
+
console.log('ERROR: Should have thrown an error!');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.log('✅ Correctly handled invalid tool error:', error.message);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
|
|
190
|
+
// Test 7: Chat with context
|
|
191
|
+
console.log('💬 Test 7: Stateful conversation via MCP');
|
|
192
|
+
await client.callTool('chat_with_duck', {
|
|
193
|
+
conversation_id: 'mcp-test',
|
|
194
|
+
message: 'Remember this number: 42',
|
|
195
|
+
provider: 'openai'
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const chatResult = await client.callTool('chat_with_duck', {
|
|
199
|
+
conversation_id: 'mcp-test',
|
|
200
|
+
message: 'What number did I ask you to remember?'
|
|
201
|
+
});
|
|
202
|
+
console.log('Context test:', chatResult.content[0].text.split('\n')[0] + '\n');
|
|
203
|
+
|
|
204
|
+
console.log('✅ All MCP interface tests completed successfully!');
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('❌ Test failed:', error.message);
|
|
208
|
+
} finally {
|
|
209
|
+
client.stop();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Add timeout to prevent hanging
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
console.error('Test timeout - forcefully exiting');
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}, 30000);
|
|
219
|
+
|
|
220
|
+
// Run the tests
|
|
221
|
+
runMCPTests().catch(console.error);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { formatDuckResponse, getRandomDuckMessage, duckMessages } from '../src/utils/ascii-art';
|
|
3
|
+
|
|
4
|
+
describe('formatDuckResponse', () => {
|
|
5
|
+
it('should format response without model', () => {
|
|
6
|
+
const result = formatDuckResponse('GPT Duck', 'Hello world');
|
|
7
|
+
expect(result).toBe('🦆 [GPT Duck]: Hello world');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should format response with model', () => {
|
|
11
|
+
const result = formatDuckResponse('GPT Duck', 'Hello world', 'gpt-4o-mini');
|
|
12
|
+
expect(result).toBe('🦆 [GPT Duck | gpt-4o-mini]: Hello world');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should handle undefined model', () => {
|
|
16
|
+
const result = formatDuckResponse('GPT Duck', 'Hello world', undefined);
|
|
17
|
+
expect(result).toBe('🦆 [GPT Duck]: Hello world');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('getRandomDuckMessage', () => {
|
|
22
|
+
it('should return a message from startup messages', () => {
|
|
23
|
+
const result = getRandomDuckMessage('startup');
|
|
24
|
+
expect(duckMessages.startup).toContain(result);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should return a message from error messages', () => {
|
|
28
|
+
const result = getRandomDuckMessage('error');
|
|
29
|
+
expect(duckMessages.error).toContain(result);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return a message from success messages', () => {
|
|
33
|
+
const result = getRandomDuckMessage('success');
|
|
34
|
+
expect(duckMessages.success).toContain(result);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { ConfigManager } from '../src/config/config';
|
|
3
|
+
|
|
4
|
+
// Mock logger to avoid console noise during tests
|
|
5
|
+
jest.mock('../src/utils/logger');
|
|
6
|
+
|
|
7
|
+
describe('ConfigManager - Custom Providers', () => {
|
|
8
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Save original environment
|
|
12
|
+
originalEnv = { ...process.env };
|
|
13
|
+
|
|
14
|
+
// Clear custom provider environment variables
|
|
15
|
+
Object.keys(process.env).forEach(key => {
|
|
16
|
+
if (key.startsWith('CUSTOM_')) {
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Clear other provider keys that might interfere
|
|
22
|
+
delete process.env.OPENAI_API_KEY;
|
|
23
|
+
delete process.env.GEMINI_API_KEY;
|
|
24
|
+
delete process.env.GROQ_API_KEY;
|
|
25
|
+
delete process.env.TOGETHER_API_KEY;
|
|
26
|
+
delete process.env.PERPLEXITY_API_KEY;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
// Restore original environment
|
|
31
|
+
process.env = originalEnv;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getCustomProvidersFromEnv', () => {
|
|
35
|
+
it('should parse single custom provider from environment', () => {
|
|
36
|
+
// Set up environment variables
|
|
37
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
38
|
+
process.env.CUSTOM_MYAPI_API_KEY = 'test-key-123';
|
|
39
|
+
process.env.CUSTOM_MYAPI_BASE_URL = 'https://my-api.com/v1';
|
|
40
|
+
process.env.CUSTOM_MYAPI_MODELS = 'model1,model2,model3';
|
|
41
|
+
process.env.CUSTOM_MYAPI_DEFAULT_MODEL = 'model1';
|
|
42
|
+
process.env.CUSTOM_MYAPI_NICKNAME = 'My Custom Duck';
|
|
43
|
+
|
|
44
|
+
const configManager = new ConfigManager();
|
|
45
|
+
const providers = configManager.getAllProviders();
|
|
46
|
+
|
|
47
|
+
expect(providers.myapi).toBeDefined();
|
|
48
|
+
expect(providers.myapi).toEqual({
|
|
49
|
+
api_key: 'test-key-123',
|
|
50
|
+
base_url: 'https://my-api.com/v1',
|
|
51
|
+
models: ['model1', 'model2', 'model3'],
|
|
52
|
+
default_model: 'model1',
|
|
53
|
+
nickname: 'My Custom Duck',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should parse multiple custom providers from environment', () => {
|
|
58
|
+
// Set up multiple providers
|
|
59
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
60
|
+
process.env.CUSTOM_API1_API_KEY = 'key1';
|
|
61
|
+
process.env.CUSTOM_API1_BASE_URL = 'https://api1.com/v1';
|
|
62
|
+
process.env.CUSTOM_API1_NICKNAME = 'API 1 Duck';
|
|
63
|
+
|
|
64
|
+
process.env.CUSTOM_API2_API_KEY = 'key2';
|
|
65
|
+
process.env.CUSTOM_API2_BASE_URL = 'https://api2.com/v1';
|
|
66
|
+
process.env.CUSTOM_API2_MODELS = 'model-a,model-b';
|
|
67
|
+
process.env.CUSTOM_API2_DEFAULT_MODEL = 'model-a';
|
|
68
|
+
|
|
69
|
+
const configManager = new ConfigManager();
|
|
70
|
+
const providers = configManager.getAllProviders();
|
|
71
|
+
|
|
72
|
+
// Check first provider
|
|
73
|
+
expect(providers.api1).toBeDefined();
|
|
74
|
+
expect(providers.api1.api_key).toBe('key1');
|
|
75
|
+
expect(providers.api1.base_url).toBe('https://api1.com/v1');
|
|
76
|
+
expect(providers.api1.nickname).toBe('API 1 Duck');
|
|
77
|
+
expect(providers.api1.models).toEqual(['custom-model']); // default
|
|
78
|
+
expect(providers.api1.default_model).toBe('custom-model'); // default
|
|
79
|
+
|
|
80
|
+
// Check second provider
|
|
81
|
+
expect(providers.api2).toBeDefined();
|
|
82
|
+
expect(providers.api2.api_key).toBe('key2');
|
|
83
|
+
expect(providers.api2.base_url).toBe('https://api2.com/v1');
|
|
84
|
+
expect(providers.api2.models).toEqual(['model-a', 'model-b']);
|
|
85
|
+
expect(providers.api2.default_model).toBe('model-a');
|
|
86
|
+
expect(providers.api2.nickname).toBe('API2 Duck'); // auto-generated
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should convert provider names to lowercase', () => {
|
|
90
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
91
|
+
process.env.CUSTOM_MYUPPERAPI_API_KEY = 'test-key';
|
|
92
|
+
process.env.CUSTOM_MYUPPERAPI_BASE_URL = 'https://upper.com/v1';
|
|
93
|
+
|
|
94
|
+
const configManager = new ConfigManager();
|
|
95
|
+
const providers = configManager.getAllProviders();
|
|
96
|
+
|
|
97
|
+
expect(providers.myupperapi).toBeDefined();
|
|
98
|
+
expect(providers.MYUPPERAPI).toBeUndefined();
|
|
99
|
+
expect(providers.myupperapi.nickname).toBe('MYUPPERAPI Duck');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should require both API_KEY and BASE_URL', () => {
|
|
103
|
+
// Only API_KEY, missing BASE_URL
|
|
104
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
105
|
+
process.env.CUSTOM_INCOMPLETE1_API_KEY = 'test-key';
|
|
106
|
+
|
|
107
|
+
// Only BASE_URL, missing API_KEY
|
|
108
|
+
process.env.CUSTOM_INCOMPLETE2_BASE_URL = 'https://test.com/v1';
|
|
109
|
+
|
|
110
|
+
const configManager = new ConfigManager();
|
|
111
|
+
const providers = configManager.getAllProviders();
|
|
112
|
+
|
|
113
|
+
// Neither should be created
|
|
114
|
+
expect(providers.incomplete1).toBeUndefined();
|
|
115
|
+
expect(providers.incomplete2).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle comma-separated models list', () => {
|
|
119
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
120
|
+
process.env.CUSTOM_TESTMODELS_API_KEY = 'test-key';
|
|
121
|
+
process.env.CUSTOM_TESTMODELS_BASE_URL = 'https://test.com/v1';
|
|
122
|
+
process.env.CUSTOM_TESTMODELS_MODELS = ' model1 , model2 , model3 ';
|
|
123
|
+
|
|
124
|
+
const configManager = new ConfigManager();
|
|
125
|
+
const providers = configManager.getAllProviders();
|
|
126
|
+
|
|
127
|
+
expect(providers.testmodels.models).toEqual(['model1', 'model2', 'model3']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should use default values for optional fields', () => {
|
|
131
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
132
|
+
process.env.CUSTOM_MINIMAL_API_KEY = 'test-key';
|
|
133
|
+
process.env.CUSTOM_MINIMAL_BASE_URL = 'https://minimal.com/v1';
|
|
134
|
+
|
|
135
|
+
const configManager = new ConfigManager();
|
|
136
|
+
const providers = configManager.getAllProviders();
|
|
137
|
+
|
|
138
|
+
expect(providers.minimal).toBeDefined();
|
|
139
|
+
expect(providers.minimal).toEqual({
|
|
140
|
+
api_key: 'test-key',
|
|
141
|
+
base_url: 'https://minimal.com/v1',
|
|
142
|
+
models: ['custom-model'], // default
|
|
143
|
+
default_model: 'custom-model', // default
|
|
144
|
+
nickname: 'MINIMAL Duck', // auto-generated
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle custom nicknames', () => {
|
|
149
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
150
|
+
process.env.CUSTOM_NICKNAMED_API_KEY = 'test-key';
|
|
151
|
+
process.env.CUSTOM_NICKNAMED_BASE_URL = 'https://test.com/v1';
|
|
152
|
+
process.env.CUSTOM_NICKNAMED_NICKNAME = 'My Special Test Duck 🦆';
|
|
153
|
+
|
|
154
|
+
const configManager = new ConfigManager();
|
|
155
|
+
const providers = configManager.getAllProviders();
|
|
156
|
+
|
|
157
|
+
expect(providers.nicknamed.nickname).toBe('My Special Test Duck 🦆');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle empty models string', () => {
|
|
161
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
162
|
+
process.env.CUSTOM_EMPTYMODELS_API_KEY = 'test-key';
|
|
163
|
+
process.env.CUSTOM_EMPTYMODELS_BASE_URL = 'https://test.com/v1';
|
|
164
|
+
process.env.CUSTOM_EMPTYMODELS_MODELS = '';
|
|
165
|
+
|
|
166
|
+
const configManager = new ConfigManager();
|
|
167
|
+
const providers = configManager.getAllProviders();
|
|
168
|
+
|
|
169
|
+
// Empty models should fall back to default
|
|
170
|
+
expect(providers.emptymodels.models).toEqual(['custom-model']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should integrate with existing providers without conflicts', () => {
|
|
174
|
+
// Set up API keys for built-in providers
|
|
175
|
+
process.env.OPENAI_API_KEY = 'openai-key';
|
|
176
|
+
process.env.GEMINI_API_KEY = 'gemini-key';
|
|
177
|
+
|
|
178
|
+
// Add custom provider
|
|
179
|
+
process.env.CUSTOM_INTEGRATION_API_KEY = 'custom-key';
|
|
180
|
+
process.env.CUSTOM_INTEGRATION_BASE_URL = 'https://custom.com/v1';
|
|
181
|
+
|
|
182
|
+
const configManager = new ConfigManager();
|
|
183
|
+
const providers = configManager.getAllProviders();
|
|
184
|
+
|
|
185
|
+
// Should have built-in providers
|
|
186
|
+
expect(providers.openai).toBeDefined();
|
|
187
|
+
expect(providers.gemini).toBeDefined();
|
|
188
|
+
|
|
189
|
+
// Should have custom provider
|
|
190
|
+
expect(providers.integration).toBeDefined();
|
|
191
|
+
|
|
192
|
+
// Custom provider shouldn't override built-ins
|
|
193
|
+
expect(providers.openai.base_url).toBe('https://api.openai.com/v1');
|
|
194
|
+
expect(providers.integration.base_url).toBe('https://custom.com/v1');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle local LLM configuration via custom providers', () => {
|
|
198
|
+
// Test common local LLM setups
|
|
199
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
200
|
+
process.env.CUSTOM_OLLAMA_API_KEY = 'not-needed';
|
|
201
|
+
process.env.CUSTOM_OLLAMA_BASE_URL = 'http://localhost:11434/v1';
|
|
202
|
+
process.env.CUSTOM_OLLAMA_MODELS = 'llama3.2,mistral,codellama';
|
|
203
|
+
process.env.CUSTOM_OLLAMA_NICKNAME = 'Local Ollama Duck';
|
|
204
|
+
|
|
205
|
+
process.env.CUSTOM_LMSTUDIO_API_KEY = 'not-needed';
|
|
206
|
+
process.env.CUSTOM_LMSTUDIO_BASE_URL = 'http://localhost:1234/v1';
|
|
207
|
+
process.env.CUSTOM_LMSTUDIO_NICKNAME = 'LM Studio Duck';
|
|
208
|
+
|
|
209
|
+
const configManager = new ConfigManager();
|
|
210
|
+
const providers = configManager.getAllProviders();
|
|
211
|
+
|
|
212
|
+
expect(providers.ollama).toBeDefined();
|
|
213
|
+
expect(providers.ollama.api_key).toBe('not-needed');
|
|
214
|
+
expect(providers.ollama.base_url).toBe('http://localhost:11434/v1');
|
|
215
|
+
expect(providers.ollama.models).toEqual(['llama3.2', 'mistral', 'codellama']);
|
|
216
|
+
|
|
217
|
+
expect(providers.lmstudio).toBeDefined();
|
|
218
|
+
expect(providers.lmstudio.api_key).toBe('not-needed');
|
|
219
|
+
expect(providers.lmstudio.base_url).toBe('http://localhost:1234/v1');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('Integration with ProviderManager', () => {
|
|
224
|
+
it('should allow custom providers to be used by ProviderManager', () => {
|
|
225
|
+
process.env.OPENAI_API_KEY = 'dummy-key'; // Ensure getDefaultProviders is called
|
|
226
|
+
process.env.CUSTOM_TESTPROVIDER_API_KEY = 'test-key';
|
|
227
|
+
process.env.CUSTOM_TESTPROVIDER_BASE_URL = 'https://test.com/v1';
|
|
228
|
+
process.env.CUSTOM_TESTPROVIDER_NICKNAME = 'Test Provider Duck';
|
|
229
|
+
|
|
230
|
+
const configManager = new ConfigManager();
|
|
231
|
+
const config = configManager.getConfig();
|
|
232
|
+
|
|
233
|
+
expect(config.providers.testprovider).toBeDefined();
|
|
234
|
+
expect(config.providers.testprovider.nickname).toBe('Test Provider Duck');
|
|
235
|
+
expect(config.providers.testprovider.api_key).toBe('test-key');
|
|
236
|
+
expect(config.providers.testprovider.base_url).toBe('https://test.com/v1');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|