llm-switcher 1.1.1 → 1.1.3
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 +16 -1
- package/README.vi.md +16 -1
- package/blindfold/make-certs.sh +2 -1
- package/package.json +2 -2
- package/scripts/run-tests.mjs +19 -0
- package/state.mjs +3 -1
- package/switch.mjs +13 -3
- package/tests/datadir.test.mjs +14 -0
- package/tests/state.test.mjs +27 -1
- package/ui.html +29 -18
package/README.md
CHANGED
|
@@ -191,7 +191,20 @@ flowchart LR
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
-
## Changes in
|
|
194
|
+
## Changes in 1.1.3
|
|
195
|
+
|
|
196
|
+
- **Dashboard.** Opened without its access token, the page no longer waits on "Checking status...". It says that it is locked and names `switch ui`, which opens it with the token.
|
|
197
|
+
- **Dashboard.** The Codex model names (session, review, subagent) are on the **Models** tab, next to the other model settings. The **Blindfold** tab holds only the interceptor settings.
|
|
198
|
+
|
|
199
|
+
### Changes in 1.1.2
|
|
200
|
+
|
|
201
|
+
- **npm package.** Install with `npm install -g llm-switcher` and run `switch`. An npm install keeps its data in `~/.llm-switcher`, so an upgrade does not erase your configuration. A git checkout keeps its data next to the code, as before.
|
|
202
|
+
- **Contract lab.** The gateway can send a small sample of complete exchanges to an [intact](https://github.com/louisphamdev/intact) server, which finds fields that the converter loses. It is off by default. See "Contract lab" below.
|
|
203
|
+
- **macOS.** `blindfold/make-certs.sh` now runs with LibreSSL, the default `openssl` on macOS.
|
|
204
|
+
- **Upgrade from 1.1.0 or older.** A running gateway older than 1.1.1 cannot prove its identity. `switch` now names it and does not stop it. Stop it by hand once, then run `switch on`.
|
|
205
|
+
- **Tests.** `npm test` runs only `tests/**/*.test.mjs`, also on Node.js 18 and 20.
|
|
206
|
+
|
|
207
|
+
### Earlier changes
|
|
195
208
|
|
|
196
209
|
- The desktop dashboard now uses a compact developer-tool layout. It has clearer route controls, keyboard-accessible tabs, labeled model fields, and no decorative emoji.
|
|
197
210
|
- Codex profiles now use three documented roles: `main`, `review`, and `subagent`.
|
|
@@ -222,6 +235,8 @@ cp "$(npm root -g)/llm-switcher/config.example.json" ~/.llm-switcher/config.json
|
|
|
222
235
|
|
|
223
236
|
An npm install keeps `config.json`, `admin.token` and the launch files in `~/.llm-switcher`. An upgrade replaces the package folder only, so your configuration stays.
|
|
224
237
|
|
|
238
|
+
If you upgrade from 1.1.0 or older, stop the running gateway before you run `switch`. An older gateway cannot prove its identity, so `switch` does not stop it for you.
|
|
239
|
+
|
|
225
240
|
**Option B: git clone**
|
|
226
241
|
```bash
|
|
227
242
|
git clone https://github.com/louisphamdev/llm-switcher.git
|
package/README.vi.md
CHANGED
|
@@ -191,7 +191,20 @@ flowchart LR
|
|
|
191
191
|
|
|
192
192
|
---
|
|
193
193
|
|
|
194
|
-
## Thay đổi trong
|
|
194
|
+
## Thay đổi trong bản 1.1.3
|
|
195
|
+
|
|
196
|
+
- **Dashboard.** Khi mở mà thiếu access token, trang không còn đứng ở "Checking status...". Trang báo đang bị khoá và chỉ lệnh `switch ui`, lệnh này mở trang kèm token.
|
|
197
|
+
- **Dashboard.** Tên model của Codex (session, review, subagent) nằm ở tab **Models**, cạnh các cấu hình model khác. Tab **Blindfold** chỉ còn cấu hình interceptor.
|
|
198
|
+
|
|
199
|
+
### Thay đổi trong bản 1.1.2
|
|
200
|
+
|
|
201
|
+
- **Gói npm.** Cài bằng `npm install -g llm-switcher` rồi chạy `switch`. Bản cài bằng npm lưu dữ liệu trong `~/.llm-switcher`, nên nâng cấp không xoá cấu hình. Bản git checkout vẫn lưu dữ liệu cạnh mã nguồn như trước.
|
|
202
|
+
- **Contract lab.** Gateway có thể gửi một phần nhỏ các lượt trao đổi hoàn chỉnh lên server [intact](https://github.com/louisphamdev/intact) để tìm field mà converter làm mất. Mặc định tính năng này tắt. Xem mục "Contract lab" bên dưới.
|
|
203
|
+
- **macOS.** `blindfold/make-certs.sh` giờ chạy được với LibreSSL, là `openssl` mặc định trên macOS.
|
|
204
|
+
- **Nâng cấp từ 1.1.0 trở xuống.** Gateway cũ hơn 1.1.1 không chứng minh được danh tính. `switch` giờ gọi đúng tên nó và không tự dừng nó. Dừng nó bằng tay một lần, rồi chạy `switch on`.
|
|
205
|
+
- **Test.** `npm test` chỉ chạy `tests/**/*.test.mjs`, kể cả trên Node.js 18 và 20.
|
|
206
|
+
|
|
207
|
+
### Các thay đổi trước đó
|
|
195
208
|
|
|
196
209
|
- Dashboard cho máy tính nay có bố cục gọn như một công cụ dành cho lập trình viên. Các điều khiển route rõ hơn, tab dùng được bằng bàn phím, trường model có nhãn đầy đủ và không còn emoji trang trí.
|
|
197
210
|
- Profile Codex dùng ba vai trò theo tài liệu chính thức: `main`, `review` và `subagent`.
|
|
@@ -222,6 +235,8 @@ cp "$(npm root -g)/llm-switcher/config.example.json" ~/.llm-switcher/config.json
|
|
|
222
235
|
|
|
223
236
|
Bản cài bằng npm lưu `config.json`, `admin.token` và các file khởi chạy trong `~/.llm-switcher`. Khi nâng cấp, npm chỉ thay thư mục package, nên cấu hình của bạn vẫn còn.
|
|
224
237
|
|
|
238
|
+
Nếu bạn nâng cấp từ 1.1.0 trở xuống, hãy dừng gateway đang chạy trước khi chạy `switch`. Gateway cũ không chứng minh được danh tính, nên `switch` không tự dừng nó.
|
|
239
|
+
|
|
225
240
|
**Cách B: git clone**
|
|
226
241
|
```bash
|
|
227
242
|
git clone https://github.com/louisphamdev/llm-switcher.git
|
package/blindfold/make-certs.sh
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Build the private CA and the leaf certificate that blindfold.mjs presents.
|
|
3
3
|
#
|
|
4
4
|
# Windows: run this from Git Bash. The openssl that ships with Git for Windows works.
|
|
5
|
+
# macOS: /usr/bin/openssl is LibreSSL. Use only options that LibreSSL also has (it has no `x509 -ext`).
|
|
5
6
|
# The earlier attempt used PowerShell New-SelfSignedCertificate, whose leaf was refused
|
|
6
7
|
# with "unsuitable certificate purpose" because it carried no serverAuth extended key
|
|
7
8
|
# usage. The extension files below are what fix that, so do not drop them.
|
|
@@ -92,4 +93,4 @@ mv -f "$WORK/ca.key" "$WORK/ca.pem" "$WORK/leaf.key" "$WORK/leaf.pem" "$OUT_DIR/
|
|
|
92
93
|
|
|
93
94
|
echo "[blindfold] CA : $OUT_DIR/ca.pem"
|
|
94
95
|
echo "[blindfold] leaf : $OUT_DIR/leaf.pem"
|
|
95
|
-
openssl x509 -in "$OUT_DIR/leaf.pem" -noout -
|
|
96
|
+
openssl x509 -in "$OUT_DIR/leaf.pem" -noout -text | grep -A1 -E "Extended Key Usage|Subject Alternative Name"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-switcher",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code, Codex, OpenAI and Gemini clients",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"start": "node proxy.mjs",
|
|
33
|
-
"test": "node
|
|
33
|
+
"test": "node scripts/run-tests.mjs",
|
|
34
34
|
"test:live": "node tests/live-optimizer-interop.mjs"
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Runs only tests/**/*.test.mjs. A bare `node --test` also collects test files from git-ignored
|
|
2
|
+
// folders (an old copy in temp/), and Node 18 and 20 do not expand a glob argument.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const files = [];
|
|
10
|
+
(function walk(dir) {
|
|
11
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
12
|
+
const p = path.join(dir, e.name);
|
|
13
|
+
if (e.isDirectory()) walk(p);
|
|
14
|
+
else if (e.name.endsWith('.test.mjs')) files.push(path.relative(root, p));
|
|
15
|
+
}
|
|
16
|
+
})(path.join(root, 'tests'));
|
|
17
|
+
|
|
18
|
+
const r = spawnSync(process.execPath, ['--test', ...process.argv.slice(2), ...files], { cwd: root, stdio: 'inherit' });
|
|
19
|
+
process.exit(r.status ?? 1);
|
package/state.mjs
CHANGED
|
@@ -792,13 +792,15 @@ function getJson(port, pathname, timeoutMs = 3000) {
|
|
|
792
792
|
|
|
793
793
|
const newNonce = () => crypto.randomBytes(16).toString('hex');
|
|
794
794
|
|
|
795
|
-
/** 'ours' | 'foreign' | 'silent' | 'free'. Treat 'silent' like 'foreign' in every decision. */
|
|
795
|
+
/** 'ours' | 'legacy' | 'foreign' | 'silent' | 'free'. Treat 'legacy' and 'silent' like 'foreign' in every decision. */
|
|
796
796
|
export async function probeGateway(port) {
|
|
797
797
|
const nonce = newNonce();
|
|
798
798
|
const r = await getJson(port, `/health?challenge=${nonce}`);
|
|
799
799
|
if (r.state !== 'answered') return r.state;
|
|
800
800
|
const b = r.body;
|
|
801
801
|
if (b?.proxy !== 'llm-switcher' || b.port !== port) return 'foreign';
|
|
802
|
+
// Gateways before 1.1.1 answer without a proof: name them, but never trust them enough to stop them.
|
|
803
|
+
if (!('proof' in b)) return 'legacy';
|
|
802
804
|
const proof = identityProof(nonce, { role: 'gateway', port, pid: b.pid });
|
|
803
805
|
return proof && b.proof === proof ? 'ours' : 'foreign';
|
|
804
806
|
}
|
package/switch.mjs
CHANGED
|
@@ -82,9 +82,14 @@ function startProxyBackground(port) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// 'silent' is a listener that never answered: a hung gateway of ours, or another program.
|
|
85
|
-
const isHeld = (state) => state === 'foreign' || state === 'silent';
|
|
85
|
+
const isHeld = (state) => state === 'foreign' || state === 'silent' || state === 'legacy';
|
|
86
86
|
|
|
87
87
|
function refuseForeignPort(port, owner, state = 'foreign') {
|
|
88
|
+
if (state === 'legacy') {
|
|
89
|
+
console.error(`[Error] An llm-switcher gateway older than 1.1.1 runs on port ${port}. Stop it, then run \`switch on\` again.`);
|
|
90
|
+
console.error(' It cannot prove its identity, so this switcher does not stop it. Nothing was changed.');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
88
93
|
console.error(state === 'silent'
|
|
89
94
|
? `[Error] Port ${port} accepts connections but does not answer. A hung ${owner} or another program holds it.`
|
|
90
95
|
: `[Error] Port ${port} is held by another process, not by ${owner}.`);
|
|
@@ -204,7 +209,7 @@ function listeningPids(port) {
|
|
|
204
209
|
return [...pids];
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
// Returns 'stopped', 'not-running', 'not-ours', 'silent' or 'still-running'. The kill targets the process that
|
|
212
|
+
// Returns 'stopped', 'not-running', 'not-ours', 'legacy', 'silent' or 'still-running'. The kill targets the process that
|
|
208
213
|
// listens on the port, right after the identity probe confirmed that listener is this switcher.
|
|
209
214
|
async function stopProxy(port) {
|
|
210
215
|
const state = await probeGateway(port);
|
|
@@ -226,6 +231,7 @@ async function stopProxy(port) {
|
|
|
226
231
|
return 'stopped';
|
|
227
232
|
}
|
|
228
233
|
if (state === 'foreign') return 'not-ours';
|
|
234
|
+
if (state === 'legacy') return 'legacy';
|
|
229
235
|
if (state === 'silent') {
|
|
230
236
|
if (ownUnit) {
|
|
231
237
|
serviceStop(svc);
|
|
@@ -465,6 +471,10 @@ async function turnOff(targetArg) {
|
|
|
465
471
|
console.error(`[Error] The gateway on port ${port} is still running. Stop it by hand; the launcher files are already cleared.`);
|
|
466
472
|
process.exit(1);
|
|
467
473
|
}
|
|
474
|
+
if (result === 'legacy') {
|
|
475
|
+
console.error(`[Error] An llm-switcher gateway older than 1.1.1 runs on port ${port}. Stop it, then run \`switch on\` again.`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
468
478
|
if (result === 'not-ours' || result === 'silent') {
|
|
469
479
|
console.error(result === 'silent'
|
|
470
480
|
? `[Error] Port ${port} accepts connections but does not answer, so it is not proven to be this switcher. It was not stopped.`
|
|
@@ -502,7 +512,7 @@ async function showStatus() {
|
|
|
502
512
|
const flagged = fs.existsSync(paths.activeFlag);
|
|
503
513
|
|
|
504
514
|
console.log('=== LLM Switcher Status ===');
|
|
505
|
-
const held = { foreign: `PORT ${port} HELD BY ANOTHER PROCESS`, silent: `PORT ${port} DOES NOT ANSWER (hung gateway or another program)` };
|
|
515
|
+
const held = { legacy: `OLD GATEWAY (< 1.1.1) ON PORT ${port}: stop it, then run \`switch on\``, foreign: `PORT ${port} HELD BY ANOTHER PROCESS`, silent: `PORT ${port} DOES NOT ANSWER (hung gateway or another program)` };
|
|
506
516
|
console.log(`Proxy Service: ${isRunning ? `RUNNING (port ${port})` : held[gateway] || 'STOPPED'}`);
|
|
507
517
|
console.log(`Web UI: http://127.0.0.1:${port}/ui`);
|
|
508
518
|
console.log(`Launcher Flag: ${flagged ? 'active.flag present' : 'absent (launchers use official endpoints)'}`);
|
package/tests/datadir.test.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
6
7
|
import { resolveDataDir } from '../state.mjs';
|
|
7
8
|
|
|
8
9
|
const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'llm-sw-datadir-'));
|
|
@@ -35,3 +36,16 @@ test('package.json is publishable and exposes the switch command', () => {
|
|
|
35
36
|
assert.equal(pkg.bin?.switch, 'switch.mjs');
|
|
36
37
|
assert.ok(fs.existsSync(new URL('../LICENSE', import.meta.url)), 'LICENSE file exists');
|
|
37
38
|
});
|
|
39
|
+
|
|
40
|
+
// macOS ships LibreSSL as /usr/bin/openssl. It has no `x509 -ext`; the script must not depend on it.
|
|
41
|
+
test('make-certs.sh runs with an openssl that lacks LibreSSL-missing options', { skip: process.platform === 'win32' }, () => {
|
|
42
|
+
const dir = tmp();
|
|
43
|
+
const real = execFileSync('sh', ['-c', 'command -v openssl'], { encoding: 'utf8' }).trim();
|
|
44
|
+
const bin = path.join(dir, 'bin');
|
|
45
|
+
fs.mkdirSync(bin);
|
|
46
|
+
fs.writeFileSync(path.join(bin, 'openssl'), `#!/bin/sh\nfor a in "$@"; do [ "$a" = "-ext" ] && { echo "unknown option -ext" >&2; exit 1; }; done\nexec "${real}" "$@"\n`, { mode: 0o755 });
|
|
47
|
+
const out = path.join(dir, 'certs');
|
|
48
|
+
const script = new URL('../blindfold/make-certs.sh', import.meta.url).pathname;
|
|
49
|
+
execFileSync('bash', [script, 'chatgpt.com', out], { env: { ...process.env, PATH: `${bin}:${process.env.PATH}` }, stdio: 'pipe' });
|
|
50
|
+
for (const f of ['ca.pem', 'leaf.pem', 'leaf.key']) assert.ok(fs.existsSync(path.join(out, f)), f);
|
|
51
|
+
});
|
package/tests/state.test.mjs
CHANGED
|
@@ -514,7 +514,9 @@ test('make-certs.sh builds a CA that can sign only for its host', (t) => {
|
|
|
514
514
|
fs.writeFileSync(path.join(dir, 'evil.ext'), 'subjectAltName = DNS:evil.test\n');
|
|
515
515
|
ossl('x509', '-req', '-in', 'evil.csr', '-CA', path.join(certs, 'ca.pem'), '-CAkey', path.join(certs, 'ca.key'),
|
|
516
516
|
'-CAcreateserial', '-days', '1', '-extfile', 'evil.ext', '-out', 'evil.pem');
|
|
517
|
-
|
|
517
|
+
// LibreSSL prints the verify error on stdout, OpenSSL 3 on stderr.
|
|
518
|
+
assert.throws(() => ossl('verify', '-CAfile', path.join(certs, 'ca.pem'), 'evil.pem'),
|
|
519
|
+
(e) => /permitted subtree violation/.test(`${e.stdout}${e.stderr}`));
|
|
518
520
|
});
|
|
519
521
|
|
|
520
522
|
// ---- Launch state, config cache, logs, probes (audit F28, F31, F42, M5, racer "blocked gateway") ----
|
|
@@ -595,6 +597,30 @@ test('a port that accepts but never answers probes as silent, not as foreign', a
|
|
|
595
597
|
assert.equal(await probeGateway(server.address().port), 'silent');
|
|
596
598
|
});
|
|
597
599
|
|
|
600
|
+
// A gateway from before 1.1.1 answers /health without an identity proof. It is ours in spirit but
|
|
601
|
+
// cannot be proven, so it is never stopped; the CLI names it instead of calling it a foreign process.
|
|
602
|
+
test('a pre-1.1.1 gateway answers without a proof and probes as legacy, not as foreign', async (t) => {
|
|
603
|
+
const http = await import('node:http');
|
|
604
|
+
const server = http.createServer((req, res) => {
|
|
605
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
606
|
+
res.end(JSON.stringify({ status: 'ok', proxy: 'llm-switcher', port: server.address().port, configLoaded: true }));
|
|
607
|
+
});
|
|
608
|
+
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
609
|
+
t.after(() => server.close());
|
|
610
|
+
assert.equal(await probeGateway(server.address().port), 'legacy');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('an llm-switcher answer with a wrong proof is still foreign', async (t) => {
|
|
614
|
+
const http = await import('node:http');
|
|
615
|
+
const server = http.createServer((req, res) => {
|
|
616
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
617
|
+
res.end(JSON.stringify({ status: 'ok', proxy: 'llm-switcher', port: server.address().port, pid: 1, proof: 'forged' }));
|
|
618
|
+
});
|
|
619
|
+
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
|
620
|
+
t.after(() => server.close());
|
|
621
|
+
assert.equal(await probeGateway(server.address().port), 'foreign');
|
|
622
|
+
});
|
|
623
|
+
|
|
598
624
|
// Codex parses an unquoted --config value as TOML first. The Windows shim passes names unquoted, so a
|
|
599
625
|
// name that TOML reads as a number, a boolean or a date would change type (audit N-3 residual).
|
|
600
626
|
test('isSafeModelName refuses names that TOML reads as something other than a string', () => {
|
package/ui.html
CHANGED
|
@@ -940,7 +940,7 @@
|
|
|
940
940
|
<button id="tab-button-general" type="button" class="tab-btn active" role="tab" aria-selected="true" aria-controls="tab-general" tabindex="0" onclick="switchTab('tab-general')">General</button>
|
|
941
941
|
<button id="tab-button-routing" type="button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-routing" tabindex="-1" onclick="switchTab('tab-routing')">Routing</button>
|
|
942
942
|
<button id="tab-button-models" type="button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-models" tabindex="-1" onclick="switchTab('tab-models')">Models</button>
|
|
943
|
-
<button id="tab-button-codex" type="button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-codex" tabindex="-1" onclick="switchTab('tab-codex')" hidden>
|
|
943
|
+
<button id="tab-button-codex" type="button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-codex" tabindex="-1" onclick="switchTab('tab-codex')" hidden>Blindfold</button>
|
|
944
944
|
<button id="tab-button-inspector" type="button" class="tab-btn" role="tab" aria-selected="false" aria-controls="tab-inspector" tabindex="-1" onclick="switchTab('tab-inspector'); loadInspectorLogs();">Request log</button>
|
|
945
945
|
</div>
|
|
946
946
|
|
|
@@ -1037,23 +1037,10 @@
|
|
|
1037
1037
|
<!-- Dynamically populated via renderModelSlots -->
|
|
1038
1038
|
</div>
|
|
1039
1039
|
|
|
1040
|
-
<!--
|
|
1041
|
-
<div
|
|
1042
|
-
<
|
|
1043
|
-
|
|
1044
|
-
<span id="model-count-label" style="font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;">Available Upstream Models</span>
|
|
1045
|
-
<label class="sr-only" for="model-filter">Filter upstream models</label>
|
|
1046
|
-
<input type="search" id="model-filter" placeholder="Filter models" style="padding: 5px 9px; font-size: 11px; width: 200px;" oninput="filterModels()">
|
|
1047
|
-
</div>
|
|
1048
|
-
<div id="model-items" class="model-list-scroll"></div>
|
|
1049
|
-
</div>
|
|
1050
|
-
</div>
|
|
1051
|
-
</div>
|
|
1052
|
-
|
|
1053
|
-
<!-- Tab 4: Live Inspector -->
|
|
1054
|
-
<!-- Tab 4: Codex — only the names this CLI is allowed to observe -->
|
|
1055
|
-
<div id="tab-codex" class="tab-pane" role="tabpanel" aria-labelledby="tab-button-codex" hidden>
|
|
1056
|
-
<div class="warning-callout">
|
|
1040
|
+
<!-- Codex model config lives with the other model config; only the interceptor stays on its own tab. -->
|
|
1041
|
+
<div id="codex-names" style="margin-top: 16px;" hidden>
|
|
1042
|
+
<span style="font-size: 12px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;">Names Codex sees</span>
|
|
1043
|
+
<div class="warning-callout">
|
|
1057
1044
|
<span class="badge badge-convert">NOTE</span>
|
|
1058
1045
|
<div>
|
|
1059
1046
|
<strong>Codex sees only these names.</strong> The slot aliases <code>main</code>, <code>review</code> and
|
|
@@ -1076,7 +1063,24 @@
|
|
|
1076
1063
|
<input type="text" id="p-public-subagent" list="model-options" placeholder="e.g. gpt-5.6-luna">
|
|
1077
1064
|
</div>
|
|
1078
1065
|
</div>
|
|
1066
|
+
</div>
|
|
1079
1067
|
|
|
1068
|
+
<!-- Model Explorer Box -->
|
|
1069
|
+
<div class="form-group full" id="model-browser" style="display: none; margin-top: 14px;">
|
|
1070
|
+
<div class="model-browser">
|
|
1071
|
+
<div class="model-browser-header">
|
|
1072
|
+
<span id="model-count-label" style="font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;">Available Upstream Models</span>
|
|
1073
|
+
<label class="sr-only" for="model-filter">Filter upstream models</label>
|
|
1074
|
+
<input type="search" id="model-filter" placeholder="Filter models" style="padding: 5px 9px; font-size: 11px; width: 200px;" oninput="filterModels()">
|
|
1075
|
+
</div>
|
|
1076
|
+
<div id="model-items" class="model-list-scroll"></div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
|
|
1081
|
+
<!-- Tab 4: Live Inspector -->
|
|
1082
|
+
<!-- Tab 4: Codex — only the names this CLI is allowed to observe -->
|
|
1083
|
+
<div id="tab-codex" class="tab-pane" role="tabpanel" aria-labelledby="tab-button-codex" hidden>
|
|
1080
1084
|
<div class="form-grid" style="border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px;">
|
|
1081
1085
|
<div class="form-group full">
|
|
1082
1086
|
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; text-transform: none;">
|
|
@@ -1382,6 +1386,8 @@
|
|
|
1382
1386
|
const applies = servesCodex(format);
|
|
1383
1387
|
const btn = document.getElementById('tab-button-codex');
|
|
1384
1388
|
if (btn) btn.hidden = !applies;
|
|
1389
|
+
const names = document.getElementById('codex-names');
|
|
1390
|
+
if (names) names.hidden = !applies;
|
|
1385
1391
|
if (!applies && document.getElementById('tab-codex')?.classList.contains('active')) {
|
|
1386
1392
|
switchTab('tab-general');
|
|
1387
1393
|
}
|
|
@@ -1468,6 +1474,11 @@
|
|
|
1468
1474
|
try {
|
|
1469
1475
|
const res = await api('/api/status');
|
|
1470
1476
|
const data = await res.json();
|
|
1477
|
+
if (res.status === 401) {
|
|
1478
|
+
document.getElementById('status-title').textContent = 'Dashboard locked';
|
|
1479
|
+
document.getElementById('status-sub').textContent = 'Run `switch ui` in a terminal. It opens this page with its access token.';
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1471
1482
|
if (!res.ok) {
|
|
1472
1483
|
showToast(data.error || `Gateway error HTTP ${res.status}`, true);
|
|
1473
1484
|
return;
|