llm-switcher 1.6.6 → 1.6.7
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/CHANGELOG.md +7 -0
- package/docs/goal-check.md +75 -0
- package/goalcheck-call.mjs +208 -0
- package/goalcheck.mjs +498 -0
- package/package.json +1 -1
- package/preflight-check.mjs +188 -0
- package/proxy.mjs +341 -2
- package/shim.mjs +13 -2
- package/state.mjs +5 -0
- package/tests/goalcheck.e2e.test.mjs +316 -0
- package/tests/goalcheck.test.mjs +312 -0
- package/tests/preflight.test.mjs +119 -0
- package/ui.html +676 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const preflightScript = path.join(root, 'preflight-check.mjs');
|
|
12
|
+
const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
13
|
+
|
|
14
|
+
function runPreflight(tool, dir) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const cp = spawn(process.execPath, [preflightScript, tool, dir], {
|
|
17
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
18
|
+
});
|
|
19
|
+
let stdout = '';
|
|
20
|
+
let stderr = '';
|
|
21
|
+
cp.stdout.on('data', b => stdout += b);
|
|
22
|
+
cp.stderr.on('data', b => stderr += b);
|
|
23
|
+
cp.on('close', status => resolve({ status, stdout, stderr }));
|
|
24
|
+
cp.on('error', reject);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('preflight-check: verifies available models, warns on dropped models and invalid keys', async t => {
|
|
29
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'llmsw-preflight-'));
|
|
30
|
+
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
31
|
+
|
|
32
|
+
let mockStatus = 200;
|
|
33
|
+
let mockModels = [
|
|
34
|
+
{ id: 'zencore/space-bunny-free' },
|
|
35
|
+
{ id: 'zencore/muse-spark-1.3-contributor-free' },
|
|
36
|
+
{ id: 'codex/gpt-6-astra' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const mockUpstream = http.createServer((req, res) => {
|
|
40
|
+
if (mockStatus !== 200) {
|
|
41
|
+
res.statusCode = mockStatus;
|
|
42
|
+
return res.end('{"error":{"message":"Invalid API key"}}');
|
|
43
|
+
}
|
|
44
|
+
res.setHeader('content-type', 'application/json');
|
|
45
|
+
res.end(JSON.stringify({ data: mockModels }));
|
|
46
|
+
});
|
|
47
|
+
await listen(mockUpstream);
|
|
48
|
+
t.after(() => mockUpstream.close());
|
|
49
|
+
|
|
50
|
+
const port = mockUpstream.address().port;
|
|
51
|
+
const config = {
|
|
52
|
+
activeProfiles: { claude: 'mock-prof', codex: 'mock-prof' },
|
|
53
|
+
profiles: {
|
|
54
|
+
'mock-prof': {
|
|
55
|
+
name: 'Mock Provider',
|
|
56
|
+
baseURL: `http://127.0.0.1:${port}/v1`,
|
|
57
|
+
apiKey: 'test-key',
|
|
58
|
+
defaultModels: {
|
|
59
|
+
sonnet: 'zencore/space-bunny-free',
|
|
60
|
+
opus: 'zencore/space-bunny-free',
|
|
61
|
+
haiku: 'zencore/muse-spark-1.3-contributor-free'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
|
|
68
|
+
fs.writeFileSync(path.join(dir, 'route-claude.txt'), 'claude -> mock-prof | 127.0.0.1 | zencore/space-bunny-free\n');
|
|
69
|
+
|
|
70
|
+
// Case 1: All models available -> verified ✓
|
|
71
|
+
const r1 = await runPreflight('claude', dir);
|
|
72
|
+
assert.equal(r1.status, 0);
|
|
73
|
+
assert.match(r1.stdout, /verified ✓/);
|
|
74
|
+
assert.doesNotMatch(r1.stdout, /CẢNH BÁO/);
|
|
75
|
+
|
|
76
|
+
// Case 2: Consecutive call hits cache
|
|
77
|
+
const r2 = await runPreflight('claude', dir);
|
|
78
|
+
assert.equal(r2.status, 0);
|
|
79
|
+
assert.match(r2.stdout, /verified ✓/);
|
|
80
|
+
|
|
81
|
+
// Clear cache for next test cases
|
|
82
|
+
fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
|
|
83
|
+
|
|
84
|
+
// Case 3: One model was dropped/deprecated (e.g. haiku set to dropped-model)
|
|
85
|
+
config.profiles['mock-prof'].defaultModels.haiku = 'zencore/old-dropped-model';
|
|
86
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
|
|
87
|
+
|
|
88
|
+
const r3 = await runPreflight('claude', dir);
|
|
89
|
+
assert.equal(r3.status, 0);
|
|
90
|
+
assert.match(r3.stdout, /CẢNH BÁO/);
|
|
91
|
+
assert.match(r3.stdout, /old-dropped-model/);
|
|
92
|
+
assert.match(r3.stdout, /slot haiku/);
|
|
93
|
+
assert.match(r3.stdout, /switch ui/);
|
|
94
|
+
|
|
95
|
+
fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
|
|
96
|
+
|
|
97
|
+
// Case 4: API key rejected (401)
|
|
98
|
+
mockStatus = 401;
|
|
99
|
+
const r4 = await runPreflight('claude', dir);
|
|
100
|
+
assert.equal(r4.status, 0);
|
|
101
|
+
assert.match(r4.stdout, /API key cho profile 'mock-prof' không hợp lệ/);
|
|
102
|
+
assert.match(r4.stdout, /HTTP 401/);
|
|
103
|
+
|
|
104
|
+
fs.rmSync(path.join(dir, 'preflight-cache.json'), { force: true });
|
|
105
|
+
|
|
106
|
+
// Case 5: Endpoint unreachable (bad port)
|
|
107
|
+
config.profiles['mock-prof'].baseURL = 'http://127.0.0.1:1';
|
|
108
|
+
fs.writeFileSync(path.join(dir, 'config.json'), JSON.stringify(config, null, 2));
|
|
109
|
+
|
|
110
|
+
const r5 = await runPreflight('claude', dir);
|
|
111
|
+
assert.equal(r5.status, 0);
|
|
112
|
+
assert.match(r5.stdout, /Không thể kết nối tới endpoint/);
|
|
113
|
+
|
|
114
|
+
// Case 6: Tool is off (no route file) -> exits quietly
|
|
115
|
+
fs.rmSync(path.join(dir, 'route-claude.txt'), { force: true });
|
|
116
|
+
const r6 = await runPreflight('claude', dir);
|
|
117
|
+
assert.equal(r6.status, 0);
|
|
118
|
+
assert.equal(r6.stdout.trim(), '');
|
|
119
|
+
});
|