llm-switcher 1.2.0 → 1.2.2
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/classifier.mjs +238 -0
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- package/docs/response-matrix.json +1130 -1130
- package/formats.mjs +30 -1
- package/icons/antigravity.png +0 -0
- package/icons/claude.png +0 -0
- package/icons/codex.png +0 -0
- package/icons/deepseek.png +0 -0
- package/icons/gemini.png +0 -0
- package/icons/github.png +0 -0
- package/icons/groq.png +0 -0
- package/icons/intact.svg +1 -0
- package/icons/ollama.png +0 -0
- package/icons/openai.png +0 -0
- package/icons/openrouter.png +0 -0
- package/icons/qwen.png +0 -0
- package/icons/vertex.png +0 -0
- package/package.json +1 -1
- package/proxy.mjs +27 -15
- package/skills/llm-switcher/SKILL.md +93 -93
- package/switch +0 -0
- package/tests/classifier.test.mjs +210 -0
- package/tests/formats.test.mjs +30 -0
- package/tests/helpers.mjs +24 -24
- package/tests/live-optimizer-interop.mjs +205 -205
- package/ui.html +1700 -1623
package/ui.html
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
<
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>LLM Switcher · Edge Gateway</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%237c6cf6'/%3E%3Cstop offset='100%25' stop-color='%235eead4'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='9' fill='url(%23g)'/%3E%3Cpath d='M8 12h16m-4-4 4 4-4 4M24 20H8m4-4-4 4 4 4' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
|
|
8
|
+
<meta name="theme-color" content="#7c6cf6">
|
|
9
9
|
<script>
|
|
10
10
|
try {
|
|
11
|
-
const savedTheme = localStorage.getItem('lsw-theme');
|
|
11
|
+
const savedTheme = localStorage.getItem('intact-theme') || localStorage.getItem('lsw-theme');
|
|
12
12
|
if (savedTheme === 'light' || (!savedTheme && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches)) {
|
|
13
13
|
document.documentElement.dataset.theme = 'light';
|
|
14
14
|
}
|
|
@@ -16,1122 +16,1078 @@
|
|
|
16
16
|
</script>
|
|
17
17
|
<style>
|
|
18
18
|
:root {
|
|
19
|
-
|
|
20
|
-
--bg: #
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
--
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
--
|
|
19
|
+
color-scheme: dark;
|
|
20
|
+
--bg: #0c1017;
|
|
21
|
+
--side: #10151e;
|
|
22
|
+
--card: #141a25;
|
|
23
|
+
--card2: #1b2331;
|
|
24
|
+
--line: #232d3d;
|
|
25
|
+
--fg: #e4e9f1;
|
|
26
|
+
--muted: #95a0b3;
|
|
27
|
+
--faint: #8b94a7;
|
|
28
|
+
--accent: #8b80f9;
|
|
29
|
+
--accent-strong: #6c5ce7;
|
|
30
|
+
--accent-soft: rgba(139, 128, 249, 0.15);
|
|
31
|
+
--accent-fg: #ffffff;
|
|
32
|
+
--mint: #5eead4;
|
|
33
|
+
--ok: #4ade80;
|
|
34
|
+
--ok-soft: rgba(74, 222, 128, 0.12);
|
|
35
|
+
--warn: #fbbf24;
|
|
36
|
+
--warn-soft: rgba(251, 191, 36, 0.1);
|
|
37
|
+
--err: #fb7185;
|
|
38
|
+
--err-soft: rgba(251, 113, 133, 0.12);
|
|
39
|
+
--code: #5eead4;
|
|
40
|
+
--shadow: 0 20px 50px rgba(0, 0, 0, 0.55);
|
|
41
|
+
|
|
42
|
+
/* Compatibility aliases */
|
|
43
|
+
--surface: var(--side);
|
|
44
|
+
--surface-card: var(--card);
|
|
45
|
+
--surface-elevated: var(--card2);
|
|
46
|
+
--surface-well: var(--bg);
|
|
47
|
+
--border: var(--line);
|
|
48
|
+
--border-strong: #334155;
|
|
49
|
+
--border-active: var(--accent);
|
|
50
|
+
--text: var(--fg);
|
|
51
|
+
--text-muted: var(--muted);
|
|
52
|
+
--text-dim: var(--faint);
|
|
53
|
+
--primary-bg: var(--accent-strong);
|
|
54
|
+
--primary-fg: var(--accent-fg);
|
|
55
|
+
--primary-hover: var(--accent);
|
|
56
|
+
--accent-muted: var(--accent-soft);
|
|
57
|
+
--success: var(--ok);
|
|
58
|
+
--success-muted: var(--ok-soft);
|
|
59
|
+
--warning: var(--warn);
|
|
60
|
+
--warning-muted: var(--warn-soft);
|
|
61
|
+
--danger: var(--err);
|
|
62
|
+
--danger-muted: var(--err-soft);
|
|
44
63
|
--radius-sm: 6px;
|
|
45
64
|
--radius: 8px;
|
|
46
|
-
--radius-lg:
|
|
47
|
-
--font-sans:
|
|
48
|
-
--font-mono: ui-monospace,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
--radius-lg: 12px;
|
|
66
|
+
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
67
|
+
--font-mono: ui-monospace, SFMono-Regular, Consolas, monospace;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media (prefers-color-scheme: light) {
|
|
71
|
+
:root:not([data-theme="dark"]) {
|
|
72
|
+
color-scheme: light;
|
|
73
|
+
--bg: #f3f5f9;
|
|
74
|
+
--side: #ffffff;
|
|
75
|
+
--card: #ffffff;
|
|
76
|
+
--card2: #eef1f6;
|
|
77
|
+
--line: #dfe4ec;
|
|
78
|
+
--fg: #141a25;
|
|
79
|
+
--muted: #526076;
|
|
80
|
+
--faint: #5f6b7d;
|
|
81
|
+
--accent: #5b4bdb;
|
|
82
|
+
--accent-strong: #5b4bdb;
|
|
83
|
+
--accent-soft: rgba(91, 75, 219, 0.1);
|
|
84
|
+
--mint: #0f766e;
|
|
85
|
+
--ok: #166534;
|
|
86
|
+
--ok-soft: rgba(22, 101, 52, 0.1);
|
|
87
|
+
--warn: #92400e;
|
|
88
|
+
--warn-soft: rgba(146, 64, 14, 0.08);
|
|
89
|
+
--err: #be123c;
|
|
90
|
+
--err-soft: rgba(190, 18, 60, 0.08);
|
|
91
|
+
--code: #0f766e;
|
|
92
|
+
--shadow: 0 20px 50px rgba(20, 26, 37, 0.14);
|
|
93
|
+
--border-strong: #cbd5e1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:root[data-theme="light"] {
|
|
77
98
|
color-scheme: light;
|
|
99
|
+
--bg: #f3f5f9;
|
|
100
|
+
--side: #ffffff;
|
|
101
|
+
--card: #ffffff;
|
|
102
|
+
--card2: #eef1f6;
|
|
103
|
+
--line: #dfe4ec;
|
|
104
|
+
--fg: #141a25;
|
|
105
|
+
--muted: #526076;
|
|
106
|
+
--faint: #5f6b7d;
|
|
107
|
+
--accent: #5b4bdb;
|
|
108
|
+
--accent-strong: #5b4bdb;
|
|
109
|
+
--accent-soft: rgba(91, 75, 219, 0.1);
|
|
110
|
+
--mint: #0f766e;
|
|
111
|
+
--ok: #166534;
|
|
112
|
+
--ok-soft: rgba(22, 101, 52, 0.1);
|
|
113
|
+
--warn: #92400e;
|
|
114
|
+
--warn-soft: rgba(146, 64, 14, 0.08);
|
|
115
|
+
--err: #be123c;
|
|
116
|
+
--err-soft: rgba(190, 18, 60, 0.08);
|
|
117
|
+
--code: #0f766e;
|
|
118
|
+
--shadow: 0 20px 50px rgba(20, 26, 37, 0.14);
|
|
119
|
+
--border-strong: #cbd5e1;
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
* { box-sizing: border-box; margin: 0; padding: 0;
|
|
122
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
123
|
+
html, body { height: 100%; }
|
|
81
124
|
body {
|
|
125
|
+
font: 14px/1.5 var(--font-sans);
|
|
82
126
|
background: var(--bg);
|
|
83
|
-
color: var(--
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
line-height: 1.5;
|
|
127
|
+
color: var(--fg);
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: 15rem 1fr;
|
|
87
130
|
-webkit-font-smoothing: antialiased;
|
|
88
|
-
text-rendering: optimizeLegibility;
|
|
89
131
|
}
|
|
90
|
-
.container { max-width: 1180px; margin: 0 auto; }
|
|
91
132
|
|
|
92
|
-
/*
|
|
93
|
-
|
|
133
|
+
/* Aside Navigation */
|
|
134
|
+
aside {
|
|
135
|
+
background: var(--side);
|
|
136
|
+
border-right: 1px solid var(--line);
|
|
137
|
+
padding: 1.1rem 0.75rem;
|
|
94
138
|
display: flex;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.logo-badge {
|
|
103
|
-
width: 34px;
|
|
104
|
-
height: 32px;
|
|
105
|
-
border-radius: var(--radius-sm);
|
|
106
|
-
background: var(--surface-elevated);
|
|
107
|
-
border: 1px solid var(--border-strong);
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
justify-content: center;
|
|
111
|
-
color: var(--text);
|
|
112
|
-
font-family: var(--font-mono);
|
|
113
|
-
font-weight: 700;
|
|
114
|
-
font-size: 11px;
|
|
115
|
-
letter-spacing: 0.06em;
|
|
116
|
-
}
|
|
117
|
-
.title-group h1 {
|
|
118
|
-
font-size: 17px;
|
|
119
|
-
font-weight: 600;
|
|
120
|
-
color: var(--text);
|
|
121
|
-
letter-spacing: -0.025em;
|
|
122
|
-
}
|
|
123
|
-
.title-group p {
|
|
124
|
-
font-size: 12px;
|
|
125
|
-
color: var(--text-dim);
|
|
126
|
-
margin-top: 1px;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 0.25rem;
|
|
141
|
+
position: sticky;
|
|
142
|
+
top: 0;
|
|
143
|
+
height: 100vh;
|
|
144
|
+
overflow-y: auto;
|
|
145
|
+
z-index: 10;
|
|
127
146
|
}
|
|
128
|
-
.header-actions { display: flex; gap: 8px; align-items: center; }
|
|
129
147
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
display: inline-flex;
|
|
148
|
+
.brand {
|
|
149
|
+
display: flex;
|
|
133
150
|
align-items: center;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
font-weight: 500;
|
|
139
|
-
border-radius: var(--radius-sm);
|
|
140
|
-
border: 1px solid transparent;
|
|
141
|
-
cursor: pointer;
|
|
142
|
-
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
143
|
-
white-space: nowrap;
|
|
151
|
+
gap: 0.7rem;
|
|
152
|
+
padding: 0.35rem 0.5rem 1.25rem;
|
|
153
|
+
margin-bottom: 0.5rem;
|
|
154
|
+
border-bottom: 1px solid var(--line);
|
|
144
155
|
text-decoration: none;
|
|
156
|
+
color: var(--fg);
|
|
157
|
+
}
|
|
158
|
+
.logo {
|
|
159
|
+
width: 38px;
|
|
160
|
+
height: 38px;
|
|
161
|
+
flex: none;
|
|
162
|
+
filter: drop-shadow(0 4px 14px rgba(124, 108, 246, 0.45));
|
|
163
|
+
}
|
|
164
|
+
.logo svg { width: 100%; height: 100%; display: block; }
|
|
165
|
+
.brand b {
|
|
166
|
+
display: block;
|
|
167
|
+
font-size: 1.25rem;
|
|
168
|
+
font-weight: 800;
|
|
169
|
+
letter-spacing: -0.03em;
|
|
170
|
+
line-height: 1.1;
|
|
171
|
+
color: var(--fg);
|
|
172
|
+
}
|
|
173
|
+
.brand small {
|
|
174
|
+
display: block;
|
|
175
|
+
color: var(--muted);
|
|
176
|
+
font-size: 0.72rem;
|
|
177
|
+
letter-spacing: 0.02em;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
aside nav { display: grid; gap: 0.25rem; }
|
|
181
|
+
aside a.nav-link {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: 0.65rem;
|
|
185
|
+
padding: 0.5rem 0.7rem;
|
|
186
|
+
border-radius: 8px;
|
|
187
|
+
color: var(--muted);
|
|
188
|
+
text-decoration: none;
|
|
189
|
+
font-size: 0.88rem;
|
|
190
|
+
font-weight: 500;
|
|
191
|
+
transition: background 0.15s, color 0.15s;
|
|
145
192
|
}
|
|
146
|
-
.
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.tab-btn:focus-visible,
|
|
152
|
-
.switch input:focus-visible + .slider,
|
|
153
|
-
input:focus-visible,
|
|
154
|
-
select:focus-visible {
|
|
155
|
-
outline: 2px solid var(--accent);
|
|
156
|
-
outline-offset: 2px;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.btn-primary {
|
|
160
|
-
background: var(--primary-bg);
|
|
161
|
-
color: var(--primary-fg);
|
|
162
|
-
border-color: var(--primary-bg);
|
|
163
|
-
}
|
|
164
|
-
.btn-primary:hover:not(:disabled) {
|
|
165
|
-
background: var(--primary-hover);
|
|
166
|
-
border-color: var(--primary-hover);
|
|
193
|
+
aside a.nav-link:hover { color: var(--fg); background: var(--card2); }
|
|
194
|
+
aside a.nav-link[aria-current="page"] {
|
|
195
|
+
background: var(--accent-soft);
|
|
196
|
+
color: var(--accent);
|
|
197
|
+
font-weight: 600;
|
|
167
198
|
}
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
199
|
+
aside a.nav-link svg { width: 18px; height: 18px; flex: none; }
|
|
200
|
+
aside a.nav-link .ct {
|
|
201
|
+
margin-left: auto;
|
|
202
|
+
font-size: 0.72rem;
|
|
203
|
+
color: var(--faint);
|
|
204
|
+
font-variant-numeric: tabular-nums;
|
|
205
|
+
padding: 0.1rem 0.4rem;
|
|
206
|
+
border-radius: 999px;
|
|
207
|
+
background: var(--card2);
|
|
173
208
|
}
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
209
|
+
aside a.nav-link .ct:empty { display: none; }
|
|
210
|
+
aside a.nav-link[aria-current="page"] .ct {
|
|
211
|
+
background: color-mix(in srgb, var(--accent) 20%, transparent);
|
|
212
|
+
color: var(--accent);
|
|
177
213
|
}
|
|
214
|
+
aside .spacer { flex: 1; }
|
|
178
215
|
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
.prefs {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
gap: 0.65rem;
|
|
220
|
+
padding: 16px 14px;
|
|
221
|
+
border-top: 1px solid var(--line);
|
|
222
|
+
margin-top: 0.5rem;
|
|
186
223
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
background: var(--
|
|
190
|
-
|
|
191
|
-
border-
|
|
224
|
+
.seg {
|
|
225
|
+
display: inline-flex;
|
|
226
|
+
background: var(--bg);
|
|
227
|
+
border: 1px solid var(--line);
|
|
228
|
+
border-radius: 8px;
|
|
229
|
+
padding: 4px;
|
|
230
|
+
gap: 3px;
|
|
231
|
+
}
|
|
232
|
+
.seg button {
|
|
233
|
+
font: inherit;
|
|
234
|
+
min-height: 24px;
|
|
235
|
+
padding: 0.15rem 0.6rem;
|
|
236
|
+
font-size: 0.75rem;
|
|
237
|
+
background: transparent;
|
|
238
|
+
color: var(--muted);
|
|
239
|
+
border: 0;
|
|
240
|
+
border-radius: 6px;
|
|
241
|
+
cursor: pointer;
|
|
192
242
|
}
|
|
193
|
-
.
|
|
194
|
-
|
|
243
|
+
.seg button:hover { color: var(--fg); }
|
|
244
|
+
.seg button[aria-pressed="true"] {
|
|
245
|
+
background: var(--accent-soft);
|
|
246
|
+
color: var(--accent);
|
|
247
|
+
font-weight: 600;
|
|
195
248
|
}
|
|
196
249
|
|
|
197
|
-
/*
|
|
198
|
-
.
|
|
250
|
+
/* Page Area */
|
|
251
|
+
.page {
|
|
252
|
+
min-width: 0;
|
|
199
253
|
display: flex;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
border-radius: var(--radius);
|
|
205
|
-
padding: 14px 18px;
|
|
206
|
-
margin-bottom: 24px;
|
|
207
|
-
}
|
|
208
|
-
.status-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
209
|
-
.status-dot-wrapper {
|
|
210
|
-
position: relative;
|
|
211
|
-
width: 12px;
|
|
212
|
-
height: 12px;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
min-height: 100vh;
|
|
256
|
+
}
|
|
257
|
+
.ph {
|
|
213
258
|
display: flex;
|
|
214
259
|
align-items: center;
|
|
215
|
-
justify-content:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
border-radius: 50%;
|
|
222
|
-
background: var(--text-dim);
|
|
223
|
-
box-shadow: 0 0 0 2px rgba(100, 116, 139, 0.2);
|
|
224
|
-
transition: background 0.2s, box-shadow 0.2s;
|
|
225
|
-
}
|
|
226
|
-
.status-dot.active {
|
|
227
|
-
background: var(--success);
|
|
228
|
-
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
|
|
229
|
-
}
|
|
230
|
-
.status-title {
|
|
231
|
-
font-size: 13px;
|
|
232
|
-
font-weight: 600;
|
|
233
|
-
color: var(--text);
|
|
234
|
-
letter-spacing: -0.01em;
|
|
260
|
+
justify-content: space-between;
|
|
261
|
+
gap: 0.75rem;
|
|
262
|
+
padding: 1.1rem 2rem;
|
|
263
|
+
border-bottom: 1px solid var(--line);
|
|
264
|
+
min-height: 4.5rem;
|
|
265
|
+
flex-wrap: wrap;
|
|
235
266
|
}
|
|
236
|
-
.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
267
|
+
.title-box h1 { font-size: 1.25rem; margin: 0; font-weight: 650; }
|
|
268
|
+
.title-box p { margin: 0.15rem 0 0; color: var(--muted); font-size: 0.85rem; }
|
|
269
|
+
.ph-actions { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
|
270
|
+
|
|
271
|
+
.content {
|
|
272
|
+
padding: 1.5rem 2rem 3rem;
|
|
273
|
+
width: 100%;
|
|
274
|
+
display: grid;
|
|
275
|
+
gap: 1.5rem;
|
|
276
|
+
align-content: start;
|
|
242
277
|
}
|
|
278
|
+
.content > * { min-width: 0; }
|
|
243
279
|
|
|
244
|
-
/*
|
|
245
|
-
.
|
|
280
|
+
/* Cards & Grids */
|
|
281
|
+
.sec { display: grid; gap: 0.85rem; }
|
|
282
|
+
.sech { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
|
|
283
|
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; }
|
|
284
|
+
.card.pad { padding: 1.1rem 1.25rem; }
|
|
285
|
+
.cardh {
|
|
246
286
|
display: flex;
|
|
247
|
-
justify-content: space-between;
|
|
248
287
|
align-items: center;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
font-weight: 600;
|
|
254
|
-
letter-spacing: 0.05em;
|
|
255
|
-
color: var(--text-muted);
|
|
256
|
-
text-transform: uppercase;
|
|
257
|
-
}
|
|
258
|
-
.section-subtitle {
|
|
259
|
-
font-size: 11px;
|
|
260
|
-
color: var(--text-dim);
|
|
261
|
-
font-weight: 400;
|
|
262
|
-
text-transform: none;
|
|
263
|
-
letter-spacing: 0;
|
|
288
|
+
gap: 0.75rem;
|
|
289
|
+
padding: 0.9rem 1.25rem;
|
|
290
|
+
border-bottom: 1px solid var(--line);
|
|
291
|
+
flex-wrap: wrap;
|
|
264
292
|
}
|
|
293
|
+
.cardh .grow { min-width: min(12rem, 100%); }
|
|
294
|
+
.grow { flex: 1; }
|
|
295
|
+
.muted { color: var(--muted); }
|
|
296
|
+
.faint { color: var(--faint); }
|
|
297
|
+
.small { font-size: 0.8rem; }
|
|
298
|
+
code, .mono { font-family: var(--font-mono); font-size: 0.82rem; }
|
|
299
|
+
code { color: var(--code); }
|
|
265
300
|
|
|
266
|
-
/*
|
|
267
|
-
.
|
|
301
|
+
/* Provider Grid (Intact Style) */
|
|
302
|
+
.pgrid {
|
|
268
303
|
display: grid;
|
|
269
|
-
grid-template-columns: repeat(auto-
|
|
270
|
-
gap:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.cli-slot-card {
|
|
274
|
-
background: var(--surface-card);
|
|
275
|
-
border: 1px solid var(--border);
|
|
276
|
-
border-radius: var(--radius);
|
|
277
|
-
padding: 12px 14px;
|
|
304
|
+
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
|
305
|
+
gap: 0.85rem;
|
|
306
|
+
}
|
|
307
|
+
.pcard {
|
|
278
308
|
display: flex;
|
|
279
309
|
flex-direction: column;
|
|
280
|
-
gap:
|
|
310
|
+
gap: 0.75rem;
|
|
311
|
+
padding: 1rem 1.15rem;
|
|
312
|
+
background: var(--card);
|
|
313
|
+
border: 1px solid var(--line);
|
|
314
|
+
border-radius: 11px;
|
|
315
|
+
color: inherit;
|
|
316
|
+
min-width: 0;
|
|
317
|
+
cursor: pointer;
|
|
281
318
|
transition: border-color 0.15s, background 0.15s;
|
|
282
319
|
}
|
|
283
|
-
.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
.cli-slot-header {
|
|
320
|
+
.pcard:hover { border-color: var(--accent); }
|
|
321
|
+
.pcard.active { border-color: color-mix(in srgb, var(--ok) 50%, transparent); }
|
|
322
|
+
.pcard .top {
|
|
288
323
|
display: flex;
|
|
289
|
-
justify-content: space-between;
|
|
290
|
-
align-items: center;
|
|
291
|
-
}
|
|
292
|
-
.cli-slot-title-group {
|
|
293
|
-
display: flex;
|
|
294
|
-
align-items: center;
|
|
295
|
-
gap: 8px;
|
|
296
|
-
}
|
|
297
|
-
.cli-slot-icon {
|
|
298
|
-
display: inline-flex;
|
|
299
324
|
align-items: center;
|
|
300
|
-
|
|
301
|
-
width:
|
|
302
|
-
height: 24px;
|
|
303
|
-
border: 1px solid var(--border-strong);
|
|
304
|
-
border-radius: 5px;
|
|
305
|
-
color: var(--text-muted);
|
|
306
|
-
background: var(--surface-well);
|
|
307
|
-
font-family: var(--font-mono);
|
|
308
|
-
font-size: 9px;
|
|
309
|
-
font-weight: 700;
|
|
310
|
-
letter-spacing: 0.04em;
|
|
325
|
+
gap: 0.75rem;
|
|
326
|
+
min-width: 0;
|
|
311
327
|
}
|
|
312
|
-
.
|
|
313
|
-
font-size: 12px;
|
|
328
|
+
.pcard .nm {
|
|
314
329
|
font-weight: 600;
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
.cli-slot-route {
|
|
318
|
-
font-size: 10px;
|
|
319
|
-
color: var(--text-dim);
|
|
320
|
-
font-family: var(--font-mono);
|
|
321
|
-
}
|
|
322
|
-
.cli-slot-select {
|
|
323
|
-
background: var(--surface-well);
|
|
324
|
-
border: 1px solid var(--border);
|
|
325
|
-
border-radius: var(--radius-sm);
|
|
326
|
-
padding: 7px 10px;
|
|
327
|
-
color: var(--text);
|
|
328
|
-
font-size: 12px;
|
|
329
|
-
width: 100%;
|
|
330
|
-
outline: none;
|
|
331
|
-
cursor: pointer;
|
|
332
|
-
transition: border-color 0.15s;
|
|
333
|
-
}
|
|
334
|
-
.cli-slot-select:focus {
|
|
335
|
-
border-color: var(--border-active);
|
|
336
|
-
}
|
|
337
|
-
.sr-only {
|
|
338
|
-
position: absolute;
|
|
339
|
-
width: 1px;
|
|
340
|
-
height: 1px;
|
|
341
|
-
padding: 0;
|
|
342
|
-
margin: -1px;
|
|
330
|
+
font-size: 0.95rem;
|
|
343
331
|
overflow: hidden;
|
|
344
|
-
|
|
332
|
+
text-overflow: ellipsis;
|
|
345
333
|
white-space: nowrap;
|
|
346
|
-
border: 0;
|
|
347
334
|
}
|
|
348
335
|
|
|
349
|
-
|
|
350
|
-
|
|
336
|
+
.av {
|
|
337
|
+
width: 36px;
|
|
338
|
+
height: 36px;
|
|
339
|
+
border-radius: 9px;
|
|
351
340
|
display: grid;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
box-shadow: 0 0 0 1px var(--border-active);
|
|
371
|
-
}
|
|
372
|
-
.profile-card.active {
|
|
373
|
-
border-left: 3px solid var(--success);
|
|
341
|
+
place-items: center;
|
|
342
|
+
font-weight: 700;
|
|
343
|
+
font-size: 0.85rem;
|
|
344
|
+
color: #ffffff;
|
|
345
|
+
flex: none;
|
|
346
|
+
}
|
|
347
|
+
.av.sm { width: 24px; height: 24px; border-radius: 6px; font-size: 0.65rem; }
|
|
348
|
+
.av.logo {
|
|
349
|
+
background: #ffffff;
|
|
350
|
+
padding: 5px;
|
|
351
|
+
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
|
352
|
+
}
|
|
353
|
+
.av.logo.sm { padding: 3px; }
|
|
354
|
+
.av.logo img {
|
|
355
|
+
width: 100%;
|
|
356
|
+
height: 100%;
|
|
357
|
+
object-fit: contain;
|
|
358
|
+
display: block;
|
|
374
359
|
}
|
|
375
360
|
|
|
376
|
-
.
|
|
377
|
-
display: flex;
|
|
378
|
-
justify-content: space-between;
|
|
379
|
-
align-items: center;
|
|
380
|
-
gap: 8px;
|
|
381
|
-
margin-bottom: 6px;
|
|
382
|
-
}
|
|
383
|
-
.card-name-group {
|
|
384
|
-
display: flex;
|
|
361
|
+
.status {
|
|
362
|
+
display: inline-flex;
|
|
385
363
|
align-items: center;
|
|
386
|
-
gap:
|
|
387
|
-
|
|
388
|
-
|
|
364
|
+
gap: 0.35rem;
|
|
365
|
+
font-size: 0.74rem;
|
|
366
|
+
color: var(--faint);
|
|
389
367
|
}
|
|
390
|
-
.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
368
|
+
.status.on { color: var(--ok); font-weight: 500; }
|
|
369
|
+
.status.on::before {
|
|
370
|
+
content: "";
|
|
371
|
+
width: 6px;
|
|
372
|
+
height: 6px;
|
|
373
|
+
border-radius: 50%;
|
|
374
|
+
background: currentColor;
|
|
394
375
|
}
|
|
395
376
|
|
|
396
|
-
/* Badges */
|
|
397
377
|
.badge {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
border-radius:
|
|
378
|
+
display: inline-flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
gap: 0.25rem;
|
|
381
|
+
font-size: 0.72rem;
|
|
382
|
+
font-weight: 500;
|
|
383
|
+
border-radius: 6px;
|
|
384
|
+
padding: 0.1rem 0.45rem;
|
|
385
|
+
background: var(--card2);
|
|
386
|
+
color: var(--muted);
|
|
404
387
|
white-space: nowrap;
|
|
405
388
|
border: 1px solid transparent;
|
|
406
389
|
}
|
|
407
|
-
.badge-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
color: var(--success);
|
|
415
|
-
border-color: rgba(16, 185, 129, 0.25);
|
|
416
|
-
}
|
|
417
|
-
.badge-convert {
|
|
418
|
-
background: var(--warning-muted);
|
|
419
|
-
color: var(--warning);
|
|
420
|
-
border-color: rgba(245, 158, 11, 0.25);
|
|
421
|
-
}
|
|
422
|
-
.badge-outline {
|
|
423
|
-
background: transparent;
|
|
424
|
-
color: var(--text-dim);
|
|
425
|
-
border-color: var(--border);
|
|
426
|
-
}
|
|
427
|
-
.badge-route {
|
|
428
|
-
background: var(--surface-well);
|
|
429
|
-
color: var(--text-muted);
|
|
430
|
-
border-color: var(--border);
|
|
431
|
-
font-family: var(--font-mono);
|
|
432
|
-
font-size: 10px;
|
|
433
|
-
}
|
|
434
|
-
.badge-1m {
|
|
435
|
-
background: var(--success-muted);
|
|
436
|
-
color: var(--success);
|
|
437
|
-
border: 1px solid rgba(16, 185, 129, 0.25);
|
|
438
|
-
font-size: 9px;
|
|
439
|
-
font-weight: 600;
|
|
440
|
-
padding: 1px 4px;
|
|
441
|
-
border-radius: 3px;
|
|
442
|
-
margin-left: 4px;
|
|
443
|
-
}
|
|
444
|
-
.badge-1m-warn {
|
|
445
|
-
background: var(--warning-muted);
|
|
446
|
-
color: var(--warning);
|
|
447
|
-
border: 1px solid rgba(245, 158, 11, 0.3);
|
|
448
|
-
font-size: 9px;
|
|
449
|
-
font-weight: 600;
|
|
450
|
-
padding: 1px 5px;
|
|
451
|
-
border-radius: 4px;
|
|
452
|
-
margin-left: 4px;
|
|
453
|
-
white-space: nowrap;
|
|
454
|
-
cursor: help;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
.warning-callout {
|
|
458
|
-
background: var(--warning-muted);
|
|
459
|
-
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
460
|
-
color: var(--warning);
|
|
461
|
-
padding: 10px 12px;
|
|
462
|
-
border-radius: var(--radius-sm);
|
|
463
|
-
font-size: 12px;
|
|
464
|
-
margin-bottom: 14px;
|
|
465
|
-
line-height: 1.5;
|
|
390
|
+
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
391
|
+
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
392
|
+
.badge.accent { background: var(--accent-soft); color: var(--accent); }
|
|
393
|
+
.badge.err { background: var(--err-soft); color: var(--err); }
|
|
394
|
+
.badge.outline { border-color: var(--line); }
|
|
395
|
+
|
|
396
|
+
.row {
|
|
466
397
|
display: flex;
|
|
467
|
-
|
|
468
|
-
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 0.85rem;
|
|
400
|
+
padding: 0.85rem 1.25rem;
|
|
401
|
+
border-bottom: 1px solid var(--line);
|
|
469
402
|
}
|
|
403
|
+
.row:last-child { border-bottom: 0; }
|
|
404
|
+
.row .who { flex: 1; min-width: 0; }
|
|
405
|
+
.row .who .l { font-weight: 600; font-size: 0.92rem; }
|
|
406
|
+
.row .meta { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin-top: 0.2rem; color: var(--muted); font-size: 0.8rem; }
|
|
470
407
|
|
|
471
|
-
.
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
.card-models strong { color: var(--text); font-weight: 500; }
|
|
492
|
-
.card-footer {
|
|
408
|
+
.chips { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
|
|
409
|
+
.chip {
|
|
410
|
+
display: grid;
|
|
411
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
412
|
+
align-items: center;
|
|
413
|
+
gap: 0.1rem 0.5rem;
|
|
414
|
+
min-width: 0;
|
|
415
|
+
background: var(--card2);
|
|
416
|
+
border: 1px solid var(--line);
|
|
417
|
+
border-radius: 6px;
|
|
418
|
+
padding: 0.35rem 0.55rem;
|
|
419
|
+
font-size: 0.75rem;
|
|
420
|
+
color: var(--fg);
|
|
421
|
+
}
|
|
422
|
+
.chip .lbl { grid-column: 1 / -1; font-size: 0.7rem; overflow-wrap: anywhere; }
|
|
423
|
+
.chip .val { font-family: var(--font-mono); overflow-wrap: anywhere; }
|
|
424
|
+
.chip .b1m { font-weight: 600; font-size: 0.7rem; }
|
|
425
|
+
|
|
426
|
+
.copyline {
|
|
493
427
|
display: flex;
|
|
494
|
-
justify-content: space-between;
|
|
495
428
|
align-items: center;
|
|
496
|
-
|
|
497
|
-
|
|
429
|
+
gap: 0.5rem;
|
|
430
|
+
background: var(--bg);
|
|
431
|
+
border: 1px solid var(--line);
|
|
432
|
+
border-radius: 8px;
|
|
433
|
+
padding: 0.35rem 0.5rem 0.35rem 0.75rem;
|
|
434
|
+
min-width: 0;
|
|
498
435
|
}
|
|
436
|
+
.copyline code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; }
|
|
499
437
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
width: 32px;
|
|
505
|
-
height: 18px;
|
|
506
|
-
flex-shrink: 0;
|
|
507
|
-
}
|
|
508
|
-
.switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
|
509
|
-
.slider {
|
|
510
|
-
position: absolute;
|
|
511
|
-
cursor: pointer;
|
|
512
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
513
|
-
background-color: var(--surface-elevated);
|
|
514
|
-
border: 1px solid var(--border-strong);
|
|
515
|
-
transition: 0.2s;
|
|
516
|
-
border-radius: 18px;
|
|
517
|
-
}
|
|
518
|
-
.slider:before {
|
|
519
|
-
position: absolute;
|
|
520
|
-
content: "";
|
|
521
|
-
height: 12px;
|
|
522
|
-
width: 12px;
|
|
523
|
-
left: 2px;
|
|
524
|
-
bottom: 2px;
|
|
525
|
-
background-color: var(--text-dim);
|
|
526
|
-
transition: 0.2s;
|
|
527
|
-
border-radius: 50%;
|
|
528
|
-
}
|
|
529
|
-
input:checked + .slider {
|
|
530
|
-
background-color: var(--success);
|
|
531
|
-
border-color: var(--success);
|
|
532
|
-
}
|
|
533
|
-
input:checked + .slider:before {
|
|
534
|
-
transform: translateX(14px);
|
|
535
|
-
background-color: #ffffff;
|
|
438
|
+
.stats {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
|
441
|
+
gap: 0.85rem;
|
|
536
442
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
border-radius: var(--radius-lg);
|
|
543
|
-
padding: 20px;
|
|
544
|
-
margin-bottom: 30px;
|
|
443
|
+
.stat {
|
|
444
|
+
background: var(--card);
|
|
445
|
+
border: 1px solid var(--line);
|
|
446
|
+
border-radius: 12px;
|
|
447
|
+
padding: 1rem 1.15rem;
|
|
545
448
|
}
|
|
449
|
+
.stat .k { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
450
|
+
.stat .v { font-size: 1.5rem; font-weight: 700; margin-top: 0.2rem; }
|
|
546
451
|
|
|
547
|
-
/*
|
|
548
|
-
.
|
|
452
|
+
/* Buttons */
|
|
453
|
+
button, .btn {
|
|
454
|
+
font: inherit;
|
|
455
|
+
min-height: 34px;
|
|
456
|
+
padding: 0.35rem 0.85rem;
|
|
457
|
+
border: 0;
|
|
458
|
+
border-radius: 8px;
|
|
459
|
+
cursor: pointer;
|
|
549
460
|
display: inline-flex;
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
padding: 3px;
|
|
556
|
-
flex-wrap: wrap;
|
|
557
|
-
}
|
|
558
|
-
.tab-btn {
|
|
559
|
-
background: transparent;
|
|
560
|
-
border: none;
|
|
561
|
-
color: var(--text-muted);
|
|
562
|
-
padding: 6px 12px;
|
|
563
|
-
font-size: 12px;
|
|
461
|
+
align-items: center;
|
|
462
|
+
gap: 0.4rem;
|
|
463
|
+
justify-content: center;
|
|
464
|
+
text-decoration: none;
|
|
465
|
+
font-size: 0.84rem;
|
|
564
466
|
font-weight: 500;
|
|
565
|
-
border-
|
|
566
|
-
cursor: pointer;
|
|
567
|
-
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
467
|
+
transition: filter 0.15s, background 0.15s, color 0.15s, border-color 0.15s;
|
|
568
468
|
}
|
|
569
|
-
.
|
|
570
|
-
|
|
571
|
-
color: var(--
|
|
572
|
-
background: var(--surface-elevated);
|
|
469
|
+
button.primary, .btn.btn-primary {
|
|
470
|
+
background: var(--accent-strong);
|
|
471
|
+
color: var(--accent-fg);
|
|
573
472
|
font-weight: 600;
|
|
574
|
-
border: 1px solid var(--border-strong);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
.tab-pane { display: none; }
|
|
578
|
-
.tab-pane.active { display: block; }
|
|
579
|
-
|
|
580
|
-
.form-grid {
|
|
581
|
-
display: grid;
|
|
582
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
583
|
-
gap: 14px;
|
|
584
473
|
}
|
|
585
|
-
.
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
.
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
background: var(--
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
474
|
+
button.primary:hover, .btn.btn-primary:hover { filter: brightness(1.1); }
|
|
475
|
+
button.ghost, .btn.btn-outline {
|
|
476
|
+
background: transparent;
|
|
477
|
+
color: var(--fg);
|
|
478
|
+
border: 1px solid var(--line);
|
|
479
|
+
}
|
|
480
|
+
button.ghost:hover, .btn.btn-outline:hover {
|
|
481
|
+
background: var(--card2);
|
|
482
|
+
border-color: var(--faint);
|
|
483
|
+
}
|
|
484
|
+
button.plain { background: transparent; color: var(--muted); }
|
|
485
|
+
button.plain:hover { color: var(--fg); background: var(--card2); }
|
|
486
|
+
button.danger, .btn.btn-danger {
|
|
487
|
+
background: var(--err-soft);
|
|
488
|
+
color: var(--err);
|
|
489
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, transparent);
|
|
490
|
+
}
|
|
491
|
+
button.danger:hover, .btn.btn-danger:hover {
|
|
492
|
+
background: var(--err);
|
|
493
|
+
color: #ffffff;
|
|
494
|
+
}
|
|
495
|
+
button.icon { min-height: 28px; min-width: 28px; padding: 0.2rem; background: transparent; color: var(--muted); border-radius: 6px; }
|
|
496
|
+
button.icon:hover { color: var(--fg); background: var(--card2); }
|
|
497
|
+
button:disabled, .btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
|
|
498
|
+
.btn-sm { min-height: 26px; padding: 0.2rem 0.6rem; font-size: 0.78rem; border-radius: 6px; }
|
|
499
|
+
|
|
500
|
+
/* Inputs */
|
|
501
|
+
input, select, textarea {
|
|
502
|
+
font: inherit;
|
|
503
|
+
padding: 0.45rem 0.7rem;
|
|
504
|
+
border: 1px solid var(--line);
|
|
505
|
+
border-radius: 8px;
|
|
506
|
+
background: var(--bg);
|
|
507
|
+
color: var(--fg);
|
|
508
|
+
min-height: 36px;
|
|
611
509
|
width: 100%;
|
|
612
|
-
|
|
613
|
-
min-width: 0;
|
|
510
|
+
font-size: 0.85rem;
|
|
614
511
|
}
|
|
615
|
-
input:focus, select:focus {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
.
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
512
|
+
input:focus, select:focus, textarea:focus, button:focus-visible, a:focus-visible {
|
|
513
|
+
outline: none;
|
|
514
|
+
border-color: var(--accent);
|
|
515
|
+
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
516
|
+
}
|
|
517
|
+
label.f { display: grid; gap: 0.35rem; font-size: 0.82rem; font-weight: 500; }
|
|
518
|
+
.hint { font-size: 0.75rem; color: var(--faint); font-weight: 400; }
|
|
519
|
+
.search { position: relative; max-width: 18rem; width: 100%; }
|
|
520
|
+
.search svg { position: absolute; left: 0.65rem; top: 50%; transform: translateY(-50%); color: var(--faint); width: 15px; height: 15px; pointer-events: none; }
|
|
521
|
+
.search input { padding-left: 2rem; min-height: 32px; }
|
|
522
|
+
|
|
523
|
+
/* Switch */
|
|
524
|
+
.switch { position: relative; width: 36px; height: 20px; flex: none; display: inline-block; }
|
|
525
|
+
.switch input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; min-height: 0; }
|
|
526
|
+
.switch span { position: absolute; inset: 0; border-radius: 999px; background: var(--line); transition: background 0.15s; pointer-events: none; }
|
|
527
|
+
.switch span::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #ffffff; transition: transform 0.15s; }
|
|
528
|
+
.switch input:checked + span { background: var(--accent-strong); }
|
|
529
|
+
.switch input:checked + span::after { transform: translateX(16px); }
|
|
530
|
+
|
|
531
|
+
/* Modal */
|
|
532
|
+
.overlay {
|
|
533
|
+
position: fixed;
|
|
534
|
+
inset: 0;
|
|
535
|
+
background: rgba(0, 0, 0, 0.65);
|
|
536
|
+
backdrop-filter: blur(3px);
|
|
537
|
+
display: grid;
|
|
538
|
+
place-items: center;
|
|
539
|
+
padding: 1rem;
|
|
540
|
+
z-index: 50;
|
|
541
|
+
}
|
|
542
|
+
.modal {
|
|
543
|
+
background: var(--card);
|
|
544
|
+
border: 1px solid var(--line);
|
|
545
|
+
border-radius: 14px;
|
|
546
|
+
box-shadow: var(--shadow);
|
|
547
|
+
width: min(38rem, 100%);
|
|
548
|
+
max-height: 90vh;
|
|
640
549
|
display: flex;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
margin-top: 20px;
|
|
644
|
-
padding-top: 16px;
|
|
645
|
-
border-top: 1px solid var(--border);
|
|
646
|
-
gap: 12px;
|
|
647
|
-
flex-wrap: wrap;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/* Test Results Card */
|
|
651
|
-
.test-result-box {
|
|
652
|
-
margin-top: 14px;
|
|
653
|
-
border-radius: var(--radius-sm);
|
|
654
|
-
padding: 12px 14px;
|
|
655
|
-
font-size: 12px;
|
|
656
|
-
display: none;
|
|
657
|
-
border: 1px solid transparent;
|
|
658
|
-
}
|
|
659
|
-
.test-result-box.success {
|
|
660
|
-
background: var(--success-muted);
|
|
661
|
-
border-color: rgba(16, 185, 129, 0.25);
|
|
662
|
-
color: var(--success);
|
|
663
|
-
}
|
|
664
|
-
.test-result-box.error {
|
|
665
|
-
background: var(--danger-muted);
|
|
666
|
-
border-color: rgba(239, 68, 68, 0.25);
|
|
667
|
-
color: var(--danger);
|
|
668
|
-
}
|
|
669
|
-
.test-meta { display: flex; gap: 12px; font-weight: 600; margin-bottom: 6px; font-family: var(--font-mono); }
|
|
670
|
-
.test-sample {
|
|
671
|
-
background: var(--surface-well);
|
|
672
|
-
border: 1px solid var(--border);
|
|
673
|
-
padding: 8px 10px;
|
|
674
|
-
border-radius: 4px;
|
|
675
|
-
font-family: var(--font-mono);
|
|
676
|
-
font-size: 11px;
|
|
677
|
-
white-space: pre-wrap;
|
|
678
|
-
word-break: break-all;
|
|
679
|
-
max-height: 120px;
|
|
680
|
-
overflow-y: auto;
|
|
681
|
-
color: var(--text-muted);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/* Model Explorer */
|
|
685
|
-
.model-browser {
|
|
686
|
-
margin-top: 10px;
|
|
687
|
-
background: var(--surface-well);
|
|
688
|
-
border: 1px solid var(--border);
|
|
689
|
-
border-radius: var(--radius-sm);
|
|
690
|
-
padding: 12px;
|
|
550
|
+
flex-direction: column;
|
|
551
|
+
overflow: hidden;
|
|
691
552
|
}
|
|
692
|
-
.
|
|
553
|
+
.mh {
|
|
693
554
|
display: flex;
|
|
694
555
|
justify-content: space-between;
|
|
695
556
|
align-items: center;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
flex-wrap: wrap;
|
|
557
|
+
padding: 1rem 1.25rem;
|
|
558
|
+
border-bottom: 1px solid var(--line);
|
|
699
559
|
}
|
|
700
|
-
.
|
|
701
|
-
|
|
560
|
+
.mh strong { font-size: 1.05rem; }
|
|
561
|
+
.mb {
|
|
562
|
+
padding: 1.15rem 1.25rem;
|
|
702
563
|
overflow-y: auto;
|
|
703
564
|
display: flex;
|
|
704
565
|
flex-direction: column;
|
|
705
|
-
gap:
|
|
706
|
-
padding-right: 4px;
|
|
566
|
+
gap: 1rem;
|
|
707
567
|
}
|
|
708
|
-
.
|
|
568
|
+
.mf {
|
|
709
569
|
display: flex;
|
|
710
570
|
justify-content: space-between;
|
|
711
571
|
align-items: center;
|
|
712
|
-
padding:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
572
|
+
padding: 0.85rem 1.25rem;
|
|
573
|
+
border-top: 1px solid var(--line);
|
|
574
|
+
background: var(--card2);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Tabs (Intact style) */
|
|
578
|
+
.tabs {
|
|
579
|
+
display: flex;
|
|
580
|
+
gap: 0.25rem;
|
|
581
|
+
box-shadow: inset 0 -1px 0 var(--line);
|
|
582
|
+
overflow-x: auto;
|
|
583
|
+
overflow-y: hidden;
|
|
584
|
+
scrollbar-width: none;
|
|
585
|
+
}
|
|
586
|
+
.tabs::-webkit-scrollbar { display: none; }
|
|
587
|
+
.tabs button.tab-btn {
|
|
588
|
+
padding: 0.55rem 0.95rem;
|
|
589
|
+
color: var(--muted);
|
|
590
|
+
background: transparent;
|
|
591
|
+
border: 0;
|
|
592
|
+
border-bottom: 2px solid transparent;
|
|
593
|
+
white-space: nowrap;
|
|
594
|
+
font-weight: 500;
|
|
595
|
+
font-size: 0.85rem;
|
|
596
|
+
cursor: pointer;
|
|
597
|
+
border-radius: 0;
|
|
598
|
+
min-height: 0;
|
|
718
599
|
}
|
|
719
|
-
.
|
|
720
|
-
|
|
721
|
-
|
|
600
|
+
.tabs button.tab-btn:hover { color: var(--fg); }
|
|
601
|
+
.tabs button.tab-btn.active {
|
|
602
|
+
color: var(--fg);
|
|
603
|
+
border-bottom-color: var(--accent);
|
|
604
|
+
font-weight: 600;
|
|
722
605
|
}
|
|
723
|
-
.model-id { font-family: var(--font-mono); color: var(--text); font-size: 11px; word-break: break-all; }
|
|
724
|
-
.model-btn-group { display: flex; gap: 4px; flex-shrink: 0; }
|
|
725
606
|
|
|
726
|
-
/*
|
|
607
|
+
/* Log Items */
|
|
727
608
|
.log-item {
|
|
728
|
-
background: var(--
|
|
729
|
-
border: 1px solid var(--
|
|
730
|
-
border-radius:
|
|
731
|
-
padding:
|
|
732
|
-
|
|
609
|
+
background: var(--card);
|
|
610
|
+
border: 1px solid var(--line);
|
|
611
|
+
border-radius: 9px;
|
|
612
|
+
padding: 0.75rem 1rem;
|
|
613
|
+
margin-bottom: 0.6rem;
|
|
733
614
|
display: flex;
|
|
734
615
|
flex-direction: column;
|
|
735
|
-
gap:
|
|
736
|
-
}
|
|
737
|
-
.log-header {
|
|
738
|
-
display: flex;
|
|
739
|
-
justify-content: space-between;
|
|
740
|
-
align-items: center;
|
|
741
|
-
gap: 8px;
|
|
742
|
-
flex-wrap: wrap;
|
|
616
|
+
gap: 0.45rem;
|
|
743
617
|
}
|
|
744
|
-
.log-
|
|
745
|
-
.log-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
border
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
max-height: 120px;
|
|
756
|
-
overflow-y: auto;
|
|
618
|
+
.log-item details { margin-top: 0.2rem; }
|
|
619
|
+
.log-item details summary { cursor: pointer; color: var(--accent); font-size: 0.78rem; user-select: none; }
|
|
620
|
+
.log-item pre {
|
|
621
|
+
margin-top: 0.4rem;
|
|
622
|
+
background: var(--bg);
|
|
623
|
+
border: 1px solid var(--line);
|
|
624
|
+
border-radius: 6px;
|
|
625
|
+
padding: 0.6rem 0.75rem;
|
|
626
|
+
overflow-x: auto;
|
|
627
|
+
font-size: 0.76rem;
|
|
628
|
+
line-height: 1.4;
|
|
757
629
|
}
|
|
758
630
|
|
|
759
|
-
/* Toast
|
|
631
|
+
/* Toast */
|
|
760
632
|
.toast {
|
|
761
633
|
position: fixed;
|
|
762
|
-
bottom:
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
color: var(--
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
634
|
+
bottom: 1.25rem;
|
|
635
|
+
left: 50%;
|
|
636
|
+
transform: translateX(-50%);
|
|
637
|
+
background: var(--card2);
|
|
638
|
+
color: var(--fg);
|
|
639
|
+
border: 1px solid var(--line);
|
|
640
|
+
box-shadow: var(--shadow);
|
|
641
|
+
padding: 0.55rem 1.1rem;
|
|
642
|
+
border-radius: 10px;
|
|
643
|
+
font-size: 0.85rem;
|
|
644
|
+
z-index: 60;
|
|
772
645
|
display: none;
|
|
773
|
-
|
|
646
|
+
animation: fadeIn 0.15s ease-out;
|
|
774
647
|
}
|
|
648
|
+
.toast.err { border-color: var(--err); color: var(--err); }
|
|
649
|
+
@keyframes fadeIn { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
775
650
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
651
|
+
/* Test Result Box in Modal */
|
|
652
|
+
.test-result-box {
|
|
653
|
+
border: 1px solid var(--line);
|
|
654
|
+
border-radius: 8px;
|
|
655
|
+
padding: 0.75rem;
|
|
656
|
+
background: var(--bg);
|
|
657
|
+
font-size: 0.8rem;
|
|
658
|
+
display: none;
|
|
783
659
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
660
|
+
.test-result-box.success { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
661
|
+
.test-result-box.error { border-color: color-mix(in srgb, var(--err) 40%, transparent); }
|
|
662
|
+
|
|
663
|
+
/* Responsive Design (Matching Intact) */
|
|
664
|
+
@media (max-width: 820px) {
|
|
665
|
+
body { grid-template-columns: minmax(0, 1fr); }
|
|
666
|
+
aside, main, .page { min-width: 0; }
|
|
667
|
+
aside {
|
|
668
|
+
position: sticky;
|
|
669
|
+
top: 0;
|
|
670
|
+
height: auto;
|
|
671
|
+
flex-direction: row;
|
|
672
|
+
flex-wrap: wrap;
|
|
673
|
+
align-items: center;
|
|
674
|
+
padding: 0.55rem 0.85rem 0.4rem;
|
|
675
|
+
gap: 0.4rem;
|
|
676
|
+
border-right: 0;
|
|
677
|
+
border-bottom: 1px solid var(--line);
|
|
678
|
+
z-index: 10;
|
|
679
|
+
}
|
|
680
|
+
aside .brand { padding: 0 0.5rem 0 0; margin: 0; border: 0; }
|
|
681
|
+
aside .brand small { display: none; }
|
|
682
|
+
aside .brand b { font-size: 1.15rem; }
|
|
683
|
+
aside .logo { width: 30px; height: 30px; }
|
|
684
|
+
aside nav {
|
|
685
|
+
order: 3;
|
|
686
|
+
flex: 1 0 100%;
|
|
687
|
+
min-width: 0;
|
|
688
|
+
display: flex;
|
|
689
|
+
gap: 0.25rem;
|
|
690
|
+
overflow-x: auto;
|
|
691
|
+
scrollbar-width: none;
|
|
692
|
+
margin: 0 -0.25rem;
|
|
693
|
+
}
|
|
694
|
+
aside nav::-webkit-scrollbar { display: none; }
|
|
695
|
+
aside a.nav-link { padding: 0.45rem 0.65rem; white-space: nowrap; font-size: 0.82rem; }
|
|
696
|
+
aside a.nav-link svg { width: 15px; height: 15px; }
|
|
697
|
+
aside a.nav-link .ct { display: inline-block; font-size: 0.68rem; }
|
|
698
|
+
aside .spacer { flex: 1; }
|
|
699
|
+
aside .prefs {
|
|
700
|
+
width: auto;
|
|
701
|
+
padding: 0;
|
|
702
|
+
border: 0;
|
|
703
|
+
flex-direction: row;
|
|
704
|
+
gap: 0.5rem;
|
|
705
|
+
align-items: center;
|
|
706
|
+
}
|
|
707
|
+
aside .prefs > div { width: auto !important; margin-top: 0 !important; gap: 0.4rem; }
|
|
708
|
+
aside .prefs span { display: none; }
|
|
709
|
+
.ph { padding: 1rem; flex-wrap: wrap; gap: 0.75rem; }
|
|
710
|
+
.content { padding: 1rem 1rem 2.5rem; }
|
|
711
|
+
.row { padding: 0.75rem 1rem; flex-wrap: wrap; }
|
|
712
|
+
.pgrid { grid-template-columns: 1fr; }
|
|
794
713
|
}
|
|
795
714
|
</style>
|
|
796
715
|
</head>
|
|
797
716
|
<body>
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
<
|
|
801
|
-
<
|
|
802
|
-
<
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
717
|
+
<!-- Sidebar -->
|
|
718
|
+
<aside>
|
|
719
|
+
<a class="brand" href="#/routes" aria-label="LLM Switcher home">
|
|
720
|
+
<span class="logo">
|
|
721
|
+
<svg viewBox="0 0 32 32" aria-hidden="true">
|
|
722
|
+
<defs>
|
|
723
|
+
<linearGradient id="lsw-grad" x1="0" y1="0" x2="1" y2="1">
|
|
724
|
+
<stop offset="0%" stop-color="#7c6cf6"/>
|
|
725
|
+
<stop offset="100%" stop-color="#5eead4"/>
|
|
726
|
+
</linearGradient>
|
|
727
|
+
</defs>
|
|
728
|
+
<rect width="32" height="32" rx="9" fill="url(#lsw-grad)"/>
|
|
729
|
+
<path d="M8 12h16m-4-4 4 4-4 4M24 20H8m4-4-4 4 4 4" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
730
|
+
</svg>
|
|
731
|
+
</span>
|
|
732
|
+
<div>
|
|
733
|
+
<b>LLM Switcher</b>
|
|
734
|
+
<small>Edge Gateway · :<span class="gateway-port">3456</span></small>
|
|
811
735
|
</div>
|
|
812
|
-
</
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
<
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
<
|
|
821
|
-
|
|
822
|
-
|
|
736
|
+
</a>
|
|
737
|
+
|
|
738
|
+
<nav id="sideNav">
|
|
739
|
+
<a class="nav-link" href="#/routes" data-nav="routes">
|
|
740
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 12h16M12 4v16"/><circle cx="12" cy="12" r="8"/></svg>
|
|
741
|
+
Routes & Profiles<span class="ct" id="ct-routes"></span>
|
|
742
|
+
</a>
|
|
743
|
+
<a class="nav-link" href="#/models" data-nav="models">
|
|
744
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/></svg>
|
|
745
|
+
Model Catalog<span class="ct" id="ct-models"></span>
|
|
746
|
+
</a>
|
|
747
|
+
<a class="nav-link" href="#/logs" data-nav="logs">
|
|
748
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="m9 15 2 2 4-4"/></svg>
|
|
749
|
+
Request Inspector<span class="ct" id="ct-logs"></span>
|
|
750
|
+
</a>
|
|
751
|
+
<a class="nav-link" href="#/doctor" data-nav="doctor">
|
|
752
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="m9 12 2 2 4-4"/></svg>
|
|
753
|
+
System Doctor
|
|
754
|
+
</a>
|
|
755
|
+
</nav>
|
|
756
|
+
|
|
757
|
+
<div class="spacer"></div>
|
|
758
|
+
|
|
759
|
+
<div class="prefs" style="padding-top: 18px;">
|
|
760
|
+
<div style="display:flex; justify-content:space-between; align-items:center; width:100%;">
|
|
761
|
+
<span style="font-size:0.75rem; color:var(--muted);">Theme</span>
|
|
762
|
+
<div class="seg sm">
|
|
763
|
+
<button type="button" id="btn-theme-dark" onclick="applyThemeMode('dark')">Dark</button>
|
|
764
|
+
<button type="button" id="btn-theme-light" onclick="applyThemeMode('light')">Light</button>
|
|
823
765
|
</div>
|
|
824
766
|
</div>
|
|
825
|
-
<div
|
|
826
|
-
<
|
|
767
|
+
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:0.35rem;">
|
|
768
|
+
<span class="status on" id="status-aside-indicator">Gateway Active</span>
|
|
769
|
+
<button type="button" id="btn-toggle" class="btn btn-outline btn-sm" onclick="toggleProxy()">Toggle All</button>
|
|
827
770
|
</div>
|
|
828
771
|
</div>
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
772
|
+
</aside>
|
|
773
|
+
|
|
774
|
+
<!-- Main Panel -->
|
|
775
|
+
<div class="page">
|
|
776
|
+
<div class="ph">
|
|
777
|
+
<div class="title-box">
|
|
778
|
+
<h1 id="page-title">Routes & Profiles</h1>
|
|
779
|
+
<p id="status-sub">Route Claude Code, Codex, and OpenAI clients independently.</p>
|
|
835
780
|
</div>
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
<!-- Slot: Claude Code -->
|
|
839
|
-
<div id="card-slot-claude" class="cli-slot-card">
|
|
840
|
-
<div class="cli-slot-header">
|
|
841
|
-
<div class="cli-slot-title-group">
|
|
842
|
-
<span class="cli-slot-icon" aria-hidden="true">CC</span>
|
|
843
|
-
<div>
|
|
844
|
-
<div class="cli-slot-name">Claude Code</div>
|
|
845
|
-
<div class="cli-slot-route">/v1/messages</div>
|
|
846
|
-
</div>
|
|
847
|
-
</div>
|
|
848
|
-
<span id="badge-slot-claude" class="badge">OFF</span>
|
|
849
|
-
</div>
|
|
850
|
-
<label class="sr-only" for="select-slot-claude">Profile for Claude Code</label>
|
|
851
|
-
<select id="select-slot-claude" class="cli-slot-select" onchange="switchCliTarget('claude', this.value)">
|
|
852
|
-
<option value="">Official endpoint</option>
|
|
853
|
-
</select>
|
|
781
|
+
<div class="ph-actions" id="ph-actions">
|
|
782
|
+
<!-- Rendered dynamically per route -->
|
|
854
783
|
</div>
|
|
784
|
+
</div>
|
|
855
785
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
<
|
|
786
|
+
<div class="content">
|
|
787
|
+
<!-- VIEW 1: Routes & Profiles -->
|
|
788
|
+
<div id="view-routes" class="view-panel" style="display:none;">
|
|
789
|
+
<div class="sec">
|
|
790
|
+
<!-- Active Coding CLI Targets -->
|
|
791
|
+
<div class="card">
|
|
792
|
+
<div class="cardh">
|
|
793
|
+
<h2 style="font-size:1.02rem; font-weight:600;">Active Coding CLI Targets</h2>
|
|
794
|
+
<span class="badge ok" style="margin-left:auto;">Multi-Active Invariant</span>
|
|
864
795
|
</div>
|
|
865
|
-
</div>
|
|
866
|
-
<span id="badge-slot-codex" class="badge">OFF</span>
|
|
867
|
-
</div>
|
|
868
|
-
<label class="sr-only" for="select-slot-codex">Profile for Codex</label>
|
|
869
|
-
<select id="select-slot-codex" class="cli-slot-select" onchange="switchCliTarget('codex', this.value)">
|
|
870
|
-
<option value="">Official endpoint</option>
|
|
871
|
-
</select>
|
|
872
|
-
</div>
|
|
873
796
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
797
|
+
<!-- Claude Code Target Row -->
|
|
798
|
+
<div class="row" id="card-slot-claude">
|
|
799
|
+
<span class="av logo" title="Claude Code CLI"><img src="/icons/claude.png" alt="Claude Code"></span>
|
|
800
|
+
<div class="who">
|
|
801
|
+
<div class="l">Claude Code CLI</div>
|
|
802
|
+
<div class="meta">Native Anthropic Messages · <code>/v1/messages</code></div>
|
|
803
|
+
</div>
|
|
804
|
+
<div style="min-width:14rem;">
|
|
805
|
+
<select id="select-slot-claude" onchange="switchCliTarget('claude', this.value)">
|
|
806
|
+
<option value="">Official endpoint</option>
|
|
807
|
+
</select>
|
|
808
|
+
</div>
|
|
809
|
+
<span id="badge-slot-claude" class="badge outline">OFF</span>
|
|
810
|
+
</div>
|
|
885
811
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
812
|
+
<!-- Codex Target Row -->
|
|
813
|
+
<div class="row" id="card-slot-codex">
|
|
814
|
+
<span class="av logo" title="OpenAI Codex CLI"><img src="/icons/codex.png" alt="OpenAI Codex"></span>
|
|
815
|
+
<div class="who">
|
|
816
|
+
<div class="l">OpenAI Codex CLI</div>
|
|
817
|
+
<div class="meta">Responses WebSocket · <code>/v1/responses</code></div>
|
|
818
|
+
</div>
|
|
819
|
+
<div style="min-width:14rem;">
|
|
820
|
+
<select id="select-slot-codex" onchange="switchCliTarget('codex', this.value)">
|
|
821
|
+
<option value="">Official endpoint</option>
|
|
822
|
+
</select>
|
|
823
|
+
</div>
|
|
824
|
+
<span id="badge-slot-codex" class="badge outline">OFF</span>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
892
827
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('codex-vtx')">Codex Vertex</button>
|
|
899
|
-
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('momo')">Selfhost Hybrid</button>
|
|
900
|
-
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('custom')">Local OpenAI</button>
|
|
901
|
-
</div>
|
|
828
|
+
<!-- Configured Profiles Header -->
|
|
829
|
+
<div class="sech" style="margin-top:0.5rem;">
|
|
830
|
+
<h2 style="font-size:1.02rem; font-weight:600;">Configured Provider Profiles</h2>
|
|
831
|
+
<span class="hint">Click any card to edit configuration</span>
|
|
832
|
+
</div>
|
|
902
833
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
<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>
|
|
907
|
-
<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>
|
|
908
|
-
<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>
|
|
909
|
-
<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>
|
|
834
|
+
<!-- Profiles Grid -->
|
|
835
|
+
<div id="profiles-grid" class="pgrid"></div>
|
|
836
|
+
</div>
|
|
910
837
|
</div>
|
|
911
838
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
<div
|
|
915
|
-
<div class="
|
|
916
|
-
<div class="
|
|
917
|
-
<
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
<
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
<label for="p-url">Base URL</label>
|
|
926
|
-
<input type="url" id="p-url" required placeholder="https://api.your-provider.com/v1">
|
|
927
|
-
</div>
|
|
928
|
-
<div class="form-group full">
|
|
929
|
-
<label for="p-key-val">API key or bearer token</label>
|
|
930
|
-
<input type="password" id="p-key-val" autocomplete="off" placeholder="sk-...">
|
|
839
|
+
<!-- VIEW 2: Discovered Models Catalog -->
|
|
840
|
+
<div id="view-models" class="view-panel" style="display:none;">
|
|
841
|
+
<div class="sec">
|
|
842
|
+
<div class="card pad">
|
|
843
|
+
<div class="sech">
|
|
844
|
+
<div>
|
|
845
|
+
<h2 style="font-size:1.05rem; font-weight:600;">Dynamic Model Discovery</h2>
|
|
846
|
+
<p class="hint" style="margin-top:0.2rem;">Official model names are auto-polled on tool version bumps to prevent drift and 404 errors.</p>
|
|
847
|
+
</div>
|
|
848
|
+
<div class="search">
|
|
849
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
|
850
|
+
<input type="text" id="catalog-search" placeholder="Search models…" oninput="filterCatalogView(this.value)">
|
|
851
|
+
</div>
|
|
931
852
|
</div>
|
|
932
853
|
</div>
|
|
933
|
-
</div>
|
|
934
854
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
<option value="anthropic">Claude Code only (/v1/messages)</option>
|
|
943
|
-
<option value="responses">Codex only (/v1/responses)</option>
|
|
944
|
-
</select>
|
|
945
|
-
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">Saved as <code>tool</code> in config.json. OpenAI Chat and Vertex are no longer client inputs: the gateway stands between Claude Code, Codex and the provider.</span>
|
|
946
|
-
</div>
|
|
947
|
-
<div class="form-group">
|
|
948
|
-
<label for="p-outformat">Upstream protocol</label>
|
|
949
|
-
<select id="p-outformat">
|
|
950
|
-
<option value="">AUTO (Determined by Routing Mode)</option>
|
|
951
|
-
<option value="openai-chat">OpenAI Chat Completions</option>
|
|
952
|
-
<option value="anthropic">Anthropic Messages</option>
|
|
953
|
-
<option value="vertex">Vertex generateContent</option>
|
|
954
|
-
</select>
|
|
955
|
-
</div>
|
|
956
|
-
<div class="form-group full">
|
|
957
|
-
<label for="p-thinking-mode">Reasoning mode for Chat upstreams</label>
|
|
958
|
-
<select id="p-thinking-mode">
|
|
959
|
-
<option value="auto">AUTO — gateway (9Router): restore thinking, inject <think> guide, send thinking + reasoning_effort</option>
|
|
960
|
-
<option value="native">NATIVE — strict OpenAI API: only reasoning_effort when client asks, no prompt changes</option>
|
|
961
|
-
<option value="off">OFF — never send reasoning parameters</option>
|
|
962
|
-
</select>
|
|
963
|
-
</div>
|
|
964
|
-
<div class="form-group full">
|
|
965
|
-
<label for="p-mode">Routing mode</label>
|
|
966
|
-
<select id="p-mode">
|
|
967
|
-
<option value="hybrid">HYBRID (Claude models -> Direct Native | Other models -> Convert)</option>
|
|
968
|
-
<option value="convert">CONVERT (Always convert protocols via gateway)</option>
|
|
969
|
-
<option value="direct">DIRECT (Always forward directly via native Anthropic API)</option>
|
|
970
|
-
</select>
|
|
855
|
+
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(20rem, 1fr)); gap:1.2rem;">
|
|
856
|
+
<div class="card pad">
|
|
857
|
+
<div class="sech" style="margin-bottom:0.8rem;">
|
|
858
|
+
<h3 style="font-size:0.95rem; font-weight:600; color:var(--accent); display:flex; align-items:center; gap:0.5rem;"><span class="av logo sm" style="width:22px; height:22px; padding:2px;"><img src="/icons/claude.png" alt="Claude"></span> Claude Code Tiers</h3>
|
|
859
|
+
<span class="badge accent" id="badge-claude-count">0 tiers</span>
|
|
860
|
+
</div>
|
|
861
|
+
<div id="catalog-claude-items" style="display:grid; gap:0.6rem;"></div>
|
|
971
862
|
</div>
|
|
972
|
-
<div class="
|
|
973
|
-
<div style="
|
|
974
|
-
<span class="
|
|
975
|
-
<span
|
|
863
|
+
<div class="card pad">
|
|
864
|
+
<div class="sech" style="margin-bottom:0.8rem;">
|
|
865
|
+
<h3 style="font-size:0.95rem; font-weight:600; color:var(--mint); display:flex; align-items:center; gap:0.5rem;"><span class="av logo sm" style="width:22px; height:22px; padding:2px;"><img src="/icons/codex.png" alt="Codex"></span> Codex Roles</h3>
|
|
866
|
+
<span class="badge ok" id="badge-codex-count">0 roles</span>
|
|
976
867
|
</div>
|
|
977
|
-
<
|
|
978
|
-
<strong>Optimizer chain:</strong> Point the CLI at the optimizer. Point the optimizer upstream to <code>http://127.0.0.1:<span class="gateway-port">3456</span></code>.
|
|
979
|
-
</span>
|
|
868
|
+
<div id="catalog-codex-items" style="display:grid; gap:0.6rem;"></div>
|
|
980
869
|
</div>
|
|
981
870
|
</div>
|
|
982
871
|
</div>
|
|
872
|
+
</div>
|
|
983
873
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
<div
|
|
989
|
-
<
|
|
874
|
+
<!-- VIEW 3: Request Inspector -->
|
|
875
|
+
<div id="view-logs" class="view-panel" style="display:none;">
|
|
876
|
+
<div class="sec">
|
|
877
|
+
<div class="card pad">
|
|
878
|
+
<div class="sech">
|
|
879
|
+
<div>
|
|
880
|
+
<h2 style="font-size:1.05rem; font-weight:600;">Live Request Inspector</h2>
|
|
881
|
+
<p class="hint" style="margin-top:0.2rem;">In-memory ring buffer capturing the most recent traffic through the edge gateway.</p>
|
|
882
|
+
</div>
|
|
883
|
+
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
884
|
+
<label class="hint" style="display:flex; align-items:center; gap:0.4rem; cursor:pointer;">
|
|
885
|
+
<input type="checkbox" id="chk-auto-refresh-logs" onchange="toggleLogAutoRefresh()" style="width:auto; min-height:0;" checked> Auto-refresh (3s)
|
|
886
|
+
</label>
|
|
887
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="clearInspectorLogs()">Clear Logs</button>
|
|
888
|
+
</div>
|
|
990
889
|
</div>
|
|
991
890
|
</div>
|
|
992
891
|
|
|
993
|
-
<div
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
Fetch models
|
|
997
|
-
</button>
|
|
998
|
-
</div>
|
|
999
|
-
|
|
1000
|
-
<div class="form-grid" id="slot-fields">
|
|
1001
|
-
<!-- Dynamically populated via renderModelSlots -->
|
|
1002
|
-
</div>
|
|
892
|
+
<div id="inspector-logs-container"></div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
1003
895
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
<
|
|
1009
|
-
|
|
1010
|
-
<
|
|
1011
|
-
<
|
|
896
|
+
<!-- VIEW 4: System Doctor -->
|
|
897
|
+
<div id="view-doctor" class="view-panel" style="display:none;">
|
|
898
|
+
<div class="sec">
|
|
899
|
+
<div class="stats">
|
|
900
|
+
<div class="stat">
|
|
901
|
+
<div class="k">Edge Gateway Port</div>
|
|
902
|
+
<div class="v gateway-port" style="color:var(--accent);">3456</div>
|
|
903
|
+
<div class="status on" style="margin-top:0.3rem;">Bound to 127.0.0.1</div>
|
|
1012
904
|
</div>
|
|
1013
|
-
</div>
|
|
1014
905
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
<
|
|
1018
|
-
<
|
|
1019
|
-
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">Shown in the CLI <code>/model</code> picker and sent with every request.</span>
|
|
1020
|
-
</div>
|
|
1021
|
-
<div class="form-group">
|
|
1022
|
-
<label for="p-public-review">Review model name</label>
|
|
1023
|
-
<input type="text" id="p-public-review" list="model-options" placeholder="e.g. gpt-5.6-terra">
|
|
906
|
+
<div class="stat">
|
|
907
|
+
<div class="k">Blindfold Interceptor</div>
|
|
908
|
+
<div class="v" id="blindfold-port-display" style="color:var(--mint); font-size:1.15rem;">Active</div>
|
|
909
|
+
<div class="status on" style="margin-top:0.3rem;">Zero tool mutation</div>
|
|
1024
910
|
</div>
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
<
|
|
911
|
+
|
|
912
|
+
<div class="stat">
|
|
913
|
+
<div class="k">Security Invariant</div>
|
|
914
|
+
<div class="v" style="color:var(--ok);">Clean (100%)</div>
|
|
915
|
+
<div class="hint" style="margin-top:0.3rem;">No settings.json edits</div>
|
|
1028
916
|
</div>
|
|
1029
917
|
</div>
|
|
1030
|
-
</div>
|
|
1031
918
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
<div class="
|
|
1035
|
-
<div class="
|
|
1036
|
-
<
|
|
1037
|
-
|
|
1038
|
-
|
|
919
|
+
<div class="card pad">
|
|
920
|
+
<h3 style="font-size:0.95rem; font-weight:600; margin-bottom:0.75rem;">Environment Isolation & Host Routing</h3>
|
|
921
|
+
<div class="sec">
|
|
922
|
+
<div class="copyline">
|
|
923
|
+
<code>NO_PROXY=127.0.0.1,localhost (Safe from RTK/Headroom/VPN conflicts)</code>
|
|
924
|
+
</div>
|
|
925
|
+
<div class="copyline">
|
|
926
|
+
<code>Blindfold Hosts: api.anthropic.com, api.openai.com, chatgpt.com</code>
|
|
927
|
+
</div>
|
|
928
|
+
<div class="copyline">
|
|
929
|
+
<code>Admin Token Protection: mode 0600 (admin.token) required for state mutation</code>
|
|
1039
930
|
</div>
|
|
1040
|
-
<div id="model-items" class="model-list-scroll"></div>
|
|
1041
931
|
</div>
|
|
1042
932
|
</div>
|
|
1043
933
|
</div>
|
|
934
|
+
</div>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
1044
937
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
938
|
+
<!-- Profile Modal Editor -->
|
|
939
|
+
<div id="profile-modal-overlay" class="overlay" style="display:none;" onclick="if(event.target===this) closeProfileModal()">
|
|
940
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="form-title">
|
|
941
|
+
<div class="mh">
|
|
942
|
+
<div>
|
|
943
|
+
<strong id="form-title">Edit Provider Profile</strong>
|
|
944
|
+
<span id="active-tag" style="font-size:0.75rem; color:var(--ok); margin-left:0.5rem; font-weight:600;"></span>
|
|
945
|
+
</div>
|
|
946
|
+
<button class="icon" type="button" onclick="closeProfileModal()" aria-label="Close dialog">✕</button>
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
<form id="profile-form" onsubmit="saveProfile(event)" style="display:flex; flex-direction:column; overflow:hidden;">
|
|
950
|
+
<!-- Modal Inset Tabs -->
|
|
951
|
+
<div style="padding:0.85rem 1.25rem 0.5rem; background:var(--card2);">
|
|
952
|
+
<div class="tabs">
|
|
953
|
+
<button type="button" id="tab-button-general" class="tab-btn active" onclick="switchTab('tab-general')">General</button>
|
|
954
|
+
<button type="button" id="tab-button-routing" class="tab-btn" onclick="switchTab('tab-routing')">Routing & Protocol</button>
|
|
955
|
+
<button type="button" id="tab-button-models" class="tab-btn" onclick="switchTab('tab-models')">Model Slots</button>
|
|
1057
956
|
</div>
|
|
1058
957
|
</div>
|
|
1059
958
|
|
|
1060
|
-
<div
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
<span style="font-
|
|
959
|
+
<div class="mb">
|
|
960
|
+
<!-- TAB 1: General -->
|
|
961
|
+
<div id="tab-general" class="tab-content active" style="display:grid; gap:0.9rem;">
|
|
962
|
+
<div style="background:var(--card2); padding:0.75rem 1rem; border-radius:8px; border:1px solid var(--line);">
|
|
963
|
+
<span class="hint" style="font-weight:600; display:block; margin-bottom:0.4rem;">Template Quick-Fill:</span>
|
|
964
|
+
<div style="display:flex; gap:0.4rem; flex-wrap:wrap;">
|
|
965
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('intact')">intact</button>
|
|
966
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('9router')">9Router</button>
|
|
967
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('openrouter')">OpenRouter</button>
|
|
968
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('vertex')">Vertex AI</button>
|
|
969
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="applyTemplate('local')">Local Server</button>
|
|
970
|
+
</div>
|
|
1065
971
|
</div>
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
972
|
+
|
|
973
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
974
|
+
<label class="f">Profile Key (ID)
|
|
975
|
+
<input type="text" id="p-key" required pattern="[A-Za-z0-9][A-Za-z0-9._\-]{0,63}" placeholder="e.g. intact-claude">
|
|
976
|
+
</label>
|
|
977
|
+
<label class="f">Display Name
|
|
978
|
+
<input type="text" id="p-name" required placeholder="e.g. Intact Claude Gateway">
|
|
1069
979
|
</label>
|
|
1070
|
-
<button type="button" class="btn btn-outline btn-sm" onclick="loadInspectorLogs()">Refresh</button>
|
|
1071
|
-
<button type="button" class="btn btn-danger btn-sm" onclick="clearInspectorLogs()">Clear</button>
|
|
1072
980
|
</div>
|
|
981
|
+
|
|
982
|
+
<label class="f">Base URL
|
|
983
|
+
<input type="url" id="p-url" required placeholder="https://api.example.com/v1">
|
|
984
|
+
</label>
|
|
985
|
+
|
|
986
|
+
<label class="f">API Key
|
|
987
|
+
<input type="password" id="p-key-val" placeholder="sk-...">
|
|
988
|
+
</label>
|
|
1073
989
|
</div>
|
|
1074
990
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
991
|
+
<!-- TAB 2: Routing -->
|
|
992
|
+
<div id="tab-routing" class="tab-content" style="display:none; gap:0.9rem; flex-direction:column;">
|
|
993
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
994
|
+
<label class="f">Tool Target
|
|
995
|
+
<select id="p-tool" onchange="onToolChange()">
|
|
996
|
+
<option value="auto">All compatible tools (auto)</option>
|
|
997
|
+
<option value="claude">Claude Code only (claude)</option>
|
|
998
|
+
<option value="codex">Codex only (codex)</option>
|
|
999
|
+
</select>
|
|
1000
|
+
</label>
|
|
1001
|
+
<label class="f">Outbound Format
|
|
1002
|
+
<select id="p-outformat">
|
|
1003
|
+
<option value="openai-chat">OpenAI Chat (/v1/chat/completions)</option>
|
|
1004
|
+
<option value="anthropic">Anthropic Messages (/v1/messages)</option>
|
|
1005
|
+
<option value="vertex">Google Vertex AI (/models/...)</option>
|
|
1006
|
+
</select>
|
|
1007
|
+
</label>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
1011
|
+
<label class="f">Thinking Mode
|
|
1012
|
+
<select id="p-thinking-mode">
|
|
1013
|
+
<option value="auto">AUTO (restores reasoning, strips tags safely)</option>
|
|
1014
|
+
<option value="native">NATIVE (strict reasoning_effort)</option>
|
|
1015
|
+
<option value="off">OFF (never send thinking)</option>
|
|
1016
|
+
</select>
|
|
1017
|
+
</label>
|
|
1018
|
+
<label class="f">Routing Mode
|
|
1019
|
+
<select id="p-mode">
|
|
1020
|
+
<option value="convert">CONVERT (Bi-directional translation)</option>
|
|
1021
|
+
<option value="direct">DIRECT (Passthrough)</option>
|
|
1022
|
+
<option value="hybrid">HYBRID (Automatic)</option>
|
|
1023
|
+
</select>
|
|
1024
|
+
</label>
|
|
1078
1025
|
</div>
|
|
1079
1026
|
</div>
|
|
1080
|
-
</div>
|
|
1081
1027
|
|
|
1082
|
-
|
|
1028
|
+
<!-- TAB 3: Models & 1M -->
|
|
1029
|
+
<div id="tab-models" class="tab-content" style="display:none; gap:0.9rem; flex-direction:column;">
|
|
1030
|
+
<div class="badge warn" id="warn-1m-callout" style="display:none; padding:0.5rem 0.75rem; border-radius:8px;">
|
|
1031
|
+
<span id="warn-1m-icon">⚠️</span> <span id="warn-1m-text">1M context flag is enabled</span>
|
|
1032
|
+
</div>
|
|
1033
|
+
|
|
1034
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
1035
|
+
<h3 id="model-slot-heading" style="font-size:0.92rem; font-weight:600;">Model Slots Mapping</h3>
|
|
1036
|
+
<button type="button" id="btn-fetch-models" class="btn btn-outline btn-sm" onclick="fetchModelList()">Browse Models</button>
|
|
1037
|
+
</div>
|
|
1038
|
+
|
|
1039
|
+
<div id="slot-fields" style="display:grid; gap:0.75rem;"></div>
|
|
1040
|
+
|
|
1041
|
+
<div id="model-browser" style="display:none; background:var(--card2); padding:0.85rem; border-radius:8px; border:1px solid var(--line);">
|
|
1042
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.5rem;">
|
|
1043
|
+
<span id="model-count-label" class="hint">Loaded models</span>
|
|
1044
|
+
<input type="text" id="model-filter" placeholder="Search..." oninput="filterModels()" style="width:12rem; min-height:28px;">
|
|
1045
|
+
</div>
|
|
1046
|
+
<div id="model-items" style="max-height:8rem; overflow-y:auto; display:flex; flex-wrap:wrap; gap:0.35rem;"></div>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1083
1049
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1050
|
+
<!-- Test Connection Box -->
|
|
1051
|
+
<div id="test-result" class="test-result-box">
|
|
1052
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
1053
|
+
<span id="test-status-badge" class="badge"></span>
|
|
1054
|
+
<span id="test-latency" class="hint mono"></span>
|
|
1055
|
+
</div>
|
|
1056
|
+
<div id="test-sample" class="mono" style="margin-top:0.4rem; font-size:0.75rem; max-height:4rem; overflow:auto;"></div>
|
|
1089
1057
|
</div>
|
|
1090
|
-
<div id="test-sample" class="test-sample"></div>
|
|
1091
1058
|
</div>
|
|
1092
1059
|
|
|
1093
|
-
<div class="
|
|
1094
|
-
<
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1060
|
+
<div class="mf">
|
|
1061
|
+
<div>
|
|
1062
|
+
<button type="button" id="btn-delete" class="btn btn-danger" onclick="deleteProfile()" style="display:none;">Delete Profile</button>
|
|
1063
|
+
</div>
|
|
1064
|
+
<div style="display:flex; gap:0.6rem;">
|
|
1065
|
+
<button type="button" id="btn-test-conn" class="btn btn-outline" onclick="testConnection()">Test Connection</button>
|
|
1066
|
+
<button type="button" class="btn btn-outline" onclick="closeProfileModal()">Cancel</button>
|
|
1067
|
+
<button type="submit" class="btn btn-primary">Save Profile</button>
|
|
1098
1068
|
</div>
|
|
1099
1069
|
</div>
|
|
1100
1070
|
</form>
|
|
1101
1071
|
</div>
|
|
1102
|
-
</
|
|
1072
|
+
</div>
|
|
1103
1073
|
|
|
1104
|
-
<div id="toast" class="toast"
|
|
1074
|
+
<div id="toast" class="toast"></div>
|
|
1075
|
+
<datalist id="model-options"></datalist>
|
|
1105
1076
|
|
|
1106
1077
|
<script>
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
const
|
|
1110
|
-
(()
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
})();
|
|
1117
|
-
|
|
1118
|
-
// A config change carries the revision this page loaded. The gateway answers 409 when another tab
|
|
1119
|
-
// or the CLI saved in the meantime; the page then reloads instead of overwriting that change.
|
|
1120
|
-
const CHANGE_PATHS = new Set(['/api/switch', '/api/toggle', '/api/save-profile', '/api/delete-profile']);
|
|
1121
|
-
|
|
1122
|
-
async function api(path, opts = {}) {
|
|
1123
|
-
const headers = { ...(opts.headers || {}), 'x-llm-switcher-token': sessionStorage.getItem(ADMIN_TOKEN_KEY) || '' };
|
|
1124
|
-
let body = opts.body;
|
|
1125
|
-
if (CHANGE_PATHS.has(path) && typeof body === 'string' && state.revision) {
|
|
1126
|
-
body = JSON.stringify({ ...JSON.parse(body), revision: state.revision });
|
|
1127
|
-
}
|
|
1128
|
-
const res = await fetch(path, { ...opts, headers, body });
|
|
1129
|
-
if (res.status === 401) showToast('Not authorized. Open the dashboard with `switch ui`.', true);
|
|
1130
|
-
if (res.status === 409) setTimeout(loadStatus, 0);
|
|
1131
|
-
return res;
|
|
1078
|
+
// State & Globals
|
|
1079
|
+
const ADMIN_TOKEN_KEY = 'llm-switcher-admin-token';
|
|
1080
|
+
const servedToken = document.querySelector('meta[name="llm-switcher-token"]')?.content;
|
|
1081
|
+
const hashTokenMatch = location.hash.match(/(?:^#|&)token=([0-9a-f]+)/);
|
|
1082
|
+
if (servedToken || hashTokenMatch) {
|
|
1083
|
+
sessionStorage.setItem(ADMIN_TOKEN_KEY, servedToken || hashTokenMatch[1]);
|
|
1084
|
+
}
|
|
1085
|
+
if (hashTokenMatch) {
|
|
1086
|
+
history.replaceState(null, '', location.pathname + location.search + '#/routes');
|
|
1132
1087
|
}
|
|
1133
1088
|
|
|
1134
1089
|
let state = {
|
|
1090
|
+
port: 3456,
|
|
1135
1091
|
config: null,
|
|
1136
1092
|
activeProfile: '',
|
|
1137
1093
|
activeProfiles: {},
|
|
@@ -1144,45 +1100,26 @@
|
|
|
1144
1100
|
let loadedModels = [];
|
|
1145
1101
|
let currentProfileBuffer = null;
|
|
1146
1102
|
let renderedSlotFormat = 'auto';
|
|
1103
|
+
let logRefreshTimer = null;
|
|
1104
|
+
let cachedCatalog = null;
|
|
1105
|
+
let lastLogsCount = 0;
|
|
1147
1106
|
const MASKED_KEY = '__LLM_SWITCHER_KEEP_KEY__';
|
|
1148
|
-
// Order matters: publicModels is read by position when codexRoles has no entry.
|
|
1149
|
-
const CODEX_ROLE_SLOTS = ['main', 'review', 'subagent'];
|
|
1150
|
-
|
|
1151
|
-
const LEGACY_SLOT_KEYS = {
|
|
1152
|
-
main: ['opus'],
|
|
1153
|
-
review: ['sonnet'],
|
|
1154
|
-
subagent: ['fast', 'fallback', 'haiku', 'fable'],
|
|
1155
|
-
default: ['sonnet', 'opus', 'haiku', 'fable']
|
|
1156
|
-
};
|
|
1157
1107
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
responses: {
|
|
1171
|
-
heading: 'Codex model roles',
|
|
1172
|
-
slots: [
|
|
1173
|
-
{ id: 'main', label: 'Main model', placeholder: 'e.g. ag/gemini-3.8-flash or gpt-4o', allow1M: true },
|
|
1174
|
-
{ id: 'review', label: 'Review model', placeholder: 'e.g. ag/gemini-3.7-flash-medium', allow1M: false },
|
|
1175
|
-
{ id: 'subagent', label: 'Subagent model', placeholder: 'e.g. ag/gemini-3.6-flash-low', allow1M: false }
|
|
1176
|
-
],
|
|
1177
|
-
warning: '<strong>Context notice:</strong> The launcher applies <code>model</code>, <code>review_model</code>, and <code>agents.default_subagent_model</code> through Codex config overrides. The 1M option also sets <code>model_context_window</code> and the auto-compact limit.',
|
|
1178
|
-
icon: 'NOTE'
|
|
1179
|
-
},
|
|
1180
|
-
};
|
|
1181
|
-
SLOT_CONFIG.auto = SLOT_CONFIG.anthropic;
|
|
1108
|
+
// API Helper
|
|
1109
|
+
async function api(path, opts = {}) {
|
|
1110
|
+
const token = sessionStorage.getItem(ADMIN_TOKEN_KEY) || document.querySelector('meta[name="llm-switcher-token"]')?.content || '';
|
|
1111
|
+
const headers = {
|
|
1112
|
+
...opts.headers,
|
|
1113
|
+
'x-llm-switcher-token': token
|
|
1114
|
+
};
|
|
1115
|
+
const res = await fetch(path, { ...opts, headers });
|
|
1116
|
+
if (res.status === 401) showToast('Unauthorized: check admin.token in server directory', true);
|
|
1117
|
+
if (res.status === 409) setTimeout(loadStatus, 0);
|
|
1118
|
+
return res;
|
|
1119
|
+
}
|
|
1182
1120
|
|
|
1183
|
-
// Helper: Anti-XSS Sanitizer
|
|
1184
1121
|
function escapeHtml(str) {
|
|
1185
|
-
if (str
|
|
1122
|
+
if (!str) return '';
|
|
1186
1123
|
return String(str)
|
|
1187
1124
|
.replace(/&/g, '&')
|
|
1188
1125
|
.replace(/</g, '<')
|
|
@@ -1191,360 +1128,485 @@
|
|
|
1191
1128
|
.replace(/'/g, ''');
|
|
1192
1129
|
}
|
|
1193
1130
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1131
|
+
// Theme Mode
|
|
1132
|
+
function applyThemeMode(mode) {
|
|
1133
|
+
document.documentElement.dataset.theme = mode;
|
|
1134
|
+
localStorage.setItem('intact-theme', mode);
|
|
1135
|
+
localStorage.setItem('lsw-theme', mode);
|
|
1136
|
+
document.getElementById('btn-theme-dark')?.setAttribute('aria-pressed', mode === 'dark');
|
|
1137
|
+
document.getElementById('btn-theme-light')?.setAttribute('aria-pressed', mode === 'light');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Toast
|
|
1141
|
+
let toastTimeout = null;
|
|
1142
|
+
function showToast(msg, isErr = false) {
|
|
1143
|
+
const toast = document.getElementById('toast');
|
|
1144
|
+
if (!toast) return;
|
|
1145
|
+
toast.textContent = msg;
|
|
1146
|
+
toast.className = 'toast' + (isErr ? ' err' : '');
|
|
1147
|
+
toast.style.display = 'block';
|
|
1148
|
+
clearTimeout(toastTimeout);
|
|
1149
|
+
toastTimeout = setTimeout(() => { toast.style.display = 'none'; }, 3200);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Modal Control
|
|
1153
|
+
function openProfileModal(isNew = false) {
|
|
1154
|
+
const modal = document.getElementById('profile-modal-overlay');
|
|
1155
|
+
if (modal) modal.style.display = 'grid';
|
|
1156
|
+
if (isNew) {
|
|
1157
|
+
initNewProfile();
|
|
1201
1158
|
}
|
|
1202
|
-
try { localStorage.setItem('lsw-theme', theme); } catch (e) {}
|
|
1203
1159
|
}
|
|
1204
1160
|
|
|
1205
|
-
function
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1161
|
+
function closeProfileModal() {
|
|
1162
|
+
const modal = document.getElementById('profile-modal-overlay');
|
|
1163
|
+
if (modal) modal.style.display = 'none';
|
|
1208
1164
|
}
|
|
1209
1165
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1166
|
+
// Close modal on Escape
|
|
1167
|
+
window.addEventListener('keydown', (e) => {
|
|
1168
|
+
if (e.key === 'Escape') {
|
|
1169
|
+
const modal = document.getElementById('profile-modal-overlay');
|
|
1170
|
+
if (modal && modal.style.display !== 'none') {
|
|
1171
|
+
closeProfileModal();
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// Hash Router
|
|
1177
|
+
function handleRoute() {
|
|
1178
|
+
const hash = window.location.hash || '#/routes';
|
|
1179
|
+
const route = hash.replace(/^#\/?/, '').split('/')[0] || 'routes';
|
|
1180
|
+
const validRoutes = ['routes', 'models', 'logs', 'doctor'];
|
|
1181
|
+
const activeRoute = validRoutes.includes(route) ? route : 'routes';
|
|
1182
|
+
|
|
1183
|
+
// Update active nav link
|
|
1184
|
+
document.querySelectorAll('#sideNav .nav-link').forEach(link => {
|
|
1185
|
+
if (link.getAttribute('data-nav') === activeRoute) {
|
|
1186
|
+
link.setAttribute('aria-current', 'page');
|
|
1187
|
+
} else {
|
|
1188
|
+
link.removeAttribute('aria-current');
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// Show matching view panel
|
|
1193
|
+
document.querySelectorAll('.view-panel').forEach(panel => {
|
|
1194
|
+
panel.style.display = (panel.id === 'view-' + activeRoute) ? 'block' : 'none';
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// Update dynamic header
|
|
1198
|
+
updateHeader(activeRoute);
|
|
1199
|
+
|
|
1200
|
+
// Lazy load view content
|
|
1201
|
+
if (activeRoute === 'models') renderCatalogView();
|
|
1202
|
+
if (activeRoute === 'logs') loadInspectorLogs();
|
|
1218
1203
|
}
|
|
1204
|
+
window.addEventListener('hashchange', handleRoute);
|
|
1205
|
+
|
|
1206
|
+
function updateHeader(route) {
|
|
1207
|
+
const titleEl = document.getElementById('page-title');
|
|
1208
|
+
const subEl = document.getElementById('status-sub');
|
|
1209
|
+
const actionsEl = document.getElementById('ph-actions');
|
|
1210
|
+
if (!titleEl || !subEl || !actionsEl) return;
|
|
1219
1211
|
|
|
1212
|
+
const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
|
|
1213
|
+
const activeTargets = Object.entries(activeMap).filter(([_, v]) => Boolean(v));
|
|
1214
|
+
|
|
1215
|
+
if (route === 'routes') {
|
|
1216
|
+
titleEl.textContent = 'Routes & Profiles';
|
|
1217
|
+
if (activeTargets.length > 0) {
|
|
1218
|
+
const parts = [];
|
|
1219
|
+
if (activeMap.claude) parts.push('Claude: ' + activeMap.claude);
|
|
1220
|
+
if (activeMap.codex) parts.push('Codex: ' + activeMap.codex);
|
|
1221
|
+
subEl.textContent = 'Port ' + state.port + ' · Active for ' + parts.join(' · ');
|
|
1222
|
+
} else {
|
|
1223
|
+
subEl.textContent = 'Gateway is idle. Clients connect directly to official endpoints.';
|
|
1224
|
+
}
|
|
1225
|
+
actionsEl.innerHTML = `
|
|
1226
|
+
<button class="btn btn-primary" onclick="openProfileModal(true)">+ Add Profile</button>
|
|
1227
|
+
<button class="btn btn-outline" id="btn-toggle" onclick="toggleProxy()">${activeTargets.length > 0 ? 'Use official endpoints' : 'Activate compatible routes'}</button>
|
|
1228
|
+
<button class="btn btn-outline" onclick="loadStatus()">Refresh</button>
|
|
1229
|
+
`;
|
|
1230
|
+
} else if (route === 'models') {
|
|
1231
|
+
titleEl.textContent = 'Dynamic Model Catalog';
|
|
1232
|
+
subEl.textContent = 'Auto-polled models from upstream providers to guarantee zero 404/drift errors.';
|
|
1233
|
+
actionsEl.innerHTML = `
|
|
1234
|
+
<button class="btn btn-outline" id="btn-sync-catalog" onclick="refreshCatalogUpstream()">⚡ Sync Models with Upstream</button>
|
|
1235
|
+
<button class="btn btn-outline" onclick="renderCatalogView()">Refresh</button>
|
|
1236
|
+
`;
|
|
1237
|
+
} else if (route === 'logs') {
|
|
1238
|
+
titleEl.textContent = 'Request Inspector';
|
|
1239
|
+
subEl.textContent = 'Real-time HTTP traffic ring buffer passing through the local edge gateway.';
|
|
1240
|
+
actionsEl.innerHTML = `
|
|
1241
|
+
<button class="btn btn-outline btn-sm" onclick="clearInspectorLogs()">Clear</button>
|
|
1242
|
+
<button class="btn btn-outline btn-sm" onclick="loadInspectorLogs()">Refresh</button>
|
|
1243
|
+
`;
|
|
1244
|
+
} else if (route === 'doctor') {
|
|
1245
|
+
titleEl.textContent = 'System Health & Doctor';
|
|
1246
|
+
subEl.textContent = 'Verification of loopback bindings, interceptor certificates, and zero-mutation guarantees.';
|
|
1247
|
+
actionsEl.innerHTML = `
|
|
1248
|
+
<button class="btn btn-outline" onclick="loadStatus()">Re-check Status</button>
|
|
1249
|
+
`;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Modal Tabs
|
|
1220
1254
|
function switchTab(tabId) {
|
|
1221
|
-
document.querySelectorAll('.tab-btn').forEach(btn =>
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
btn.tabIndex = selected ? 0 : -1;
|
|
1226
|
-
});
|
|
1227
|
-
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
1228
|
-
const selected = pane.id === tabId;
|
|
1229
|
-
pane.classList.toggle('active', selected);
|
|
1230
|
-
pane.hidden = !selected;
|
|
1255
|
+
document.querySelectorAll('#profile-form .tab-btn').forEach(btn => btn.classList.remove('active'));
|
|
1256
|
+
document.querySelectorAll('#profile-form .tab-content').forEach(c => {
|
|
1257
|
+
c.classList.remove('active');
|
|
1258
|
+
c.style.display = 'none';
|
|
1231
1259
|
});
|
|
1260
|
+
|
|
1261
|
+
const btnId = tabId.replace('tab-', 'tab-button-');
|
|
1262
|
+
document.getElementById(btnId)?.classList.add('active');
|
|
1263
|
+
const target = document.getElementById(tabId);
|
|
1264
|
+
if (target) {
|
|
1265
|
+
target.classList.add('active');
|
|
1266
|
+
target.style.display = 'grid';
|
|
1267
|
+
}
|
|
1232
1268
|
}
|
|
1233
1269
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1270
|
+
// Slot Configs
|
|
1271
|
+
function getToolAvatar(profileKey, profile) {
|
|
1272
|
+
const k = (profileKey || '').toLowerCase();
|
|
1273
|
+
const n = (profile?.name || '').toLowerCase();
|
|
1274
|
+
const t = (profile?.tool || '').toLowerCase();
|
|
1275
|
+
const u = (profile?.baseURL || '').toLowerCase();
|
|
1276
|
+
|
|
1277
|
+
let iconFile = 'openai.png';
|
|
1278
|
+
let brandName = 'AI Provider';
|
|
1279
|
+
|
|
1280
|
+
if (k.includes('claude') || n.includes('claude') || t === 'claude' || t === 'anthropic' || u.includes('anthropic')) {
|
|
1281
|
+
iconFile = 'claude.png';
|
|
1282
|
+
brandName = 'Anthropic Claude';
|
|
1283
|
+
} else if (k.includes('codex') || n.includes('codex') || t === 'codex' || t === 'responses') {
|
|
1284
|
+
iconFile = 'codex.png';
|
|
1285
|
+
brandName = 'OpenAI Codex';
|
|
1286
|
+
} else if (k.includes('openai') || n.includes('openai') || u.includes('openai')) {
|
|
1287
|
+
iconFile = 'openai.png';
|
|
1288
|
+
brandName = 'OpenAI';
|
|
1289
|
+
} else if (k.includes('intact') || n.includes('intact')) {
|
|
1290
|
+
iconFile = 'intact.svg';
|
|
1291
|
+
brandName = 'Intact Gateway';
|
|
1292
|
+
} else if (k.includes('gemini') || n.includes('gemini') || k.includes('vertex') || n.includes('vertex') || u.includes('googleapis')) {
|
|
1293
|
+
iconFile = 'gemini.png';
|
|
1294
|
+
brandName = 'Google Gemini';
|
|
1295
|
+
} else if (k.includes('openrouter') || n.includes('openrouter') || u.includes('openrouter')) {
|
|
1296
|
+
iconFile = 'openrouter.png';
|
|
1297
|
+
brandName = 'OpenRouter';
|
|
1298
|
+
} else if (k.includes('local') || k.includes('ollama') || n.includes('ollama') || u.includes('11434')) {
|
|
1299
|
+
iconFile = 'ollama.png';
|
|
1300
|
+
brandName = 'Ollama';
|
|
1301
|
+
} else if (k.includes('antigravity') || n.includes('antigravity')) {
|
|
1302
|
+
iconFile = 'antigravity.png';
|
|
1303
|
+
brandName = 'Antigravity';
|
|
1304
|
+
} else if (k.includes('deepseek') || n.includes('deepseek')) {
|
|
1305
|
+
iconFile = 'deepseek.png';
|
|
1306
|
+
brandName = 'DeepSeek';
|
|
1307
|
+
} else if (k.includes('groq') || n.includes('groq')) {
|
|
1308
|
+
iconFile = 'groq.png';
|
|
1309
|
+
brandName = 'Groq';
|
|
1310
|
+
} else if (k.includes('qwen') || n.includes('qwen')) {
|
|
1311
|
+
iconFile = 'qwen.png';
|
|
1312
|
+
brandName = 'Qwen';
|
|
1313
|
+
} else if (k.includes('github') || n.includes('github')) {
|
|
1314
|
+
iconFile = 'github.png';
|
|
1315
|
+
brandName = 'GitHub Copilot';
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return '<span class="av logo" title="' + escapeHtml(brandName) + '"><img src="/icons/' + iconFile + '" alt="" loading="lazy"></span>';
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const SLOT_CONFIGS = {
|
|
1322
|
+
auto: {
|
|
1323
|
+
title: 'Universal Routing Slots',
|
|
1324
|
+
slots: [
|
|
1325
|
+
{ id: 'sonnet', name: 'Claude Sonnet', placeholder: 'claude-3-7-sonnet or antigravity/gemini-3.8-flash' },
|
|
1326
|
+
{ id: 'opus', name: 'Claude Opus', placeholder: 'claude-3-opus or antigravity/gemini-3.8-flash' },
|
|
1327
|
+
{ id: 'haiku', name: 'Claude Haiku', placeholder: 'claude-3-5-haiku or antigravity/gemini-3.8-flash' },
|
|
1328
|
+
{ id: 'fable', name: 'Claude Fable', placeholder: 'claude-fable-5-1 or antigravity/gemini-3.8-flash' },
|
|
1329
|
+
{ id: 'main', name: 'Codex Main Session', placeholder: 'gpt-5.6-sol or gpt-4o' },
|
|
1330
|
+
{ id: 'review', name: 'Codex /review Override', placeholder: 'gpt-5.6-terra or gpt-4o' },
|
|
1331
|
+
{ id: 'subagent', name: 'Codex Subagent', placeholder: 'gpt-5.6-luna or o3-mini' },
|
|
1332
|
+
{ id: 'default', name: 'Default Fallback Model', placeholder: 'gpt-4o' }
|
|
1333
|
+
]
|
|
1334
|
+
},
|
|
1335
|
+
claude: {
|
|
1336
|
+
title: 'Claude Code Model Tiers (Anthropic)',
|
|
1337
|
+
slots: [
|
|
1338
|
+
{ id: 'sonnet', name: 'Sonnet Slot', placeholder: 'claude-3-7-sonnet' },
|
|
1339
|
+
{ id: 'opus', name: 'Opus Slot', placeholder: 'claude-3-opus' },
|
|
1340
|
+
{ id: 'haiku', name: 'Haiku Slot', placeholder: 'claude-3-5-haiku' },
|
|
1341
|
+
{ id: 'fable', name: 'Fable Slot', placeholder: 'claude-fable-5-1' }
|
|
1342
|
+
]
|
|
1343
|
+
},
|
|
1344
|
+
codex: {
|
|
1345
|
+
title: 'OpenAI Codex Model Roles (Responses API)',
|
|
1346
|
+
slots: [
|
|
1347
|
+
{ id: 'main', name: 'Main Session Model', placeholder: 'gpt-5.6-sol' },
|
|
1348
|
+
{ id: 'review', name: 'Review Model', placeholder: 'gpt-5.6-terra' },
|
|
1349
|
+
{ id: 'subagent', name: 'Subagent Model', placeholder: 'gpt-5.6-luna' }
|
|
1350
|
+
]
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
function getActiveSlotConfig(fmt) {
|
|
1355
|
+
if (fmt === 'claude' || fmt === 'anthropic') return SLOT_CONFIGS.claude;
|
|
1356
|
+
if (fmt === 'codex' || fmt === 'responses') return SLOT_CONFIGS.codex;
|
|
1357
|
+
return SLOT_CONFIGS.auto;
|
|
1236
1358
|
}
|
|
1237
1359
|
|
|
1238
|
-
function valueForSlot(
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
for (const legacyKey of LEGACY_SLOT_KEYS[slot] || []) {
|
|
1242
|
-
if (source[legacyKey]) return source[legacyKey];
|
|
1243
|
-
}
|
|
1244
|
-
return '';
|
|
1360
|
+
function valueForSlot(defaultModels, slotId) {
|
|
1361
|
+
if (!defaultModels) return '';
|
|
1362
|
+
return defaultModels[slotId] || '';
|
|
1245
1363
|
}
|
|
1246
1364
|
|
|
1247
|
-
function flagForSlot(
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
for (const legacyKey of LEGACY_SLOT_KEYS[slot] || []) {
|
|
1251
|
-
if (Object.hasOwn(source, legacyKey)) return Boolean(source[legacyKey]);
|
|
1252
|
-
}
|
|
1253
|
-
return false;
|
|
1365
|
+
function flagForSlot(model1M, slotId) {
|
|
1366
|
+
if (!model1M) return false;
|
|
1367
|
+
return Boolean(model1M[slotId]);
|
|
1254
1368
|
}
|
|
1255
1369
|
|
|
1256
|
-
function renderModelSlots(
|
|
1257
|
-
const cfg = getActiveSlotConfig(format);
|
|
1258
|
-
const heading = document.getElementById('model-slot-heading');
|
|
1259
|
-
const warnText = document.getElementById('warn-1m-text');
|
|
1260
|
-
const warnIcon = document.getElementById('warn-1m-icon');
|
|
1370
|
+
function renderModelSlots(slotFmt, p) {
|
|
1261
1371
|
const container = document.getElementById('slot-fields');
|
|
1262
|
-
|
|
1263
|
-
if (heading) heading.textContent = cfg.heading;
|
|
1264
|
-
if (warnText) warnText.innerHTML = cfg.warning;
|
|
1265
|
-
if (warnIcon) warnIcon.textContent = cfg.icon;
|
|
1372
|
+
const heading = document.getElementById('model-slot-heading');
|
|
1266
1373
|
if (!container) return;
|
|
1267
1374
|
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
</div>
|
|
1287
|
-
<input type="text" id="p-${s.id}" list="model-options" placeholder="${escapeHtml(s.placeholder)}" value="${escapeHtml(val)}">
|
|
1375
|
+
const cfg = getActiveSlotConfig(slotFmt);
|
|
1376
|
+
renderedSlotFormat = slotFmt;
|
|
1377
|
+
if (heading) heading.textContent = cfg.title;
|
|
1378
|
+
container.innerHTML = '';
|
|
1379
|
+
|
|
1380
|
+
for (const s of cfg.slots) {
|
|
1381
|
+
const val = currentProfileBuffer ? valueForSlot(currentProfileBuffer.defaultModels, s.id) : valueForSlot(p?.defaultModels, s.id);
|
|
1382
|
+
const checked = currentProfileBuffer ? flagForSlot(currentProfileBuffer.model1M, s.id) : flagForSlot(p?.model1M, s.id);
|
|
1383
|
+
|
|
1384
|
+
const slotRow = document.createElement('div');
|
|
1385
|
+
slotRow.style.cssText = 'display:flex; flex-direction:column; gap:0.25rem;';
|
|
1386
|
+
slotRow.innerHTML = `
|
|
1387
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
1388
|
+
<label for="p-${s.id}" style="font-size:0.82rem; font-weight:500;">${escapeHtml(s.name)}</label>
|
|
1389
|
+
<label style="display:flex; align-items:center; gap:0.35rem; font-size:0.75rem; color:var(--accent); cursor:pointer;">
|
|
1390
|
+
<input type="checkbox" id="p-${s.id}-1m" style="width:13px; height:13px; min-height:0;" onchange="update1mWarnings()" ${checked ? 'checked' : ''}>
|
|
1391
|
+
1M context
|
|
1392
|
+
</label>
|
|
1288
1393
|
</div>
|
|
1394
|
+
<input type="text" id="p-${s.id}" list="model-options" placeholder="${escapeHtml(s.placeholder)}" value="${escapeHtml(val)}">
|
|
1289
1395
|
`;
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
renderedSlotFormat = format;
|
|
1396
|
+
container.appendChild(slotRow);
|
|
1397
|
+
}
|
|
1293
1398
|
update1mWarnings();
|
|
1294
1399
|
}
|
|
1295
1400
|
|
|
1296
|
-
function onToolChange(
|
|
1297
|
-
|
|
1298
|
-
renderModelSlots(
|
|
1299
|
-
renderCodexTab(newFmt);
|
|
1300
|
-
if (loadedModels.length > 0) renderModelBrowser(loadedModels);
|
|
1301
|
-
formDirty = true;
|
|
1401
|
+
function onToolChange() {
|
|
1402
|
+
const toolVal = document.getElementById('p-tool').value;
|
|
1403
|
+
renderModelSlots(toolVal, currentProfileBuffer);
|
|
1302
1404
|
}
|
|
1303
1405
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1406
|
+
function collectCurrentSlotValues() {
|
|
1407
|
+
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1408
|
+
if (!currentProfileBuffer) currentProfileBuffer = {};
|
|
1409
|
+
const defaultModels = { ...(currentProfileBuffer.defaultModels || {}) };
|
|
1410
|
+
const model1M = { ...(currentProfileBuffer.model1M || {}) };
|
|
1411
|
+
for (const s of cfg.slots) {
|
|
1412
|
+
const inp = document.getElementById('p-' + s.id);
|
|
1413
|
+
const chk = document.getElementById('p-' + s.id + '-1m');
|
|
1414
|
+
if (inp) {
|
|
1415
|
+
const val = inp.value.trim();
|
|
1416
|
+
if (val) defaultModels[s.id] = val;
|
|
1417
|
+
else delete defaultModels[s.id];
|
|
1418
|
+
}
|
|
1419
|
+
if (chk) {
|
|
1420
|
+
if (chk.checked) model1M[s.id] = true;
|
|
1421
|
+
else delete model1M[s.id];
|
|
1422
|
+
}
|
|
1316
1423
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const published = Array.isArray(p.publicModels) ? p.publicModels : [];
|
|
1321
|
-
CODEX_ROLE_SLOTS.forEach((slot, i) => {
|
|
1322
|
-
const input = document.getElementById(`p-public-${slot}`);
|
|
1323
|
-
// A saved role key wins even when blank; the position fallback is for profiles without codexRoles.
|
|
1324
|
-
if (input) input.value = Object.hasOwn(roles, slot) ? (roles[slot] || '') : (published[i] || '');
|
|
1325
|
-
});
|
|
1326
|
-
|
|
1424
|
+
currentProfileBuffer.defaultModels = defaultModels;
|
|
1425
|
+
currentProfileBuffer.model1M = model1M;
|
|
1426
|
+
return { defaultModels, model1M };
|
|
1327
1427
|
}
|
|
1328
1428
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
const value = document.getElementById(`p-public-${slot}`)?.value.trim() || '';
|
|
1339
|
-
roles[slot] = value;
|
|
1340
|
-
if (value && !published.includes(value)) published.push(value);
|
|
1341
|
-
});
|
|
1342
|
-
profile.codexRoles = roles;
|
|
1343
|
-
profile.publicModels = published;
|
|
1344
|
-
|
|
1429
|
+
function update1mWarnings() {
|
|
1430
|
+
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1431
|
+
let any1m = false;
|
|
1432
|
+
for (const s of cfg.slots) {
|
|
1433
|
+
const chk = document.getElementById('p-' + s.id + '-1m');
|
|
1434
|
+
if (chk && chk.checked) any1m = true;
|
|
1435
|
+
}
|
|
1436
|
+
const warnCallout = document.getElementById('warn-1m-callout');
|
|
1437
|
+
if (warnCallout) warnCallout.style.display = any1m ? 'inline-flex' : 'none';
|
|
1345
1438
|
}
|
|
1346
1439
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1440
|
+
// Prefetch Counts for Nav Badges
|
|
1441
|
+
async function prefetchNavData() {
|
|
1442
|
+
try {
|
|
1443
|
+
const res = await api('/api/catalog');
|
|
1444
|
+
if (res.ok) {
|
|
1445
|
+
const catalog = await res.json();
|
|
1446
|
+
cachedCatalog = catalog;
|
|
1447
|
+
const claudeModels = Array.isArray(catalog.claude?.models) ? catalog.claude.models : [];
|
|
1448
|
+
const codexModels = Array.isArray(catalog.codex?.models) ? catalog.codex.models : [];
|
|
1449
|
+
const count = claudeModels.length + codexModels.length;
|
|
1450
|
+
const ctModels = document.getElementById('ct-models');
|
|
1451
|
+
if (ctModels && count > 0) ctModels.textContent = count;
|
|
1452
|
+
}
|
|
1453
|
+
} catch {}
|
|
1360
1454
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1455
|
+
try {
|
|
1456
|
+
const res = await api('/api/logs');
|
|
1457
|
+
if (res.ok) {
|
|
1458
|
+
const data = await res.json();
|
|
1459
|
+
const logs = Array.isArray(data.logs) ? data.logs : [];
|
|
1460
|
+
lastLogsCount = logs.length;
|
|
1461
|
+
const ctLogs = document.getElementById('ct-logs');
|
|
1462
|
+
if (ctLogs && lastLogsCount > 0) ctLogs.textContent = lastLogsCount;
|
|
1369
1463
|
}
|
|
1370
|
-
}
|
|
1464
|
+
} catch {}
|
|
1371
1465
|
}
|
|
1372
1466
|
|
|
1467
|
+
// Load Status
|
|
1373
1468
|
async function loadStatus() {
|
|
1374
1469
|
try {
|
|
1375
1470
|
const res = await api('/api/status');
|
|
1471
|
+
if (!res.ok) return;
|
|
1376
1472
|
const data = await res.json();
|
|
1377
|
-
|
|
1378
|
-
document.getElementById('status-title').textContent = 'Dashboard locked';
|
|
1379
|
-
document.getElementById('status-sub').textContent = 'Run `switch ui` in a terminal. It opens this page with its access token.';
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
if (!res.ok) {
|
|
1383
|
-
showToast(data.error || `Gateway error HTTP ${res.status}`, true);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
state = data;
|
|
1387
|
-
document.querySelectorAll('.gateway-port').forEach(el => { el.textContent = state.port; });
|
|
1388
|
-
if (!creatingNew && (!currentlyEditingKey || !state.config.profiles[currentlyEditingKey])) {
|
|
1389
|
-
// Pick the first profile that is actually active for any CLI target,
|
|
1390
|
-
// falling back to the legacy activeProfile or the first profile key.
|
|
1391
|
-
const activeMap = state.activeProfiles || {};
|
|
1392
|
-
const firstActiveKey = Object.values(activeMap).find(k => k && state.config.profiles[k]);
|
|
1393
|
-
currentlyEditingKey = firstActiveKey
|
|
1394
|
-
|| (state.config.profiles[state.activeProfile] ? state.activeProfile : null)
|
|
1395
|
-
|| (Object.keys(state.config.profiles)[0] || '');
|
|
1396
|
-
formDirty = false;
|
|
1397
|
-
}
|
|
1473
|
+
state = { ...state, ...data };
|
|
1398
1474
|
render();
|
|
1475
|
+
updateHeader(window.location.hash.replace(/^#\/?/, '').split('/')[0] || 'routes');
|
|
1476
|
+
prefetchNavData();
|
|
1399
1477
|
} catch (err) {
|
|
1400
|
-
|
|
1478
|
+
console.error('Failed to load status', err);
|
|
1401
1479
|
}
|
|
1402
1480
|
}
|
|
1403
1481
|
|
|
1404
|
-
function isAnyTargetActive() {
|
|
1405
|
-
return Object.values(state.activeProfiles || {}).some(Boolean);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
1482
|
function render() {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const toggleBtn = document.getElementById('btn-toggle');
|
|
1483
|
+
document.querySelectorAll('.gateway-port').forEach(el => {
|
|
1484
|
+
el.textContent = state.port || '3456';
|
|
1485
|
+
});
|
|
1413
1486
|
|
|
1414
|
-
const activeMap = state.activeProfiles || (state.config
|
|
1487
|
+
const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
|
|
1415
1488
|
const activeTargets = Object.entries(activeMap).filter(([_, v]) => Boolean(v));
|
|
1416
1489
|
const isAnyActive = activeTargets.length > 0;
|
|
1417
1490
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1491
|
+
// Update Aside Indicators
|
|
1492
|
+
const asideIndicator = document.getElementById('status-aside-indicator');
|
|
1493
|
+
if (asideIndicator) {
|
|
1494
|
+
asideIndicator.className = isAnyActive ? 'status on' : 'status';
|
|
1495
|
+
asideIndicator.textContent = isAnyActive ? 'Active (' + activeTargets.length + ')' : 'Idle';
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Update Nav Counts
|
|
1499
|
+
const profilesList = Object.entries(state.config?.profiles || {});
|
|
1500
|
+
const ctRoutes = document.getElementById('ct-routes');
|
|
1501
|
+
if (ctRoutes) ctRoutes.textContent = isAnyActive ? activeTargets.length + ' on' : profilesList.length;
|
|
1502
|
+
|
|
1503
|
+
const ctModels = document.getElementById('ct-models');
|
|
1504
|
+
if (ctModels && cachedCatalog) {
|
|
1505
|
+
const count = (cachedCatalog.claude?.models?.length || 0) + (cachedCatalog.codex?.models?.length || 0);
|
|
1506
|
+
if (count > 0) ctModels.textContent = count;
|
|
1433
1507
|
}
|
|
1434
1508
|
|
|
1509
|
+
const ctLogs = document.getElementById('ct-logs');
|
|
1510
|
+
if (ctLogs && lastLogsCount > 0) ctLogs.textContent = lastLogsCount;
|
|
1511
|
+
|
|
1512
|
+
// Update Blindfold display
|
|
1435
1513
|
const bfEl = document.getElementById('blindfold-port-display');
|
|
1436
1514
|
if (bfEl) {
|
|
1437
1515
|
const bf = state.config?.blindfold;
|
|
1438
|
-
bfEl.textContent = (bf && bf.port)
|
|
1439
|
-
? `port ${bf.port} — host ${bf.host || 'api.anthropic.com, api.openai.com, chatgpt.com'}, prefix ${bf.prefix || 'default'}`
|
|
1440
|
-
: 'not configured (the tools reach the provider directly)';
|
|
1516
|
+
bfEl.textContent = (bf && bf.port) ? 'Port ' + bf.port : 'Not active';
|
|
1441
1517
|
}
|
|
1442
1518
|
|
|
1443
|
-
//
|
|
1519
|
+
// Render CLI Targets Rows
|
|
1444
1520
|
const slots = [
|
|
1445
1521
|
{ id: 'claude', name: 'Claude Code', key: 'claude' },
|
|
1446
1522
|
{ id: 'codex', name: 'Codex', key: 'codex' }
|
|
1447
1523
|
];
|
|
1448
1524
|
|
|
1449
1525
|
for (const slot of slots) {
|
|
1450
|
-
const
|
|
1451
|
-
const
|
|
1452
|
-
const select = document.getElementById(`select-slot-${slot.id}`);
|
|
1526
|
+
const badge = document.getElementById('badge-slot-' + slot.id);
|
|
1527
|
+
const select = document.getElementById('select-slot-' + slot.id);
|
|
1453
1528
|
const currentActive = activeMap[slot.key];
|
|
1454
1529
|
|
|
1455
1530
|
if (select) {
|
|
1456
1531
|
select.innerHTML = '<option value="">Official endpoint</option>';
|
|
1457
|
-
for (const [key, p] of
|
|
1532
|
+
for (const [key, p] of profilesList) {
|
|
1458
1533
|
const tool = p.tool || 'auto';
|
|
1459
1534
|
if (tool === 'auto' || tool === slot.key) {
|
|
1460
1535
|
const opt = document.createElement('option');
|
|
1461
1536
|
opt.value = key;
|
|
1462
|
-
opt.textContent =
|
|
1537
|
+
opt.textContent = p.name + ' (' + key + ')';
|
|
1463
1538
|
select.appendChild(opt);
|
|
1464
1539
|
}
|
|
1465
1540
|
}
|
|
1466
1541
|
select.value = currentActive || '';
|
|
1467
1542
|
}
|
|
1468
1543
|
|
|
1469
|
-
if (
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
badge.className = 'badge badge-direct';
|
|
1544
|
+
if (badge) {
|
|
1545
|
+
if (currentActive) {
|
|
1546
|
+
badge.className = 'badge ok';
|
|
1473
1547
|
badge.textContent = currentActive;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
if (card) card.classList.remove('active');
|
|
1477
|
-
if (badge) {
|
|
1478
|
-
badge.className = 'badge badge-outline';
|
|
1548
|
+
} else {
|
|
1549
|
+
badge.className = 'badge outline';
|
|
1479
1550
|
badge.textContent = 'OFF';
|
|
1480
1551
|
}
|
|
1481
1552
|
}
|
|
1482
1553
|
}
|
|
1483
1554
|
|
|
1484
|
-
// Render
|
|
1555
|
+
// Render Profiles Grid (Intact Style)
|
|
1485
1556
|
const grid = document.getElementById('profiles-grid');
|
|
1557
|
+
if (!grid) return;
|
|
1486
1558
|
grid.innerHTML = '';
|
|
1487
1559
|
|
|
1488
|
-
for (const [key, p] of
|
|
1560
|
+
for (const [key, p] of profilesList) {
|
|
1489
1561
|
const activeCLIs = [];
|
|
1490
1562
|
if (activeMap.claude === key) activeCLIs.push('Claude');
|
|
1491
1563
|
if (activeMap.codex === key) activeCLIs.push('Codex');
|
|
1492
1564
|
const isActiveAny = activeCLIs.length > 0;
|
|
1493
1565
|
const isEditing = (key === currentlyEditingKey);
|
|
1494
|
-
const shortFmt = (f) => ({ auto: 'auto', claude: 'claude', codex: 'codex' }[f] || f || 'auto');
|
|
1495
|
-
const routeBadge = `${shortFmt(p.tool || 'auto')}→${shortFmt(p.outFormat || p.mode)}`;
|
|
1496
1566
|
|
|
1497
1567
|
const card = document.createElement('div');
|
|
1498
|
-
card.className =
|
|
1499
|
-
card.setAttribute('aria-label',
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
if (ev.target.closest('.card-toggle') || ev.target.closest('input') || ev.target.closest('button')) return;
|
|
1504
|
-
selectProfileForEdit(key);
|
|
1505
|
-
});
|
|
1568
|
+
card.className = 'pcard' + (isActiveAny ? ' active' : '');
|
|
1569
|
+
card.setAttribute('aria-label', p.name + ' profile');
|
|
1570
|
+
|
|
1571
|
+
const toolAv = getToolAvatar(key, p);
|
|
1572
|
+
const routeBadge = (p.tool || 'auto') + ' → ' + (p.outFormat || p.mode || 'chat');
|
|
1506
1573
|
|
|
1507
1574
|
const slotFmt = p.tool === 'claude' ? 'anthropic' : p.tool === 'codex' ? 'responses' : 'auto';
|
|
1508
1575
|
const pCfg = getActiveSlotConfig(slotFmt);
|
|
1509
|
-
const
|
|
1510
|
-
const val = valueForSlot(p.defaultModels, s.id)
|
|
1576
|
+
const modelChips = pCfg.slots.map(s => {
|
|
1577
|
+
const val = valueForSlot(p.defaultModels, s.id);
|
|
1578
|
+
if (!val) return '';
|
|
1511
1579
|
const has1m = flagForSlot(p.model1M, s.id);
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
}).join('');
|
|
1580
|
+
return '<div class="chip"><span class="faint lbl">' + escapeHtml(s.name) + '</span><span class="val">' + escapeHtml(val) + '</span>' + (has1m ? '<b class="b1m">1M</b>' : '') + '</div>';
|
|
1581
|
+
}).filter(Boolean).join('');
|
|
1515
1582
|
|
|
1516
1583
|
card.innerHTML = `
|
|
1517
|
-
<div class="
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
<
|
|
1521
|
-
<
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
<label class="switch">
|
|
1526
|
-
<input type="checkbox" aria-label="Activate ${escapeHtml(p.name)} for compatible clients" ${isActiveAny ? 'checked' : ''}>
|
|
1527
|
-
<span class="slider"></span>
|
|
1528
|
-
</label>
|
|
1584
|
+
<div class="top">
|
|
1585
|
+
${toolAv}
|
|
1586
|
+
<div style="flex:1; min-width:0;">
|
|
1587
|
+
<div class="nm">${escapeHtml(p.name)}</div>
|
|
1588
|
+
<div style="display:flex; gap:0.4rem; align-items:center; flex-wrap:wrap; margin-top:0.15rem;">
|
|
1589
|
+
<span class="status ${isActiveAny ? 'on' : ''}">${isActiveAny ? 'Active (' + activeCLIs.join(', ') + ')' : 'Idle'}</span>
|
|
1590
|
+
<span class="badge accent">${escapeHtml(routeBadge)}</span>
|
|
1591
|
+
</div>
|
|
1529
1592
|
</div>
|
|
1593
|
+
<label class="switch" onclick="event.stopPropagation()">
|
|
1594
|
+
<input type="checkbox" ${isActiveAny ? 'checked' : ''} onchange="toggleProfileAllCli('${escapeHtml(key)}', this.checked)">
|
|
1595
|
+
<span></span>
|
|
1596
|
+
</label>
|
|
1530
1597
|
</div>
|
|
1531
|
-
<div class="
|
|
1532
|
-
|
|
1533
|
-
${modelsSummaryHtml}
|
|
1598
|
+
<div class="copyline" onclick="event.stopPropagation()">
|
|
1599
|
+
<code>${escapeHtml(p.baseURL)}</code>
|
|
1534
1600
|
</div>
|
|
1535
|
-
<div class="
|
|
1536
|
-
|
|
1537
|
-
|
|
1601
|
+
${modelChips ? '<div class="chips">' + modelChips + '</div>' : ''}
|
|
1602
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:auto; padding-top:0.5rem; border-top:1px solid var(--line);">
|
|
1603
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="selectProfileForEdit('${escapeHtml(key)}')">
|
|
1604
|
+
${isEditing ? 'Editing Profile' : 'Edit Profile'}
|
|
1538
1605
|
</button>
|
|
1539
|
-
<
|
|
1540
|
-
${activeCLIs.map(c => `<span class="badge badge-direct" style="font-size: 9px; margin-left: 3px;">${escapeHtml(c)}</span>`).join('')}
|
|
1541
|
-
</div>
|
|
1606
|
+
<span class="mono faint" style="font-size:0.75rem;">${escapeHtml(key)}</span>
|
|
1542
1607
|
</div>
|
|
1543
1608
|
`;
|
|
1544
1609
|
|
|
1545
|
-
const toggleWrap = card.querySelector('.card-toggle');
|
|
1546
|
-
toggleWrap.addEventListener('click', ev => ev.stopPropagation());
|
|
1547
|
-
toggleWrap.querySelector('input').addEventListener('change', ev => toggleProfileAllCli(key, ev.target.checked));
|
|
1548
1610
|
grid.appendChild(card);
|
|
1549
1611
|
}
|
|
1550
1612
|
|
|
@@ -1554,15 +1616,16 @@
|
|
|
1554
1616
|
}
|
|
1555
1617
|
|
|
1556
1618
|
function selectProfileForEdit(key) {
|
|
1557
|
-
if (formDirty && key !== formLoadedKey && !confirm('Discard unsaved changes
|
|
1619
|
+
if (formDirty && key !== formLoadedKey && !confirm('Discard unsaved changes?')) return;
|
|
1558
1620
|
currentlyEditingKey = key;
|
|
1559
1621
|
creatingNew = false;
|
|
1560
1622
|
formDirty = false;
|
|
1561
1623
|
render();
|
|
1624
|
+
openProfileModal(false);
|
|
1562
1625
|
}
|
|
1563
1626
|
|
|
1564
1627
|
async function switchCliTarget(target, profileKey) {
|
|
1565
|
-
showToast(
|
|
1628
|
+
showToast('Setting ' + target + ' target...');
|
|
1566
1629
|
try {
|
|
1567
1630
|
const res = await api('/api/switch', {
|
|
1568
1631
|
method: 'POST',
|
|
@@ -1570,401 +1633,241 @@
|
|
|
1570
1633
|
body: JSON.stringify({ target, profile: profileKey || null })
|
|
1571
1634
|
});
|
|
1572
1635
|
const data = await res.json();
|
|
1573
|
-
if (data.
|
|
1574
|
-
showToast(
|
|
1575
|
-
loadStatus();
|
|
1636
|
+
if (data.ok) {
|
|
1637
|
+
showToast('Updated ' + target + ' routing');
|
|
1638
|
+
await loadStatus();
|
|
1576
1639
|
} else {
|
|
1577
|
-
showToast(data.error || 'Failed to switch', true);
|
|
1640
|
+
showToast(data.error || 'Failed to switch target', true);
|
|
1578
1641
|
}
|
|
1579
1642
|
} catch (err) {
|
|
1580
|
-
showToast('
|
|
1643
|
+
showToast('Network error switching target', true);
|
|
1581
1644
|
}
|
|
1582
1645
|
}
|
|
1583
1646
|
|
|
1584
|
-
async function toggleProfileAllCli(key,
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
showToast(data.error || 'Failed to switch', true);
|
|
1599
|
-
loadStatus();
|
|
1600
|
-
}
|
|
1601
|
-
} catch (err) {
|
|
1602
|
-
showToast('Error activating provider', true);
|
|
1603
|
-
loadStatus();
|
|
1604
|
-
}
|
|
1605
|
-
} else {
|
|
1606
|
-
showToast(`Deactivating provider "${key}"...`);
|
|
1607
|
-
try {
|
|
1608
|
-
const res = await api('/api/switch', {
|
|
1609
|
-
method: 'POST',
|
|
1610
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1611
|
-
body: JSON.stringify({ deactivate: key })
|
|
1612
|
-
});
|
|
1613
|
-
const data = await res.json();
|
|
1614
|
-
if (!data.success) throw new Error(data.error || 'Failed');
|
|
1615
|
-
showToast(`Deactivated "${key}"!`);
|
|
1616
|
-
loadStatus();
|
|
1617
|
-
} catch (err) {
|
|
1618
|
-
showToast('Error deactivating provider', true);
|
|
1619
|
-
loadStatus();
|
|
1647
|
+
async function toggleProfileAllCli(key, enable) {
|
|
1648
|
+
showToast((enable ? 'Activating ' : 'Deactivating ') + key + '...');
|
|
1649
|
+
try {
|
|
1650
|
+
const res = await api('/api/switch', {
|
|
1651
|
+
method: 'POST',
|
|
1652
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1653
|
+
body: JSON.stringify({ profile: key, deactivate: !enable })
|
|
1654
|
+
});
|
|
1655
|
+
const data = await res.json();
|
|
1656
|
+
if (data.ok) {
|
|
1657
|
+
showToast(key + (enable ? ' activated' : ' deactivated'));
|
|
1658
|
+
await loadStatus();
|
|
1659
|
+
} else {
|
|
1660
|
+
showToast(data.error || 'Failed to toggle profile', true);
|
|
1620
1661
|
}
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
showToast('Network error toggling profile', true);
|
|
1621
1664
|
}
|
|
1622
1665
|
}
|
|
1623
1666
|
|
|
1624
1667
|
function loadProfileToForm(key) {
|
|
1625
1668
|
const p = state.config?.profiles?.[key];
|
|
1626
1669
|
if (!p) return;
|
|
1670
|
+
formLoadedKey = key;
|
|
1627
1671
|
currentProfileBuffer = JSON.parse(JSON.stringify(p));
|
|
1628
1672
|
|
|
1629
|
-
document.getElementById('form-title').
|
|
1673
|
+
document.getElementById('form-title').textContent = 'Edit Profile: ' + p.name;
|
|
1630
1674
|
document.getElementById('p-key').value = key;
|
|
1631
|
-
document.getElementById('p-key').
|
|
1632
|
-
document.getElementById('p-name').value = p.name;
|
|
1633
|
-
document.getElementById('p-
|
|
1634
|
-
|
|
1635
|
-
|
|
1675
|
+
document.getElementById('p-key').readOnly = true;
|
|
1676
|
+
document.getElementById('p-name').value = p.name || '';
|
|
1677
|
+
document.getElementById('p-url').value = p.baseURL || '';
|
|
1678
|
+
document.getElementById('p-key-val').value = p.apiKey ? MASKED_KEY : '';
|
|
1679
|
+
const toolVal = p.tool || 'auto';
|
|
1680
|
+
document.getElementById('p-tool').value = toolVal;
|
|
1681
|
+
document.getElementById('p-outformat').value = p.outFormat || 'openai-chat';
|
|
1682
|
+
document.getElementById('p-mode').value = p.mode || 'convert';
|
|
1636
1683
|
document.getElementById('p-thinking-mode').value = p.thinkingMode || 'auto';
|
|
1637
|
-
document.getElementById('
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
document.getElementById('model-browser').style.display = 'none';
|
|
1649
|
-
document.getElementById('test-result').style.display = 'none';
|
|
1650
|
-
loadedModels = [];
|
|
1651
|
-
formLoadedKey = key;
|
|
1652
|
-
formDirty = false;
|
|
1684
|
+
document.getElementById('btn-delete').style.display = 'inline-flex';
|
|
1685
|
+
|
|
1686
|
+
const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
|
|
1687
|
+
const activeTargets = Object.entries(activeMap).filter(([_, v]) => v === key).map(([t]) => t);
|
|
1688
|
+
const activeTag = document.getElementById('active-tag');
|
|
1689
|
+
if (activeTag) {
|
|
1690
|
+
activeTag.textContent = activeTargets.length ? '● Active: ' + activeTargets.join(', ') : '';
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
renderModelSlots(toolVal, p);
|
|
1653
1694
|
}
|
|
1654
1695
|
|
|
1655
1696
|
function initNewProfile() {
|
|
1656
|
-
if (formDirty && !confirm('Discard unsaved changes to this provider?')) return;
|
|
1657
|
-
currentlyEditingKey = '';
|
|
1658
1697
|
creatingNew = true;
|
|
1698
|
+
currentlyEditingKey = '';
|
|
1659
1699
|
formLoadedKey = null;
|
|
1660
|
-
formDirty = false;
|
|
1661
1700
|
currentProfileBuffer = {
|
|
1662
1701
|
name: '',
|
|
1663
|
-
|
|
1664
|
-
tool:
|
|
1665
|
-
outFormat: '',
|
|
1702
|
+
baseURL: '',
|
|
1703
|
+
tool: 'auto',
|
|
1704
|
+
outFormat: 'openai-chat',
|
|
1705
|
+
mode: 'convert',
|
|
1666
1706
|
thinkingMode: 'auto',
|
|
1667
|
-
baseURL: 'https://',
|
|
1668
|
-
apiKey: '',
|
|
1669
1707
|
defaultModels: {},
|
|
1670
1708
|
model1M: {}
|
|
1671
1709
|
};
|
|
1672
1710
|
|
|
1673
|
-
|
|
1674
|
-
document.getElementById('form-title').innerText = 'Create New Provider';
|
|
1711
|
+
document.getElementById('form-title').textContent = 'Add New Provider Profile';
|
|
1675
1712
|
document.getElementById('p-key').value = '';
|
|
1676
|
-
document.getElementById('p-key').
|
|
1713
|
+
document.getElementById('p-key').readOnly = false;
|
|
1677
1714
|
document.getElementById('p-name').value = '';
|
|
1678
|
-
document.getElementById('p-
|
|
1715
|
+
document.getElementById('p-url').value = '';
|
|
1716
|
+
document.getElementById('p-key-val').value = '';
|
|
1679
1717
|
document.getElementById('p-tool').value = 'auto';
|
|
1680
|
-
document.getElementById('p-outformat').value = '';
|
|
1718
|
+
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1719
|
+
document.getElementById('p-mode').value = 'convert';
|
|
1681
1720
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
1682
|
-
document.getElementById('p-url').value = 'https://';
|
|
1683
|
-
document.getElementById('p-key-val').value = '';
|
|
1684
|
-
|
|
1685
|
-
renderModelSlots('auto');
|
|
1686
|
-
renderCodexTab('auto');
|
|
1687
|
-
|
|
1688
1721
|
document.getElementById('btn-delete').style.display = 'none';
|
|
1689
|
-
document.getElementById('active-tag').
|
|
1690
|
-
document.getElementById('model-browser').style.display = 'none';
|
|
1691
|
-
document.getElementById('test-result').style.display = 'none';
|
|
1692
|
-
loadedModels = [];
|
|
1693
|
-
switchTab('tab-general');
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function setKeyIfNew(k) {
|
|
1697
|
-
const input = document.getElementById('p-key');
|
|
1698
|
-
if (!input.disabled) input.value = k;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function applyTemplate(type) {
|
|
1702
|
-
if (type === '9router') {
|
|
1703
|
-
setKeyIfNew('9router');
|
|
1704
|
-
document.getElementById('p-name').value = '9Router Cloud';
|
|
1705
|
-
document.getElementById('p-mode').value = 'convert';
|
|
1706
|
-
document.getElementById('p-tool').value = 'auto';
|
|
1707
|
-
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1708
|
-
document.getElementById('p-url').value = 'https://api.9router.com/v1';
|
|
1709
|
-
document.getElementById('p-thinking-mode').value = 'auto';
|
|
1710
|
-
currentProfileBuffer = {
|
|
1711
|
-
defaultModels: {
|
|
1712
|
-
opus: 'ag/claude-opus-4-6-thinking',
|
|
1713
|
-
sonnet: 'ag/claude-sonnet-4-6',
|
|
1714
|
-
haiku: 'ag/gemini-3.7-flash-high',
|
|
1715
|
-
fable: 'ag/gemini-3.8-flash-high'
|
|
1716
|
-
},
|
|
1717
|
-
model1M: { opus: true, sonnet: true, haiku: false, fable: true }
|
|
1718
|
-
};
|
|
1719
|
-
renderModelSlots('auto');
|
|
1720
|
-
} else if (type === 'openrouter') {
|
|
1721
|
-
setKeyIfNew('openrouter');
|
|
1722
|
-
document.getElementById('p-name').value = 'OpenRouter';
|
|
1723
|
-
document.getElementById('p-mode').value = 'convert';
|
|
1724
|
-
document.getElementById('p-tool').value = 'auto';
|
|
1725
|
-
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1726
|
-
document.getElementById('p-url').value = 'https://openrouter.ai/api/v1';
|
|
1727
|
-
currentProfileBuffer = {
|
|
1728
|
-
defaultModels: {
|
|
1729
|
-
opus: 'anthropic/claude-3.7-sonnet:thinking',
|
|
1730
|
-
sonnet: 'anthropic/claude-3.7-sonnet',
|
|
1731
|
-
haiku: 'google/gemini-2.0-flash-001',
|
|
1732
|
-
fable: 'qwen/qwq-32b'
|
|
1733
|
-
},
|
|
1734
|
-
model1M: { opus: true, sonnet: true, haiku: false, fable: false }
|
|
1735
|
-
};
|
|
1736
|
-
renderModelSlots('auto');
|
|
1737
|
-
} else if (type === 'codex-vtx') {
|
|
1738
|
-
setKeyIfNew('codex-vertex');
|
|
1739
|
-
document.getElementById('p-name').value = 'Codex to Vertex';
|
|
1740
|
-
document.getElementById('p-mode').value = 'convert';
|
|
1741
|
-
document.getElementById('p-tool').value = 'responses';
|
|
1742
|
-
document.getElementById('p-outformat').value = 'vertex';
|
|
1743
|
-
document.getElementById('p-url').value = 'https://api.9router.com/v1';
|
|
1744
|
-
document.getElementById('p-thinking-mode').value = 'auto';
|
|
1745
|
-
currentProfileBuffer = {
|
|
1746
|
-
defaultModels: {
|
|
1747
|
-
main: 'gemini-3.8-flash',
|
|
1748
|
-
review: 'gemini-3.7-flash-medium',
|
|
1749
|
-
subagent: 'gemini-3.6-flash-low'
|
|
1750
|
-
},
|
|
1751
|
-
model1M: { main: true, review: false, subagent: false }
|
|
1752
|
-
};
|
|
1753
|
-
renderModelSlots('responses');
|
|
1754
|
-
} else if (type === 'momo') {
|
|
1755
|
-
setKeyIfNew('selfhost');
|
|
1756
|
-
document.getElementById('p-name').value = 'Selfhost Hybrid';
|
|
1757
|
-
document.getElementById('p-mode').value = 'hybrid';
|
|
1758
|
-
document.getElementById('p-tool').value = 'auto';
|
|
1759
|
-
document.getElementById('p-outformat').value = '';
|
|
1760
|
-
document.getElementById('p-url').value = 'https://llm.example.com/v1';
|
|
1761
|
-
currentProfileBuffer = {
|
|
1762
|
-
defaultModels: {
|
|
1763
|
-
opus: 'claude-opus-4-7',
|
|
1764
|
-
sonnet: 'claude-sonnet-4-6',
|
|
1765
|
-
haiku: 'claude-haiku-4-5-20251001',
|
|
1766
|
-
fable: 'claude-haiku-4-5-20251001'
|
|
1767
|
-
},
|
|
1768
|
-
model1M: { opus: true, sonnet: true, haiku: false, fable: false }
|
|
1769
|
-
};
|
|
1770
|
-
renderModelSlots('auto');
|
|
1771
|
-
} else if (type === 'custom') {
|
|
1772
|
-
setKeyIfNew('local-openai');
|
|
1773
|
-
document.getElementById('p-name').value = 'Local OpenAI Server';
|
|
1774
|
-
document.getElementById('p-mode').value = 'convert';
|
|
1775
|
-
document.getElementById('p-tool').value = 'auto';
|
|
1776
|
-
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1777
|
-
document.getElementById('p-url').value = 'http://127.0.0.1:8000/v1';
|
|
1778
|
-
document.getElementById('p-thinking-mode').value = 'native';
|
|
1779
|
-
currentProfileBuffer = {
|
|
1780
|
-
defaultModels: { opus: 'gpt-4o' },
|
|
1781
|
-
model1M: { opus: false }
|
|
1782
|
-
};
|
|
1783
|
-
renderModelSlots('auto');
|
|
1784
|
-
}
|
|
1785
|
-
formDirty = true;
|
|
1786
|
-
showToast(`Applied ${type} template!`);
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
async function fetchModelList() {
|
|
1790
|
-
const baseURL = document.getElementById('p-url').value.trim();
|
|
1791
|
-
const apiKey = document.getElementById('p-key-val').value.trim();
|
|
1792
|
-
if (!baseURL) {
|
|
1793
|
-
showToast('Please enter Base URL first', true);
|
|
1794
|
-
switchTab('tab-general');
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
const btn = document.getElementById('btn-fetch-models');
|
|
1799
|
-
btn.innerText = 'Loading...';
|
|
1800
|
-
btn.disabled = true;
|
|
1801
|
-
|
|
1802
|
-
try {
|
|
1803
|
-
const res = await api('/api/fetch-models', {
|
|
1804
|
-
method: 'POST',
|
|
1805
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1806
|
-
body: JSON.stringify({ baseURL, apiKey, key: formLoadedKey })
|
|
1807
|
-
});
|
|
1808
|
-
const data = await res.json();
|
|
1809
|
-
if (data.ok && Array.isArray(data.models)) {
|
|
1810
|
-
loadedModels = data.models;
|
|
1811
|
-
populateModelOptions(loadedModels);
|
|
1812
|
-
renderModelBrowser(loadedModels);
|
|
1813
|
-
showToast(`Loaded ${loadedModels.length} models from upstream!`);
|
|
1814
|
-
} else {
|
|
1815
|
-
showToast(`Failed to load models: ${data.error || 'Unknown error'}`, true);
|
|
1816
|
-
}
|
|
1817
|
-
} catch (err) {
|
|
1818
|
-
showToast(`Fetch error: ${err.message}`, true);
|
|
1819
|
-
} finally {
|
|
1820
|
-
btn.innerText = 'Fetch models';
|
|
1821
|
-
btn.disabled = false;
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1722
|
+
document.getElementById('active-tag').textContent = '';
|
|
1824
1723
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
datalist.innerHTML = '';
|
|
1828
|
-
for (const m of models) {
|
|
1829
|
-
const opt = document.createElement('option');
|
|
1830
|
-
opt.value = m;
|
|
1831
|
-
datalist.appendChild(opt);
|
|
1832
|
-
}
|
|
1724
|
+
renderModelSlots('auto', currentProfileBuffer);
|
|
1725
|
+
switchTab('tab-general');
|
|
1833
1726
|
}
|
|
1834
1727
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1728
|
+
const TEMPLATES = {
|
|
1729
|
+
intact: {
|
|
1730
|
+
name: 'Intact Credential Proxy',
|
|
1731
|
+
baseURL: 'http://127.0.0.1:8080/v1',
|
|
1732
|
+
tool: 'auto',
|
|
1733
|
+
outFormat: 'openai-chat',
|
|
1734
|
+
mode: 'convert',
|
|
1735
|
+
thinkingMode: 'auto',
|
|
1736
|
+
defaultModels: { default: 'gpt-4o', sonnet: 'claude-3-7-sonnet', opus: 'claude-3-opus', haiku: 'claude-3-5-haiku' }
|
|
1737
|
+
},
|
|
1738
|
+
'9router': {
|
|
1739
|
+
name: '9Router Client Proxy',
|
|
1740
|
+
baseURL: 'http://127.0.0.1:20128/v1',
|
|
1741
|
+
tool: 'auto',
|
|
1742
|
+
outFormat: 'openai-chat',
|
|
1743
|
+
mode: 'hybrid',
|
|
1744
|
+
thinkingMode: 'auto',
|
|
1745
|
+
defaultModels: { default: 'gpt-4o', sonnet: 'claude-3-7-sonnet' }
|
|
1746
|
+
},
|
|
1747
|
+
openrouter: {
|
|
1748
|
+
name: 'OpenRouter Gateway',
|
|
1749
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
1750
|
+
tool: 'auto',
|
|
1751
|
+
outFormat: 'openai-chat',
|
|
1752
|
+
mode: 'convert',
|
|
1753
|
+
thinkingMode: 'auto',
|
|
1754
|
+
defaultModels: { default: 'anthropic/claude-3.7-sonnet', sonnet: 'anthropic/claude-3.7-sonnet' }
|
|
1755
|
+
},
|
|
1756
|
+
vertex: {
|
|
1757
|
+
name: 'Google Vertex AI',
|
|
1758
|
+
baseURL: 'https://us-central1-aiplatform.googleapis.com/v1',
|
|
1759
|
+
tool: 'anthropic',
|
|
1760
|
+
outFormat: 'vertex',
|
|
1761
|
+
mode: 'convert',
|
|
1762
|
+
thinkingMode: 'auto',
|
|
1763
|
+
defaultModels: { sonnet: 'claude-3-7-sonnet@20250219' }
|
|
1764
|
+
},
|
|
1765
|
+
local: {
|
|
1766
|
+
name: 'Local Ollama / vLLM Server',
|
|
1767
|
+
baseURL: 'http://127.0.0.1:11434/v1',
|
|
1768
|
+
tool: 'auto',
|
|
1769
|
+
outFormat: 'openai-chat',
|
|
1770
|
+
mode: 'direct',
|
|
1771
|
+
thinkingMode: 'off',
|
|
1772
|
+
defaultModels: { default: 'qwen2.5-coder:32b' }
|
|
1875
1773
|
}
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
function filterModels() {
|
|
1879
|
-
const q = (document.getElementById('model-filter').value || '').toLowerCase().trim();
|
|
1880
|
-
const filtered = loadedModels.filter(m => m.toLowerCase().includes(q));
|
|
1881
|
-
renderModelBrowser(filtered);
|
|
1882
|
-
}
|
|
1774
|
+
};
|
|
1883
1775
|
|
|
1884
|
-
function
|
|
1885
|
-
const
|
|
1886
|
-
if (
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
if (chk1M && is1M) {
|
|
1890
|
-
chk1M.checked = true;
|
|
1776
|
+
function applyTemplate(tName) {
|
|
1777
|
+
const t = TEMPLATES[tName];
|
|
1778
|
+
if (!t) return;
|
|
1779
|
+
if (creatingNew) {
|
|
1780
|
+
document.getElementById('p-key').value = tName;
|
|
1891
1781
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1782
|
+
document.getElementById('p-name').value = t.name;
|
|
1783
|
+
document.getElementById('p-url').value = t.baseURL;
|
|
1784
|
+
document.getElementById('p-tool').value = t.tool;
|
|
1785
|
+
document.getElementById('p-outformat').value = t.outFormat;
|
|
1786
|
+
document.getElementById('p-mode').value = t.mode;
|
|
1787
|
+
document.getElementById('p-thinking-mode').value = t.thinkingMode;
|
|
1788
|
+
currentProfileBuffer = { ...currentProfileBuffer, ...t };
|
|
1789
|
+
renderModelSlots(t.tool || 'auto', t);
|
|
1790
|
+
showToast('Template "' + t.name + '" applied');
|
|
1895
1791
|
}
|
|
1896
1792
|
|
|
1897
1793
|
async function toggleProxy() {
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1794
|
+
const isAnyActive = Object.values(state.activeProfiles || (state.config?.activeProfiles || {})).some(Boolean);
|
|
1795
|
+
const action = isAnyActive ? 'deactivate' : 'activate';
|
|
1796
|
+
showToast((isAnyActive ? 'Switching to official endpoints...' : 'Activating compatible routes...'));
|
|
1900
1797
|
try {
|
|
1901
1798
|
const res = await api('/api/toggle', {
|
|
1902
1799
|
method: 'POST',
|
|
1903
1800
|
headers: { 'Content-Type': 'application/json' },
|
|
1904
|
-
body: JSON.stringify({ enabled:
|
|
1801
|
+
body: JSON.stringify({ enabled: !isAnyActive })
|
|
1905
1802
|
});
|
|
1906
1803
|
const data = await res.json();
|
|
1907
|
-
if (data.
|
|
1908
|
-
showToast(
|
|
1804
|
+
if (data.ok) {
|
|
1805
|
+
showToast(isAnyActive ? 'Using official endpoints' : 'Gateway activated');
|
|
1806
|
+
await loadStatus();
|
|
1909
1807
|
} else {
|
|
1910
|
-
showToast(data.error || 'Failed to toggle
|
|
1808
|
+
showToast(data.error || 'Failed to toggle routes', true);
|
|
1911
1809
|
}
|
|
1912
|
-
loadStatus();
|
|
1913
1810
|
} catch (err) {
|
|
1914
|
-
showToast('
|
|
1811
|
+
showToast('Network error toggling routes', true);
|
|
1915
1812
|
}
|
|
1916
1813
|
}
|
|
1917
1814
|
|
|
1918
1815
|
async function saveProfile(e) {
|
|
1919
1816
|
e.preventDefault();
|
|
1920
1817
|
const key = document.getElementById('p-key').value.trim();
|
|
1921
|
-
|
|
1922
|
-
if (creatingNew && state.config?.profiles?.[key] && !confirm(`Provider "${key}" already exists. Overwrite it?`)) return;
|
|
1818
|
+
if (!key) return;
|
|
1923
1819
|
|
|
1924
1820
|
collectCurrentSlotValues();
|
|
1925
1821
|
const existing = state.config?.profiles?.[key] || {};
|
|
1926
|
-
const
|
|
1927
|
-
const merged1M = { ...(existing.model1M || {}), ...(currentProfileBuffer?.model1M || {}) };
|
|
1822
|
+
const keyVal = document.getElementById('p-key-val').value.trim();
|
|
1928
1823
|
|
|
1929
|
-
const
|
|
1824
|
+
const profilePayload = {
|
|
1930
1825
|
name: document.getElementById('p-name').value.trim(),
|
|
1931
|
-
mode: document.getElementById('p-mode').value,
|
|
1932
|
-
tool: ({ anthropic: 'claude', responses: 'codex' }[document.getElementById('p-tool').value]) || null,
|
|
1933
|
-
thinkingMode: document.getElementById('p-thinking-mode').value || 'auto',
|
|
1934
1826
|
baseURL: document.getElementById('p-url').value.trim(),
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1827
|
+
tool: document.getElementById('p-tool').value,
|
|
1828
|
+
outFormat: document.getElementById('p-outformat').value,
|
|
1829
|
+
mode: document.getElementById('p-mode').value,
|
|
1830
|
+
thinkingMode: document.getElementById('p-thinking-mode').value,
|
|
1831
|
+
defaultModels: { ...(existing.defaultModels || {}), ...(currentProfileBuffer?.defaultModels || {}) },
|
|
1832
|
+
model1M: { ...(existing.model1M || {}), ...(currentProfileBuffer?.model1M || {}) }
|
|
1938
1833
|
};
|
|
1939
|
-
profile.outFormat = outFormat;
|
|
1940
|
-
collectCodexValues(profile);
|
|
1941
1834
|
|
|
1835
|
+
if (keyVal === MASKED_KEY) {
|
|
1836
|
+
profilePayload.apiKey = existing.apiKey || '';
|
|
1837
|
+
} else if (keyVal) {
|
|
1838
|
+
profilePayload.apiKey = keyVal;
|
|
1839
|
+
} else {
|
|
1840
|
+
profilePayload.apiKey = '';
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (existing.codexRoles) profilePayload.codexRoles = existing.codexRoles;
|
|
1844
|
+
if (existing.publicModels) profilePayload.publicModels = existing.publicModels;
|
|
1845
|
+
|
|
1846
|
+
showToast('Saving profile "' + key + '"...');
|
|
1942
1847
|
try {
|
|
1943
1848
|
const res = await api('/api/save-profile', {
|
|
1944
1849
|
method: 'POST',
|
|
1945
1850
|
headers: { 'Content-Type': 'application/json' },
|
|
1946
|
-
body: JSON.stringify({ key, profile })
|
|
1851
|
+
body: JSON.stringify({ key, profile: profilePayload })
|
|
1947
1852
|
});
|
|
1948
1853
|
const data = await res.json();
|
|
1949
|
-
if (data.
|
|
1950
|
-
showToast('
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
formDirty = false;
|
|
1954
|
-
formLoadedKey = null;
|
|
1955
|
-
loadStatus();
|
|
1854
|
+
if (data.ok) {
|
|
1855
|
+
showToast('Profile "' + key + '" saved successfully');
|
|
1856
|
+
closeProfileModal();
|
|
1857
|
+
await loadStatus();
|
|
1956
1858
|
} else {
|
|
1957
|
-
showToast(data.error || 'Failed to save', true);
|
|
1859
|
+
showToast(data.error || 'Failed to save profile', true);
|
|
1958
1860
|
}
|
|
1959
1861
|
} catch (err) {
|
|
1960
|
-
showToast('
|
|
1862
|
+
showToast('Network error saving profile', true);
|
|
1961
1863
|
}
|
|
1962
1864
|
}
|
|
1963
1865
|
|
|
1964
1866
|
async function deleteProfile() {
|
|
1965
|
-
const key = document.getElementById('p-key').value;
|
|
1966
|
-
if (!confirm(
|
|
1867
|
+
const key = document.getElementById('p-key').value.trim();
|
|
1868
|
+
if (!key || !confirm('Delete provider profile "' + key + '" permanently?')) return;
|
|
1967
1869
|
|
|
1870
|
+
showToast('Deleting profile "' + key + '"...');
|
|
1968
1871
|
try {
|
|
1969
1872
|
const res = await api('/api/delete-profile', {
|
|
1970
1873
|
method: 'POST',
|
|
@@ -1972,21 +1875,18 @@
|
|
|
1972
1875
|
body: JSON.stringify({ key })
|
|
1973
1876
|
});
|
|
1974
1877
|
const data = await res.json();
|
|
1975
|
-
if (data.
|
|
1976
|
-
showToast('
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
formDirty = false;
|
|
1980
|
-
loadStatus();
|
|
1878
|
+
if (data.ok) {
|
|
1879
|
+
showToast('Profile "' + key + '" deleted');
|
|
1880
|
+
closeProfileModal();
|
|
1881
|
+
await loadStatus();
|
|
1981
1882
|
} else {
|
|
1982
|
-
showToast(data.error || 'Failed to delete', true);
|
|
1883
|
+
showToast(data.error || 'Failed to delete profile', true);
|
|
1983
1884
|
}
|
|
1984
1885
|
} catch (err) {
|
|
1985
|
-
showToast('
|
|
1886
|
+
showToast('Network error deleting profile', true);
|
|
1986
1887
|
}
|
|
1987
1888
|
}
|
|
1988
1889
|
|
|
1989
|
-
// Modern Non-blocking Connection Tester
|
|
1990
1890
|
async function testConnection() {
|
|
1991
1891
|
const btn = document.getElementById('btn-test-conn');
|
|
1992
1892
|
const box = document.getElementById('test-result');
|
|
@@ -2002,7 +1902,7 @@
|
|
|
2002
1902
|
const currentFmt = document.getElementById('p-tool')?.value || 'auto';
|
|
2003
1903
|
const cfg = getActiveSlotConfig(currentFmt);
|
|
2004
1904
|
const primarySlotId = cfg.slots[0]?.id || 'default';
|
|
2005
|
-
const modelInput = document.getElementById(
|
|
1905
|
+
const modelInput = document.getElementById('p-' + primarySlotId) || document.getElementById('p-sonnet') || document.getElementById('p-opus');
|
|
2006
1906
|
const testModel = (modelInput?.value || '').trim() || 'default';
|
|
2007
1907
|
|
|
2008
1908
|
const res = await api('/api/test-upstream', {
|
|
@@ -2010,7 +1910,7 @@
|
|
|
2010
1910
|
headers: { 'Content-Type': 'application/json' },
|
|
2011
1911
|
body: JSON.stringify({
|
|
2012
1912
|
baseURL: document.getElementById('p-url').value.trim(),
|
|
2013
|
-
apiKey: document.getElementById('p-key-val').value.trim(),
|
|
1913
|
+
apiKey: document.getElementById('p-key-val').value.trim() === MASKED_KEY ? (state.config?.profiles?.[formLoadedKey]?.apiKey || '') : document.getElementById('p-key-val').value.trim(),
|
|
2014
1914
|
mode: document.getElementById('p-mode').value,
|
|
2015
1915
|
outFormat: document.getElementById('p-outformat').value,
|
|
2016
1916
|
key: formLoadedKey,
|
|
@@ -2022,47 +1922,202 @@
|
|
|
2022
1922
|
|
|
2023
1923
|
if (data.ok) {
|
|
2024
1924
|
box.className = 'test-result-box success';
|
|
2025
|
-
statusBadge.
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
1925
|
+
statusBadge.className = 'badge ok';
|
|
1926
|
+
statusBadge.textContent = '✓ HTTP 200 OK (' + (data.outFormat || 'upstream') + ')';
|
|
1927
|
+
latencyBadge.textContent = 'Latency: ' + data.latency + 'ms';
|
|
1928
|
+
sampleBox.textContent = 'Sample Output: ' + data.sample;
|
|
1929
|
+
showToast('Connection test passed (' + data.latency + 'ms)');
|
|
2029
1930
|
} else {
|
|
2030
1931
|
box.className = 'test-result-box error';
|
|
2031
|
-
statusBadge.
|
|
1932
|
+
statusBadge.className = 'badge err';
|
|
1933
|
+
statusBadge.textContent = '✗ Failed (' + (data.status || 'Error') + ')';
|
|
2032
1934
|
latencyBadge.textContent = '';
|
|
2033
|
-
sampleBox.textContent =
|
|
2034
|
-
showToast('Upstream
|
|
1935
|
+
sampleBox.textContent = 'Error: ' + (data.error || 'Upstream connection failed');
|
|
1936
|
+
showToast('Upstream connection failed', true);
|
|
2035
1937
|
}
|
|
2036
1938
|
} catch (err) {
|
|
2037
1939
|
box.style.display = 'block';
|
|
2038
1940
|
box.className = 'test-result-box error';
|
|
1941
|
+
statusBadge.className = 'badge err';
|
|
2039
1942
|
statusBadge.textContent = '✗ Network Error';
|
|
2040
1943
|
latencyBadge.textContent = '';
|
|
2041
1944
|
sampleBox.textContent = err.message;
|
|
2042
1945
|
showToast('Network error testing upstream', true);
|
|
2043
1946
|
} finally {
|
|
2044
|
-
btn.innerText = 'Test
|
|
1947
|
+
btn.innerText = 'Test Connection';
|
|
2045
1948
|
btn.disabled = false;
|
|
2046
1949
|
}
|
|
2047
1950
|
}
|
|
2048
1951
|
|
|
2049
|
-
|
|
1952
|
+
async function fetchModelList() {
|
|
1953
|
+
const btn = document.getElementById('btn-fetch-models');
|
|
1954
|
+
const browser = document.getElementById('model-browser');
|
|
1955
|
+
const baseURL = document.getElementById('p-url').value.trim();
|
|
1956
|
+
const apiKeyVal = document.getElementById('p-key-val').value.trim();
|
|
1957
|
+
const apiKey = (apiKeyVal === MASKED_KEY) ? (state.config?.profiles?.[formLoadedKey]?.apiKey || '') : apiKeyVal;
|
|
2050
1958
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
logRefreshInterval = null;
|
|
1959
|
+
if (!baseURL) {
|
|
1960
|
+
showToast('Please enter a Base URL first', true);
|
|
1961
|
+
return;
|
|
2055
1962
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
1963
|
+
|
|
1964
|
+
btn.disabled = true;
|
|
1965
|
+
btn.innerText = 'Fetching...';
|
|
1966
|
+
try {
|
|
1967
|
+
const res = await api('/api/fetch-models', {
|
|
1968
|
+
method: 'POST',
|
|
1969
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1970
|
+
body: JSON.stringify({ baseURL, apiKey })
|
|
1971
|
+
});
|
|
1972
|
+
const data = await res.json();
|
|
1973
|
+
if (data.ok && Array.isArray(data.models)) {
|
|
1974
|
+
loadedModels = data.models;
|
|
1975
|
+
populateModelOptions(loadedModels);
|
|
1976
|
+
renderModelBrowser(loadedModels);
|
|
1977
|
+
browser.style.display = 'block';
|
|
1978
|
+
showToast('Loaded ' + loadedModels.length + ' models');
|
|
1979
|
+
} else {
|
|
1980
|
+
showToast(data.error || 'No models returned from provider', true);
|
|
1981
|
+
}
|
|
1982
|
+
} catch (err) {
|
|
1983
|
+
showToast('Error fetching models: ' + err.message, true);
|
|
1984
|
+
} finally {
|
|
1985
|
+
btn.disabled = false;
|
|
1986
|
+
btn.innerText = 'Browse Models';
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function populateModelOptions(models) {
|
|
1991
|
+
const dl = document.getElementById('model-options');
|
|
1992
|
+
if (!dl) return;
|
|
1993
|
+
dl.innerHTML = '';
|
|
1994
|
+
for (const m of models) {
|
|
1995
|
+
const opt = document.createElement('option');
|
|
1996
|
+
opt.value = m;
|
|
1997
|
+
dl.appendChild(opt);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
function renderModelBrowser(models) {
|
|
2002
|
+
const container = document.getElementById('model-items');
|
|
2003
|
+
const label = document.getElementById('model-count-label');
|
|
2004
|
+
if (!container) return;
|
|
2005
|
+
container.innerHTML = '';
|
|
2006
|
+
if (label) label.textContent = models.length + ' models found';
|
|
2007
|
+
|
|
2008
|
+
for (const m of models) {
|
|
2009
|
+
const btn = document.createElement('button');
|
|
2010
|
+
btn.type = 'button';
|
|
2011
|
+
btn.className = 'btn btn-outline btn-sm';
|
|
2012
|
+
btn.style.cssText = 'font-size:0.75rem; padding:0.15rem 0.5rem;';
|
|
2013
|
+
btn.textContent = m;
|
|
2014
|
+
btn.onclick = () => selectModel(m);
|
|
2015
|
+
container.appendChild(btn);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
function filterModels() {
|
|
2020
|
+
const q = (document.getElementById('model-filter')?.value || '').toLowerCase();
|
|
2021
|
+
const filtered = loadedModels.filter(m => m.toLowerCase().includes(q));
|
|
2022
|
+
renderModelBrowser(filtered);
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function selectModel(modelId) {
|
|
2026
|
+
const activeTab = document.querySelector('.tab-content.active');
|
|
2027
|
+
const focused = activeTab?.querySelector('input[type="text"]:focus');
|
|
2028
|
+
if (focused && focused.id.startsWith('p-')) {
|
|
2029
|
+
focused.value = modelId;
|
|
2030
|
+
} else {
|
|
2031
|
+
const sonnetInput = document.getElementById('p-sonnet') || document.getElementById('p-default');
|
|
2032
|
+
if (sonnetInput) sonnetInput.value = modelId;
|
|
2033
|
+
}
|
|
2034
|
+
showToast('Selected: ' + modelId);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Dynamic Model Catalog View
|
|
2038
|
+
async function renderCatalogView() {
|
|
2039
|
+
const claudeContainer = document.getElementById('catalog-claude-items');
|
|
2040
|
+
const codexContainer = document.getElementById('catalog-codex-items');
|
|
2041
|
+
if (!claudeContainer || !codexContainer) return;
|
|
2042
|
+
|
|
2043
|
+
try {
|
|
2044
|
+
const res = await api('/api/catalog');
|
|
2045
|
+
if (!res.ok) {
|
|
2046
|
+
showToast('Failed to load model catalog', true);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
const catalog = await res.json();
|
|
2050
|
+
cachedCatalog = catalog;
|
|
2051
|
+
|
|
2052
|
+
const claudeModels = Array.isArray(catalog.claude?.models) ? catalog.claude.models : [];
|
|
2053
|
+
const codexModels = Array.isArray(catalog.codex?.models) ? catalog.codex.models : [];
|
|
2054
|
+
|
|
2055
|
+
const claudeTiers = new Set(claudeModels.map(m => m.tier));
|
|
2056
|
+
const codexRoles = new Set(codexModels.map(m => m.role));
|
|
2057
|
+
|
|
2058
|
+
const ctModels = document.getElementById('ct-models');
|
|
2059
|
+
if (ctModels) ctModels.textContent = claudeModels.length + codexModels.length;
|
|
2060
|
+
|
|
2061
|
+
document.getElementById('badge-claude-count').textContent = claudeModels.length + ' models · ' + claudeTiers.size + ' tiers';
|
|
2062
|
+
document.getElementById('badge-codex-count').textContent = codexModels.length + ' models · ' + codexRoles.size + ' roles';
|
|
2063
|
+
|
|
2064
|
+
claudeContainer.innerHTML = claudeModels.map(m => `
|
|
2065
|
+
<div class="row" style="padding:0.6rem 0.8rem; background:var(--card2); border-radius:8px;">
|
|
2066
|
+
<div class="who">
|
|
2067
|
+
<div style="display:flex; align-items:center; gap:0.45rem;">
|
|
2068
|
+
<span class="badge accent" style="text-transform:uppercase; font-size:0.7rem;">${escapeHtml(m.tier)}</span>
|
|
2069
|
+
<span class="l mono" style="font-size:0.85rem; font-weight:600;">${escapeHtml(m.id)}</span>
|
|
2070
|
+
</div>
|
|
2071
|
+
</div>
|
|
2072
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="navigator.clipboard.writeText('${escapeHtml(m.id)}'); showToast('Copied ${escapeHtml(m.id)}');">Copy</button>
|
|
2073
|
+
</div>
|
|
2074
|
+
`).join('') || '<div class="hint">No Claude tiers discovered</div>';
|
|
2075
|
+
|
|
2076
|
+
codexContainer.innerHTML = codexModels.map(m => `
|
|
2077
|
+
<div class="row" style="padding:0.6rem 0.8rem; background:var(--card2); border-radius:8px;">
|
|
2078
|
+
<div class="who">
|
|
2079
|
+
<div style="display:flex; align-items:center; gap:0.45rem;">
|
|
2080
|
+
<span class="badge ok" style="text-transform:capitalize; font-size:0.7rem;">${escapeHtml(m.role)}</span>
|
|
2081
|
+
<span class="l mono" style="font-size:0.85rem; font-weight:600;">${escapeHtml(m.id)}</span>
|
|
2082
|
+
</div>
|
|
2083
|
+
</div>
|
|
2084
|
+
<button type="button" class="btn btn-outline btn-sm" onclick="navigator.clipboard.writeText('${escapeHtml(m.id)}'); showToast('Copied ${escapeHtml(m.id)}');">Copy</button>
|
|
2085
|
+
</div>
|
|
2086
|
+
`).join('') || '<div class="hint">No Codex roles discovered</div>';
|
|
2087
|
+
} catch (err) {
|
|
2088
|
+
showToast('Error loading model catalog', true);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function filterCatalogView(q) {
|
|
2093
|
+
const term = (q || '').toLowerCase();
|
|
2094
|
+
document.querySelectorAll('#catalog-claude-items .row, #catalog-codex-items .row').forEach(row => {
|
|
2095
|
+
const text = row.textContent.toLowerCase();
|
|
2096
|
+
row.style.display = text.includes(term) ? 'flex' : 'none';
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
async function refreshCatalogUpstream() {
|
|
2101
|
+
const btn = document.getElementById('btn-sync-catalog');
|
|
2102
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Syncing...'; }
|
|
2103
|
+
showToast('Refreshing model catalog from upstream...');
|
|
2104
|
+
try {
|
|
2105
|
+
const res = await api('/api/catalog/refresh', { method: 'POST' });
|
|
2106
|
+
const data = await res.json();
|
|
2107
|
+
if (data.ok) {
|
|
2108
|
+
showToast('Model catalog synchronized');
|
|
2109
|
+
await renderCatalogView();
|
|
2110
|
+
} else {
|
|
2111
|
+
showToast(data.error || 'Failed to refresh catalog', true);
|
|
2112
|
+
}
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
showToast('Network error syncing catalog', true);
|
|
2115
|
+
} finally {
|
|
2116
|
+
if (btn) { btn.disabled = false; btn.textContent = '⚡ Sync Models with Upstream'; }
|
|
2063
2117
|
}
|
|
2064
2118
|
}
|
|
2065
2119
|
|
|
2120
|
+
// Request Inspector Logs View
|
|
2066
2121
|
async function loadInspectorLogs() {
|
|
2067
2122
|
try {
|
|
2068
2123
|
const res = await api('/api/logs');
|
|
@@ -2070,49 +2125,52 @@
|
|
|
2070
2125
|
const container = document.getElementById('inspector-logs-container');
|
|
2071
2126
|
if (!container) return;
|
|
2072
2127
|
|
|
2128
|
+
lastLogsCount = (data.logs || []).length;
|
|
2129
|
+
const ctLogs = document.getElementById('ct-logs');
|
|
2130
|
+
if (ctLogs) ctLogs.textContent = lastLogsCount;
|
|
2131
|
+
|
|
2073
2132
|
if (!data.logs || data.logs.length === 0) {
|
|
2074
|
-
container.innerHTML = '<div
|
|
2133
|
+
container.innerHTML = '<div class="card pad hint" style="text-align:center; padding:2rem 1rem;">No requests recorded yet in the ring buffer. Make a request via Claude Code or Codex.</div>';
|
|
2075
2134
|
return;
|
|
2076
2135
|
}
|
|
2077
2136
|
|
|
2078
2137
|
container.innerHTML = '';
|
|
2079
2138
|
for (const l of data.logs) {
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2139
|
+
const item = document.createElement('div');
|
|
2140
|
+
item.className = 'log-item';
|
|
2082
2141
|
const isErr = l.status >= 400;
|
|
2083
|
-
const statusClass = isErr ? 'badge-convert' : 'badge-direct';
|
|
2084
2142
|
|
|
2085
|
-
let
|
|
2143
|
+
let detailsHtml = '';
|
|
2086
2144
|
if (l.requestPreview) {
|
|
2087
|
-
|
|
2145
|
+
detailsHtml += '<div><span class="hint" style="font-weight:600; color:var(--accent);">Prompt:</span><pre>' + escapeHtml(l.requestPreview) + '</pre></div>';
|
|
2088
2146
|
}
|
|
2089
2147
|
if (l.thinkingChars > 0) {
|
|
2090
|
-
|
|
2148
|
+
detailsHtml += '<div><span class="hint" style="font-weight:600; color:var(--warn);">Thinking:</span><pre>' + l.thinkingChars + ' characters extracted</pre></div>';
|
|
2091
2149
|
}
|
|
2092
2150
|
if (l.responsePreview) {
|
|
2093
|
-
|
|
2151
|
+
detailsHtml += '<div><span class="hint" style="font-weight:600; color:var(--ok);">Response Output:</span><pre>' + escapeHtml(l.responsePreview) + '</pre></div>';
|
|
2094
2152
|
}
|
|
2095
2153
|
if (l.error) {
|
|
2096
|
-
|
|
2154
|
+
detailsHtml += '<div><span class="hint" style="font-weight:600; color:var(--err);">Error:</span><pre>' + escapeHtml(l.error) + '</pre></div>';
|
|
2097
2155
|
}
|
|
2098
2156
|
|
|
2099
|
-
|
|
2100
|
-
<div
|
|
2101
|
-
<div
|
|
2102
|
-
<span class="
|
|
2103
|
-
<span
|
|
2104
|
-
<
|
|
2105
|
-
<span
|
|
2157
|
+
item.innerHTML = `
|
|
2158
|
+
<div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem;">
|
|
2159
|
+
<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap;">
|
|
2160
|
+
<span class="mono faint" style="font-size:0.75rem;">${escapeHtml(l.timestamp)}</span>
|
|
2161
|
+
<span class="badge ${isErr ? 'err' : 'ok'}">${escapeHtml(l.status)} ${escapeHtml(l.method || 'POST')}</span>
|
|
2162
|
+
<strong class="mono" style="font-size:0.85rem; color:var(--fg);">${escapeHtml(l.path || '/v1/messages')}</strong>
|
|
2163
|
+
<span class="badge accent">${escapeHtml(l.client || 'Client')}</span>
|
|
2106
2164
|
</div>
|
|
2107
|
-
<div style="display:
|
|
2108
|
-
<span
|
|
2109
|
-
<span
|
|
2110
|
-
<span>out ${l.tokens?.completion || 0} tokens</span>
|
|
2165
|
+
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
2166
|
+
${l.latency ? '<span class="hint mono">' + escapeHtml(l.latency) + 'ms</span>' : ''}
|
|
2167
|
+
${l.profile ? '<span class="badge">→ ' + escapeHtml(l.profile) + '</span>' : ''}
|
|
2111
2168
|
</div>
|
|
2112
2169
|
</div>
|
|
2113
|
-
${
|
|
2170
|
+
${detailsHtml ? '<details><summary>View Payload Preview</summary>' + detailsHtml + '</details>' : ''}
|
|
2114
2171
|
`;
|
|
2115
|
-
|
|
2172
|
+
|
|
2173
|
+
container.appendChild(item);
|
|
2116
2174
|
}
|
|
2117
2175
|
} catch (err) {
|
|
2118
2176
|
console.error('Failed to load logs', err);
|
|
@@ -2122,28 +2180,47 @@
|
|
|
2122
2180
|
async function clearInspectorLogs() {
|
|
2123
2181
|
try {
|
|
2124
2182
|
const res = await api('/api/logs/clear', { method: 'POST' });
|
|
2125
|
-
|
|
2126
|
-
if (
|
|
2127
|
-
|
|
2183
|
+
const data = await res.json();
|
|
2184
|
+
if (data.ok) {
|
|
2185
|
+
showToast('Request inspector logs cleared');
|
|
2186
|
+
await loadInspectorLogs();
|
|
2187
|
+
}
|
|
2128
2188
|
} catch (err) {
|
|
2129
|
-
showToast('
|
|
2189
|
+
showToast('Error clearing logs', true);
|
|
2130
2190
|
}
|
|
2131
2191
|
}
|
|
2132
2192
|
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
if (
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2193
|
+
function toggleLogAutoRefresh() {
|
|
2194
|
+
const chk = document.getElementById('chk-auto-refresh-logs');
|
|
2195
|
+
if (chk && chk.checked) {
|
|
2196
|
+
clearInterval(logRefreshTimer);
|
|
2197
|
+
logRefreshTimer = setInterval(() => {
|
|
2198
|
+
if (window.location.hash.startsWith('#/logs')) {
|
|
2199
|
+
loadInspectorLogs();
|
|
2200
|
+
}
|
|
2201
|
+
}, 3000);
|
|
2202
|
+
} else {
|
|
2203
|
+
clearInterval(logRefreshTimer);
|
|
2204
|
+
logRefreshTimer = null;
|
|
2205
|
+
}
|
|
2145
2206
|
}
|
|
2146
|
-
|
|
2207
|
+
|
|
2208
|
+
// Initial Bootstrap
|
|
2209
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2210
|
+
// Setup initial theme buttons state
|
|
2211
|
+
const currentTheme = document.documentElement.dataset.theme || 'dark';
|
|
2212
|
+
document.getElementById('btn-theme-dark')?.setAttribute('aria-pressed', currentTheme === 'dark');
|
|
2213
|
+
document.getElementById('btn-theme-light')?.setAttribute('aria-pressed', currentTheme === 'light');
|
|
2214
|
+
|
|
2215
|
+
// Setup log auto-refresh
|
|
2216
|
+
toggleLogAutoRefresh();
|
|
2217
|
+
|
|
2218
|
+
// Trigger initial route
|
|
2219
|
+
handleRoute();
|
|
2220
|
+
|
|
2221
|
+
// Load initial state from edge gateway
|
|
2222
|
+
loadStatus();
|
|
2223
|
+
});
|
|
2147
2224
|
</script>
|
|
2148
2225
|
</body>
|
|
2149
|
-
</html>
|
|
2226
|
+
</html>
|