llm-switcher 1.6.6 → 1.6.8
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 +12 -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/tests/ui.test.mjs +35 -0
- package/ui.html +1629 -31
|
@@ -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
|
+
});
|
package/tests/ui.test.mjs
CHANGED
|
@@ -115,3 +115,38 @@ test('the 1M switch is gone from the slots, the card and the payload', () => {
|
|
|
115
115
|
assert.doesNotMatch(functionBody('collectCurrentSlotValues'), /model1M\s*[,}]/);
|
|
116
116
|
assert.doesNotMatch(html, /profilePayload\.model1M/);
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
test('documentation tab is properly integrated with sidebar navigation, sections, and helpers', () => {
|
|
120
|
+
// Sidebar navigation link
|
|
121
|
+
assert.match(html, /<a class="nav-link" href="#\/docs" data-nav="docs"/);
|
|
122
|
+
// View panel exists
|
|
123
|
+
assert.match(html, /<div id="view-docs" class="view-panel"/);
|
|
124
|
+
// Table of Contents navigation exists
|
|
125
|
+
assert.match(html, /<nav class="docs-toc" id="docsToc"/);
|
|
126
|
+
|
|
127
|
+
// All 10 core sections exist
|
|
128
|
+
const expectedSections = [
|
|
129
|
+
'doc-overview',
|
|
130
|
+
'doc-install',
|
|
131
|
+
'doc-cli',
|
|
132
|
+
'doc-blindfold',
|
|
133
|
+
'doc-agy',
|
|
134
|
+
'doc-optimizers',
|
|
135
|
+
'doc-agent-mcp',
|
|
136
|
+
'doc-advanced',
|
|
137
|
+
'doc-config',
|
|
138
|
+
'doc-faq'
|
|
139
|
+
];
|
|
140
|
+
for (const s of expectedSections) {
|
|
141
|
+
assert.match(html, new RegExp(`id="${s}"`), `Missing section ${s} in ui.html`);
|
|
142
|
+
assert.match(html, new RegExp(`href="#${s}"`), `Missing TOC link for ${s} in ui.html`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Hash router handles 'docs'
|
|
146
|
+
assert.match(html, /validRoutes\s*=\s*\[[^\]]*'docs'[^\]]*\]/);
|
|
147
|
+
assert.match(html, /activeRoute === 'docs'/);
|
|
148
|
+
|
|
149
|
+
// Helper functions exist
|
|
150
|
+
assert.match(html, /function copyDocCode\(/);
|
|
151
|
+
assert.match(html, /function initDocsScrollSpy\(/);
|
|
152
|
+
});
|