codex-lb 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- app/core/clients/proxy.py +33 -3
- app/core/config/settings.py +1 -0
- app/core/openai/requests.py +21 -3
- app/core/openai/v1_requests.py +148 -0
- app/db/models.py +3 -3
- app/main.py +1 -0
- app/modules/accounts/repository.py +4 -1
- app/modules/proxy/api.py +36 -0
- app/modules/proxy/service.py +29 -0
- app/modules/request_logs/api.py +61 -7
- app/modules/request_logs/repository.py +128 -16
- app/modules/request_logs/schemas.py +11 -2
- app/modules/request_logs/service.py +97 -20
- app/modules/usage/updater.py +58 -26
- app/static/index.css +1400 -347
- app/static/index.html +627 -415
- app/static/index.js +409 -42
- codex_lb-0.4.0.dist-info/METADATA +172 -0
- {codex_lb-0.3.0.dist-info → codex_lb-0.4.0.dist-info}/RECORD +22 -22
- app/static/7.css +0 -1409
- codex_lb-0.3.0.dist-info/METADATA +0 -108
- {codex_lb-0.3.0.dist-info → codex_lb-0.4.0.dist-info}/WHEEL +0 -0
- {codex_lb-0.3.0.dist-info → codex_lb-0.4.0.dist-info}/entry_points.txt +0 -0
- {codex_lb-0.3.0.dist-info → codex_lb-0.4.0.dist-info}/licenses/LICENSE +0 -0
app/static/index.html
CHANGED
|
@@ -4,501 +4,713 @@
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
-
<meta name="color-scheme" content="light">
|
|
7
|
+
<meta name="color-scheme" content="light dark">
|
|
8
8
|
<title>Codex LB</title>
|
|
9
9
|
<link rel="icon"
|
|
10
10
|
href="data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none" viewBox="0 0 32 32"%3E%3Cpath stroke="%23000" stroke-linecap="round" stroke-width="2.484" d="M22.356 19.797H17.17M9.662 12.29l1.979 3.576a.511.511 0 0 1-.005.504l-1.974 3.409M30.758 16c0 8.15-6.607 14.758-14.758 14.758-8.15 0-14.758-6.607-14.758-14.758C1.242 7.85 7.85 1.242 16 1.242c8.15 0 14.758 6.608 14.758 14.758Z"/%3E%3C/svg%3E"
|
|
11
11
|
type="image/svg+xml">
|
|
12
|
-
<link rel="stylesheet" href="/dashboard/7.css">
|
|
13
12
|
<link rel="stylesheet" href="/dashboard/index.css">
|
|
14
13
|
</head>
|
|
15
14
|
|
|
16
|
-
<body class="has-scrollbar" x-data="feApp" x-init="init()" x-cloak>
|
|
15
|
+
<body class="has-scrollbar" x-data="feApp" :data-theme="theme" x-init="init()" x-cloak>
|
|
17
16
|
<div class="app-shell">
|
|
18
|
-
<
|
|
19
|
-
<div class="
|
|
20
|
-
<
|
|
21
|
-
<div class="
|
|
22
|
-
<button aria-label="
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
<header class="main-header">
|
|
18
|
+
<div class="header-content">
|
|
19
|
+
<h1 class="app-logo">Codex Load Balancer</h1>
|
|
20
|
+
<div class="header-actions">
|
|
21
|
+
<button class="theme-toggle" @click="toggleTheme" aria-label="Toggle theme">
|
|
22
|
+
<svg x-show="theme === 'dark'" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|
23
|
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
24
|
+
<circle cx="12" cy="12" r="4" />
|
|
25
|
+
<path d="M12 2v2" />
|
|
26
|
+
<path d="M12 20v2" />
|
|
27
|
+
<path d="m4.93 4.93 1.41 1.41" />
|
|
28
|
+
<path d="m17.66 17.66 1.41 1.41" />
|
|
29
|
+
<path d="M2 12h2" />
|
|
30
|
+
<path d="M20 12h2" />
|
|
31
|
+
<path d="m6.34 17.66-1.41 1.41" />
|
|
32
|
+
<path d="m19.07 4.93-1.41 1.41" />
|
|
33
|
+
</svg>
|
|
34
|
+
<svg x-show="theme === 'light'" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
|
35
|
+
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
36
|
+
stroke-linejoin="round">
|
|
37
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
38
|
+
</svg>
|
|
39
|
+
</button>
|
|
25
40
|
</div>
|
|
26
41
|
</div>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
</header>
|
|
43
|
+
<main class="main-content">
|
|
44
|
+
<div class="loading-overlay" x-show="isLoading" x-transition.opacity>
|
|
45
|
+
<div class="loading-panel" role="status" aria-live="polite">
|
|
46
|
+
<span class="spinner animate" aria-hidden="true"></span>
|
|
47
|
+
<div>Loading Dashboard...</div>
|
|
33
48
|
</div>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
</div>
|
|
50
|
+
<section class="tabs" aria-label="Codex FE tabs">
|
|
51
|
+
<menu role="tablist" aria-label="Codex FE tabs">
|
|
52
|
+
<template x-for="page in pages" :key="page.id">
|
|
53
|
+
<button role="tab" type="button" :aria-controls="page.tabId" :aria-selected="view === page.id"
|
|
54
|
+
:tabindex="view === page.id ? 0 : -1" @click="setView(page.id)" @focus="setView(page.id)">
|
|
55
|
+
<span x-text="page.label"></span>
|
|
56
|
+
</button>
|
|
57
|
+
</template>
|
|
58
|
+
</menu>
|
|
59
|
+
|
|
60
|
+
<article role="tabpanel" id="tab-dashboard" :hidden="view !== 'dashboard'">
|
|
61
|
+
<div class="hero-banner">
|
|
62
|
+
<h2>Quota remaining and routing health</h2>
|
|
63
|
+
<p>Live summary of remaining quota for Codex traffic routed through the ChatGPT-style backend. Account
|
|
64
|
+
status and load
|
|
65
|
+
balancing signals are surfaced here.</p>
|
|
66
|
+
<div class="badge-row">
|
|
67
|
+
<template x-for="badge in dashboard.badges" :key="badge.label">
|
|
68
|
+
<span class="status-pill" :class="badge.status" x-text="badge.label"></span>
|
|
69
|
+
</template>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="section-grid" style="margin-top: 12px">
|
|
74
|
+
<template x-for="stat in dashboard.stats" :key="stat.title">
|
|
75
|
+
<div class="panel">
|
|
76
|
+
<h3 x-text="stat.title"></h3>
|
|
77
|
+
<div class="stat-value" x-text="stat.value"></div>
|
|
78
|
+
<div class="stat-meta" x-text="stat.meta"></div>
|
|
79
|
+
</div>
|
|
41
80
|
</template>
|
|
42
|
-
</
|
|
81
|
+
</div>
|
|
43
82
|
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
83
|
+
<div class="two-column two-column--equal" style="margin-top: 12px">
|
|
84
|
+
<template x-for="donut in dashboard.donuts" :key="donut.title">
|
|
85
|
+
<div class="panel">
|
|
86
|
+
<h3 x-text="donut.title"></h3>
|
|
87
|
+
<div class="donut-wrap">
|
|
88
|
+
<div class="donut" :style="{ '--donut-data': donut.gradient }">
|
|
89
|
+
<div class="donut-center">
|
|
90
|
+
<strong x-text="donut.range"></strong>
|
|
91
|
+
<div x-text="donut.total"></div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<ul class="legend">
|
|
95
|
+
<template x-for="item in donut.items" :key="item.label">
|
|
96
|
+
<li>
|
|
97
|
+
<span class="legend-label">
|
|
98
|
+
<i :style="{ '--legend-color': item.color }"></i>
|
|
99
|
+
<span class="legend-label-text" x-text="item.label" :title="item.fullLabel"></span>
|
|
100
|
+
</span>
|
|
101
|
+
<span class="legend-detail">
|
|
102
|
+
<span class="legend-detail-label" x-text="item.detailLabel"></span>
|
|
103
|
+
<span class="legend-detail-value" :class="item.valueClass" x-text="item.detailValue"></span>
|
|
104
|
+
</span>
|
|
105
|
+
</li>
|
|
106
|
+
</template>
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
54
109
|
</div>
|
|
55
|
-
</
|
|
110
|
+
</template>
|
|
111
|
+
</div>
|
|
56
112
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
113
|
+
<section style="margin-top: 12px">
|
|
114
|
+
<h3>Account status</h3>
|
|
115
|
+
<div class="account-grid">
|
|
116
|
+
<template x-for="card in dashboard.accountCards" :key="card.email">
|
|
117
|
+
<div class="account-card">
|
|
118
|
+
<header>
|
|
119
|
+
<div>
|
|
120
|
+
<div x-text="`${card.email} (${card.plan})`" :title="`${card.email} (${card.plan})`"></div>
|
|
121
|
+
<div class="account-id" x-text="card.accountId"></div>
|
|
122
|
+
</div>
|
|
123
|
+
<span class="status-pill" :class="card.status.class" x-text="card.status.label"></span>
|
|
124
|
+
</header>
|
|
125
|
+
<div class="progress-row">
|
|
126
|
+
<label
|
|
127
|
+
x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
|
|
128
|
+
<template x-if="card.marquee">
|
|
129
|
+
<div role="progressbar" class="marquee"></div>
|
|
130
|
+
</template>
|
|
131
|
+
<template x-if="!card.marquee">
|
|
132
|
+
<div role="progressbar" :class="card.progressClass" aria-valuemin="0" aria-valuemax="100"
|
|
133
|
+
:aria-valuenow="card.remaining">
|
|
134
|
+
<div :style="{ width: `${card.remaining}%` }"></div>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
<span class="text-muted" x-text="card.remainingText"></span>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="text-muted" x-text="card.meta"></div>
|
|
140
|
+
<div class="account-actions">
|
|
141
|
+
<template x-for="action in card.actions" :key="action.label">
|
|
142
|
+
<button x-text="action.label" @click="handleAccountAction(action, card)"></button>
|
|
143
|
+
</template>
|
|
144
|
+
</div>
|
|
63
145
|
</div>
|
|
64
146
|
</template>
|
|
65
147
|
</div>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
150
|
+
<div class="panel" style="margin-top: 12px">
|
|
151
|
+
<h3>Recent requests</h3>
|
|
66
152
|
|
|
67
|
-
<div class="
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
153
|
+
<div class="controls-toolbar">
|
|
154
|
+
<div class="controls-group">
|
|
155
|
+
<input type="text" class="filter-input" placeholder="Search..." x-model="filtersDraft.search"
|
|
156
|
+
@keyup.enter="applyFilters()" style="width: 200px;">
|
|
157
|
+
|
|
158
|
+
<div class="single-select" x-data="{ open: false }" @click.outside="open = false">
|
|
159
|
+
<button type="button" class="filter-select single-select-trigger" @click="open = !open"
|
|
160
|
+
:aria-expanded="open">
|
|
161
|
+
<span x-text="timeframeLabel(filtersDraft.timeframe)"></span>
|
|
162
|
+
</button>
|
|
163
|
+
<div class="single-select-menu" x-show="open" x-transition.opacity>
|
|
164
|
+
<label class="single-select-item" :class="{ 'is-selected': filtersDraft.timeframe === 'all' }">
|
|
165
|
+
<input type="radio" name="timeframe" value="all" x-model="filtersDraft.timeframe"
|
|
166
|
+
@change="open = false">
|
|
167
|
+
<span>All time</span>
|
|
168
|
+
</label>
|
|
169
|
+
<label class="single-select-item" :class="{ 'is-selected': filtersDraft.timeframe === '1h' }">
|
|
170
|
+
<input type="radio" name="timeframe" value="1h" x-model="filtersDraft.timeframe"
|
|
171
|
+
@change="open = false">
|
|
172
|
+
<span>Last 1h</span>
|
|
173
|
+
</label>
|
|
174
|
+
<label class="single-select-item" :class="{ 'is-selected': filtersDraft.timeframe === '24h' }">
|
|
175
|
+
<input type="radio" name="timeframe" value="24h" x-model="filtersDraft.timeframe"
|
|
176
|
+
@change="open = false">
|
|
177
|
+
<span>Last 24h</span>
|
|
178
|
+
</label>
|
|
179
|
+
<label class="single-select-item" :class="{ 'is-selected': filtersDraft.timeframe === '7d' }">
|
|
180
|
+
<input type="radio" name="timeframe" value="7d" x-model="filtersDraft.timeframe"
|
|
181
|
+
@change="open = false">
|
|
182
|
+
<span>Last 7d</span>
|
|
183
|
+
</label>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
<div class="multi-select" x-data="{ open: false }" @click.outside="open = false">
|
|
189
|
+
<button type="button" class="filter-select multi-select-trigger" @click="open = !open"
|
|
190
|
+
:aria-expanded="open" :disabled="requestLogOptions.isLoading">
|
|
191
|
+
<span
|
|
192
|
+
x-text="multiSelectSummary(filtersDraft.accountIds, 'All accounts', 'account', 'accounts')"></span>
|
|
193
|
+
</button>
|
|
194
|
+
<div class="multi-select-menu" x-show="open" x-transition.opacity>
|
|
195
|
+
<div class="multi-select-scroller">
|
|
196
|
+
<div class="multi-select-actions">
|
|
197
|
+
<button type="button" class="multi-select-action"
|
|
198
|
+
@click="filtersDraft.accountIds = []; open = false">Clear</button>
|
|
76
199
|
</div>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<span class="
|
|
82
|
-
|
|
83
|
-
<span class="legend-label-text" x-text="item.label" :title="item.label"></span>
|
|
84
|
-
</span>
|
|
85
|
-
<span class="legend-detail">
|
|
86
|
-
<span class="legend-detail-label" x-text="item.detailLabel"></span>
|
|
87
|
-
<span class="legend-detail-value" x-text="item.detailValue"></span>
|
|
88
|
-
</span>
|
|
89
|
-
</li>
|
|
200
|
+
<template x-for="accountId in requestLogOptions.accountIds" :key="accountId">
|
|
201
|
+
<label class="multi-select-item">
|
|
202
|
+
<input type="checkbox" :checked="filtersDraft.accountIds.includes(accountId)"
|
|
203
|
+
@change="toggleMultiSelectValue('accountIds', accountId)">
|
|
204
|
+
<span class="multi-select-label" x-text="accountFilterLabel(accountId)"></span>
|
|
205
|
+
</label>
|
|
90
206
|
</template>
|
|
91
|
-
</
|
|
207
|
+
</div>
|
|
92
208
|
</div>
|
|
93
209
|
</div>
|
|
94
|
-
</template>
|
|
95
|
-
</div>
|
|
96
210
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
211
|
+
<div class="multi-select" x-data="{ open: false }" @click.outside="open = false">
|
|
212
|
+
<button type="button" class="filter-select multi-select-trigger" @click="open = !open"
|
|
213
|
+
:aria-expanded="open" :disabled="requestLogOptions.isLoading">
|
|
214
|
+
<span
|
|
215
|
+
x-text="multiSelectSummary(filtersDraft.modelOptions, 'All models', 'model', 'models')"></span>
|
|
216
|
+
</button>
|
|
217
|
+
<div class="multi-select-menu" x-show="open" x-transition.opacity>
|
|
218
|
+
<div class="multi-select-scroller">
|
|
219
|
+
<div class="multi-select-actions">
|
|
220
|
+
<button type="button" class="multi-select-action"
|
|
221
|
+
@click="filtersDraft.modelOptions = []; open = false">Clear</button>
|
|
106
222
|
</div>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
223
|
+
<template x-for="modelOption in requestLogOptions.modelOptions"
|
|
224
|
+
:key="modelOptionValue(modelOption)">
|
|
225
|
+
<label class="multi-select-item">
|
|
226
|
+
<input type="checkbox"
|
|
227
|
+
:checked="filtersDraft.modelOptions.includes(modelOptionValue(modelOption))"
|
|
228
|
+
@change="toggleMultiSelectValue('modelOptions', modelOptionValue(modelOption))">
|
|
229
|
+
<span class="multi-select-label" x-text="modelOptionLabel(modelOption)"></span>
|
|
230
|
+
</label>
|
|
114
231
|
</template>
|
|
115
|
-
<template x-if="!card.marquee">
|
|
116
|
-
<div role="progressbar" :class="card.progressClass" aria-valuemin="0" aria-valuemax="100"
|
|
117
|
-
:aria-valuenow="card.remaining">
|
|
118
|
-
<div :style="{ width: `${card.remaining}%` }"></div>
|
|
119
|
-
</div>
|
|
120
|
-
</template>
|
|
121
|
-
<span class="text-muted" x-text="card.remainingText"></span>
|
|
122
232
|
</div>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div class="multi-select" x-data="{ open: false }" @click.outside="open = false">
|
|
237
|
+
<button type="button" class="filter-select multi-select-trigger" @click="open = !open"
|
|
238
|
+
:aria-expanded="open">
|
|
239
|
+
<span x-text="multiSelectSummary(filtersDraft.statuses, 'All status', 'status', 'statuses')"></span>
|
|
240
|
+
</button>
|
|
241
|
+
<div class="multi-select-menu" x-show="open" x-transition.opacity>
|
|
242
|
+
<div class="multi-select-scroller">
|
|
243
|
+
<div class="multi-select-actions">
|
|
244
|
+
<button type="button" class="multi-select-action"
|
|
245
|
+
@click="filtersDraft.statuses = []; open = false">Clear</button>
|
|
246
|
+
</div>
|
|
247
|
+
<label class="multi-select-item">
|
|
248
|
+
<input type="checkbox" :checked="filtersDraft.statuses.includes('ok')"
|
|
249
|
+
@change="toggleMultiSelectValue('statuses', 'ok')">
|
|
250
|
+
<span class="multi-select-label">OK</span>
|
|
251
|
+
</label>
|
|
252
|
+
<label class="multi-select-item">
|
|
253
|
+
<input type="checkbox" :checked="filtersDraft.statuses.includes('rate_limit')"
|
|
254
|
+
@change="toggleMultiSelectValue('statuses', 'rate_limit')">
|
|
255
|
+
<span class="multi-select-label">Rate Limit</span>
|
|
256
|
+
</label>
|
|
257
|
+
<label class="multi-select-item">
|
|
258
|
+
<input type="checkbox" :checked="filtersDraft.statuses.includes('quota')"
|
|
259
|
+
@change="toggleMultiSelectValue('statuses', 'quota')">
|
|
260
|
+
<span class="multi-select-label">Quota</span>
|
|
261
|
+
</label>
|
|
262
|
+
<label class="multi-select-item">
|
|
263
|
+
<input type="checkbox" :checked="filtersDraft.statuses.includes('error')"
|
|
264
|
+
@change="toggleMultiSelectValue('statuses', 'error')">
|
|
265
|
+
<span class="multi-select-label">Error</span>
|
|
266
|
+
</label>
|
|
128
267
|
</div>
|
|
129
268
|
</div>
|
|
130
|
-
</
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<input type="number" class="filter-input" placeholder="Min Cost ($)" x-model="filtersDraft.minCost"
|
|
272
|
+
@keyup.enter="applyFilters()" style="width: 110px;" step="0.01">
|
|
273
|
+
|
|
274
|
+
<button type="button" class="filter-apply" @click="applyFilters()"
|
|
275
|
+
:disabled="recentRequestsState.isLoading">Apply</button>
|
|
131
276
|
</div>
|
|
132
|
-
</section>
|
|
133
277
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
278
|
+
<div class="controls-group">
|
|
279
|
+
<span style="font-size: 13px; color: var(--text-muted);">Show:</span>
|
|
280
|
+
<div class="single-select" x-data="{ open: false }" @click.outside="open = false">
|
|
281
|
+
<button type="button" class="filter-select single-select-trigger" @click="open = !open"
|
|
282
|
+
:aria-expanded="open" style="min-width: 60px">
|
|
283
|
+
<span x-text="pagination.limit"></span>
|
|
284
|
+
</button>
|
|
285
|
+
<div class="single-select-menu" x-show="open" x-transition.opacity style="min-width: 80px">
|
|
286
|
+
<label class="single-select-item" :class="{ 'is-selected': pagination.limit == 25 }">
|
|
287
|
+
<input type="radio" name="limit" value="25" x-model="pagination.limit" @change="open = false">
|
|
288
|
+
<span>25</span>
|
|
289
|
+
</label>
|
|
290
|
+
<label class="single-select-item" :class="{ 'is-selected': pagination.limit == 50 }">
|
|
291
|
+
<input type="radio" name="limit" value="50" x-model="pagination.limit" @change="open = false">
|
|
292
|
+
<span>50</span>
|
|
293
|
+
</label>
|
|
294
|
+
<label class="single-select-item" :class="{ 'is-selected': pagination.limit == 100 }">
|
|
295
|
+
<input type="radio" name="limit" value="100" x-model="pagination.limit" @change="open = false">
|
|
296
|
+
<span>100</span>
|
|
297
|
+
</label>
|
|
298
|
+
<label class="single-select-item" :class="{ 'is-selected': pagination.limit == 250 }">
|
|
299
|
+
<input type="radio" name="limit" value="250" x-model="pagination.limit" @change="open = false">
|
|
300
|
+
<span>250</span>
|
|
301
|
+
</label>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="table-wrap table-wrap--requests table-wrap--column-lines has-scrollbar">
|
|
308
|
+
<table>
|
|
309
|
+
<thead>
|
|
310
|
+
<tr>
|
|
311
|
+
<th class="highlighted indicator" style="width: 15%">Time</th>
|
|
312
|
+
<th style="width: 25%">Account</th>
|
|
313
|
+
<th style="width: 15%">Model</th>
|
|
314
|
+
<th style="width: 10%">Status</th>
|
|
315
|
+
<th style="width: 15%">Error</th>
|
|
316
|
+
<th style="width: 10%">Tokens</th>
|
|
317
|
+
<th style="width: 10%">Cost</th>
|
|
318
|
+
</tr>
|
|
319
|
+
</thead>
|
|
320
|
+
<tbody>
|
|
321
|
+
<template x-for="request in dashboard.requests" :key="request.key">
|
|
322
|
+
<tr>
|
|
323
|
+
<td>
|
|
324
|
+
<div x-text="request.time.time"></div>
|
|
325
|
+
<div x-text="request.time.date" class="text-muted" style="font-size: 11px;"></div>
|
|
326
|
+
</td>
|
|
327
|
+
<td class="cell-truncate" x-text="request.account" :title="request.account"></td>
|
|
328
|
+
<td class="cell-truncate" x-text="request.model" :title="request.model"></td>
|
|
329
|
+
<td><span class="status-pill" :class="request.status.class" x-text="request.status.label"></span>
|
|
330
|
+
</td>
|
|
331
|
+
<td class="cell-error">
|
|
332
|
+
<div class="error-cell" :class="request.isErrorPlaceholder ? 'placeholder' : ''"
|
|
333
|
+
x-data="{ expanded: false }">
|
|
334
|
+
<div class="error-text" :class="!expanded ? 'truncated' : ''"
|
|
335
|
+
x-text="expanded ? request.errorTitle : request.error"
|
|
336
|
+
:title="!expanded ? request.errorTitle : ''"></div>
|
|
337
|
+
<template x-if="request.isTruncated">
|
|
338
|
+
<button class="error-toggle" @click="expanded = !expanded"
|
|
339
|
+
x-text="expanded ? 'Less' : 'More'"></button>
|
|
340
|
+
</template>
|
|
341
|
+
</div>
|
|
342
|
+
</td>
|
|
343
|
+
<td>
|
|
344
|
+
<div x-text="request.tokens.total"></div>
|
|
345
|
+
<template x-if="request.tokens.cached">
|
|
346
|
+
<div x-text="`${request.tokens.cached} Cached`" class="text-muted" style="font-size: 11px;">
|
|
347
|
+
</div>
|
|
348
|
+
</template>
|
|
349
|
+
</td>
|
|
350
|
+
<td x-text="request.cost"></td>
|
|
351
|
+
</tr>
|
|
352
|
+
</template>
|
|
353
|
+
</tbody>
|
|
354
|
+
</table>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</article>
|
|
358
|
+
|
|
359
|
+
<article role="tabpanel" id="tab-accounts" :hidden="view !== 'accounts'">
|
|
360
|
+
<div class="two-column">
|
|
361
|
+
<div class="panel account-list-panel">
|
|
362
|
+
<h3>Account list</h3>
|
|
363
|
+
<div class="list-actions">
|
|
364
|
+
<div class="searchbox">
|
|
365
|
+
<input type="search" placeholder="Search accounts" aria-label="Search accounts" x-ref="accountSearch"
|
|
366
|
+
x-model="accounts.searchQuery" @keydown.escape.prevent="clearAccountSearch">
|
|
367
|
+
<button type="button" aria-label="search" title="Search accounts"
|
|
368
|
+
@click="focusAccountSearch"></button>
|
|
369
|
+
</div>
|
|
370
|
+
<label role="button" :class="{ 'is-disabled': importState.isLoading }" @click="logImportClick('label')">
|
|
371
|
+
<input type="file" class="file-input" accept="application/json" @click="logImportClick('input')"
|
|
372
|
+
@change="handleAuthImport($event)" :disabled="importState.isLoading">
|
|
373
|
+
Import auth.json
|
|
374
|
+
</label>
|
|
375
|
+
<button type="button" @click="openAddAccountDialog">Add account</button>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="table-wrap table-wrap--column-lines has-scrollbar">
|
|
137
378
|
<table>
|
|
138
379
|
<thead>
|
|
139
380
|
<tr>
|
|
140
|
-
<th class="highlighted indicator">
|
|
141
|
-
<th>
|
|
142
|
-
<th>
|
|
143
|
-
<th>Status</th>
|
|
144
|
-
<th>
|
|
145
|
-
|
|
146
|
-
|
|
381
|
+
<th class="highlighted indicator" style="width: 28%">Account ID</th>
|
|
382
|
+
<th style="width: 25%">Email</th>
|
|
383
|
+
<th style="width: 10%">Plan</th>
|
|
384
|
+
<th style="width: 12%">Status</th>
|
|
385
|
+
<th style="text-align: center; width: 12%">
|
|
386
|
+
<span style="display: inline-flex; flex-direction: column; align-items: center">
|
|
387
|
+
<span>Remaining</span>
|
|
388
|
+
<span style="font-weight: normal; font-size: 0.9em; color: var(--text-muted)"
|
|
389
|
+
x-text="`(${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
|
|
390
|
+
</span>
|
|
391
|
+
</span>
|
|
392
|
+
</th>
|
|
393
|
+
<th style="text-align: center; width: 13%">
|
|
394
|
+
<span style="display: inline-flex; flex-direction: column; align-items: center">
|
|
395
|
+
<span style="white-space: nowrap">Quota reset</span>
|
|
396
|
+
<span style="font-weight: normal; font-size: 0.9em; color: var(--text-muted)"
|
|
397
|
+
x-text="`(${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
|
|
398
|
+
</span>
|
|
399
|
+
</span>
|
|
400
|
+
</th>
|
|
147
401
|
</tr>
|
|
148
402
|
</thead>
|
|
149
403
|
<tbody>
|
|
150
|
-
<template x-for="
|
|
404
|
+
<template x-for="account in filteredAccounts" :key="account.id">
|
|
405
|
+
<tr :class="{ highlighted: account.id === accounts.selectedId }"
|
|
406
|
+
@click="selectAccount(account.id)" style="cursor: pointer">
|
|
407
|
+
<td class="cell-truncate" x-text="account.id" :title="account.id"></td>
|
|
408
|
+
<td class="cell-truncate" x-text="account.email" :title="account.email"></td>
|
|
409
|
+
<td x-text="planLabel(account.plan)"></td>
|
|
410
|
+
<td><span class="status-pill" :class="account.status"
|
|
411
|
+
x-text="statusLabel(account.status)"></span>
|
|
412
|
+
</td>
|
|
413
|
+
<td style="text-align: center"
|
|
414
|
+
:class="calculateTextUsageTextClass(account.status, account.usage?.secondaryRemainingPercent)"
|
|
415
|
+
x-text="formatPercent(account.usage?.secondaryRemainingPercent)">
|
|
416
|
+
</td>
|
|
417
|
+
<td style="text-align: center" x-text="formatQuotaResetLabel(account.resetAtSecondary)"></td>
|
|
418
|
+
</tr>
|
|
419
|
+
</template>
|
|
420
|
+
<template x-if="filteredAccounts.length === 0">
|
|
151
421
|
<tr>
|
|
152
|
-
<td
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
x-text="request.status.label"></span></td>
|
|
157
|
-
<td x-text="request.error" :title="request.errorTitle || ''"></td>
|
|
158
|
-
<td style="text-align: right" x-text="request.tokens"></td>
|
|
159
|
-
<td style="text-align: right" x-text="request.cost"></td>
|
|
422
|
+
<td colspan="6" class="text-muted" style="text-align: center">
|
|
423
|
+
<span
|
|
424
|
+
x-text="accounts.searchQuery ? 'No accounts match your search.' : 'No accounts available.'"></span>
|
|
425
|
+
</td>
|
|
160
426
|
</tr>
|
|
161
427
|
</template>
|
|
162
428
|
</tbody>
|
|
163
429
|
</table>
|
|
164
430
|
</div>
|
|
165
431
|
</div>
|
|
166
|
-
</article>
|
|
167
432
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<div class="
|
|
171
|
-
<
|
|
172
|
-
<div class="list-actions">
|
|
173
|
-
<div class="searchbox">
|
|
174
|
-
<input type="search" placeholder="Search accounts" aria-label="Search accounts"
|
|
175
|
-
x-ref="accountSearch" x-model="accounts.searchQuery" @keydown.escape.prevent="clearAccountSearch">
|
|
176
|
-
<button type="button" aria-label="search" title="Search accounts"
|
|
177
|
-
@click="focusAccountSearch"></button>
|
|
178
|
-
</div>
|
|
179
|
-
<label role="button" :class="{ 'is-disabled': importState.isLoading }"
|
|
180
|
-
@click="logImportClick('label')">
|
|
181
|
-
<input type="file" class="file-input" accept="application/json" @click="logImportClick('input')"
|
|
182
|
-
@change="handleAuthImport($event)" :disabled="importState.isLoading">
|
|
183
|
-
Import auth.json
|
|
184
|
-
</label>
|
|
185
|
-
<button type="button" @click="openAddAccountDialog">Add account</button>
|
|
433
|
+
<div class="panel">
|
|
434
|
+
<h3>Selected account</h3>
|
|
435
|
+
<div class="summary-list">
|
|
436
|
+
<div><span>Email</span><strong x-text="selectedAccount.email" :title="selectedAccount.email"></strong>
|
|
186
437
|
</div>
|
|
187
|
-
<div
|
|
188
|
-
<table>
|
|
189
|
-
<thead>
|
|
190
|
-
<tr>
|
|
191
|
-
<th class="highlighted indicator">Account ID</th>
|
|
192
|
-
<th>Email</th>
|
|
193
|
-
<th>Plan</th>
|
|
194
|
-
<th>Status</th>
|
|
195
|
-
<th style="text-align: right"
|
|
196
|
-
x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
|
|
197
|
-
</th>
|
|
198
|
-
<th
|
|
199
|
-
x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`">
|
|
200
|
-
</th>
|
|
201
|
-
</tr>
|
|
202
|
-
</thead>
|
|
203
|
-
<tbody>
|
|
204
|
-
<template x-for="account in filteredAccounts" :key="account.id">
|
|
205
|
-
<tr :class="{ highlighted: account.id === accounts.selectedId }"
|
|
206
|
-
@click="selectAccount(account.id)" style="cursor: pointer">
|
|
207
|
-
<td x-text="account.id"></td>
|
|
208
|
-
<td x-text="account.email"></td>
|
|
209
|
-
<td x-text="planLabel(account.plan)"></td>
|
|
210
|
-
<td><span class="status-pill" :class="account.status"
|
|
211
|
-
x-text="statusLabel(account.status)"></span>
|
|
212
|
-
</td>
|
|
213
|
-
<td style="text-align: right"
|
|
214
|
-
x-text="formatPercent(account.usage?.secondaryRemainingPercent)">
|
|
215
|
-
</td>
|
|
216
|
-
<td x-text="formatQuotaResetLabel(account.resetAtSecondary)"></td>
|
|
217
|
-
</tr>
|
|
218
|
-
</template>
|
|
219
|
-
<template x-if="filteredAccounts.length === 0">
|
|
220
|
-
<tr>
|
|
221
|
-
<td colspan="6" class="text-muted" style="text-align: center">
|
|
222
|
-
<span
|
|
223
|
-
x-text="accounts.searchQuery ? 'No accounts match your search.' : 'No accounts available.'"></span>
|
|
224
|
-
</td>
|
|
225
|
-
</tr>
|
|
226
|
-
</template>
|
|
227
|
-
</tbody>
|
|
228
|
-
</table>
|
|
438
|
+
<div><span>Account ID</span><strong x-text="selectedAccount.id" :title="selectedAccount.id"></strong>
|
|
229
439
|
</div>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<div><span>Email</span><strong x-text="selectedAccount.email"></strong></div>
|
|
236
|
-
<div><span>Account ID</span><strong x-text="selectedAccount.id"></strong></div>
|
|
237
|
-
<div><span>Plan</span><strong x-text="planLabel(selectedAccount.plan)"></strong></div>
|
|
238
|
-
<div>
|
|
239
|
-
<span>Status</span>
|
|
240
|
-
<span class="status-pill" :class="selectedAccount.status"
|
|
241
|
-
x-text="statusLabel(selectedAccount.status)"></span>
|
|
242
|
-
</div>
|
|
243
|
-
<div><span
|
|
244
|
-
x-text="`Quota reset (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></span><strong
|
|
245
|
-
x-text="formatQuotaResetLabel(selectedAccount.resetAtPrimary)"></strong></div>
|
|
246
|
-
<div><span
|
|
247
|
-
x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></span><strong
|
|
248
|
-
x-text="formatQuotaResetLabel(selectedAccount.resetAtSecondary)"></strong></div>
|
|
440
|
+
<div><span>Plan</span><strong x-text="planLabel(selectedAccount.plan)"></strong></div>
|
|
441
|
+
<div>
|
|
442
|
+
<span>Status</span>
|
|
443
|
+
<span class="status-pill" :class="selectedAccount.status"
|
|
444
|
+
x-text="statusLabel(selectedAccount.status)"></span>
|
|
249
445
|
</div>
|
|
446
|
+
<div><span
|
|
447
|
+
x-text="`Quota reset (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></span><strong
|
|
448
|
+
x-text="formatQuotaResetLabel(selectedAccount.resetAtPrimary)"></strong></div>
|
|
449
|
+
<div><span
|
|
450
|
+
x-text="`Quota reset (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></span><strong
|
|
451
|
+
x-text="formatQuotaResetLabel(selectedAccount.resetAtSecondary)"></strong></div>
|
|
452
|
+
</div>
|
|
250
453
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
454
|
+
<div class="split-stack" style="margin-top: 12px">
|
|
455
|
+
<div class="progress-row">
|
|
456
|
+
<label
|
|
457
|
+
x-text="`Remaining (${formatWindowLabel('primary', dashboardData.usage.primary.windowMinutes)})`"></label>
|
|
458
|
+
<div role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
|
459
|
+
:class="calculateProgressClass(selectedAccount.status, selectedAccount.usage?.primaryRemainingPercent)"
|
|
460
|
+
:aria-valuenow="formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)">
|
|
461
|
+
<div :style="{ width: `${formatPercentValue(selectedAccount.usage?.primaryRemainingPercent)}%` }">
|
|
259
462
|
</div>
|
|
260
|
-
<span class="text-muted"
|
|
261
|
-
x-text="formatPercent(selectedAccount.usage?.primaryRemainingPercent)"></span>
|
|
262
|
-
</div>
|
|
263
|
-
<div class="progress-row">
|
|
264
|
-
<label
|
|
265
|
-
x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
|
|
266
|
-
<div role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
|
267
|
-
:aria-valuenow="formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)">
|
|
268
|
-
<div
|
|
269
|
-
:style="{ width: `${formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)}%` }">
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
<span class="text-muted"
|
|
273
|
-
x-text="formatPercent(selectedAccount.usage?.secondaryRemainingPercent)"></span>
|
|
274
463
|
</div>
|
|
464
|
+
<span
|
|
465
|
+
:class="calculateTextUsageTextClass(selectedAccount.status, selectedAccount.usage?.primaryRemainingPercent)"
|
|
466
|
+
x-text="formatPercent(selectedAccount.usage?.primaryRemainingPercent)"></span>
|
|
275
467
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<div
|
|
280
|
-
|
|
468
|
+
<div class="progress-row">
|
|
469
|
+
<label
|
|
470
|
+
x-text="`Remaining (${formatWindowLabel('secondary', dashboardData.usage.secondary.windowMinutes)})`"></label>
|
|
471
|
+
<div role="progressbar" aria-valuemin="0" aria-valuemax="100"
|
|
472
|
+
:class="calculateProgressClass(selectedAccount.status, selectedAccount.usage?.secondaryRemainingPercent)"
|
|
473
|
+
:aria-valuenow="formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)">
|
|
474
|
+
<div :style="{ width: `${formatPercentValue(selectedAccount.usage?.secondaryRemainingPercent)}%` }">
|
|
281
475
|
</div>
|
|
282
|
-
<div><span>Refresh token</span><span x-text="formatRefreshTokenLabel(selectedAccount.auth)"></span>
|
|
283
|
-
</div>
|
|
284
|
-
<div><span>Id token</span><span x-text="formatIdTokenLabel(selectedAccount.auth)"></span></div>
|
|
285
476
|
</div>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<template x-if="selectedAccount.status === 'deactivated'">
|
|
290
|
-
<button @click="startReauthFlow">Re-authenticate</button>
|
|
291
|
-
</template>
|
|
292
|
-
<template x-if="selectedAccount.status === 'paused'">
|
|
293
|
-
<button @click="resumeSelectedAccount">Resume</button>
|
|
294
|
-
</template>
|
|
295
|
-
<template x-if="selectedAccount.status !== 'deactivated' && selectedAccount.status !== 'paused'">
|
|
296
|
-
<button @click="pauseSelectedAccount">Pause</button>
|
|
297
|
-
</template>
|
|
298
|
-
<button @click="deleteSelectedAccount">Delete</button>
|
|
477
|
+
<span
|
|
478
|
+
:class="calculateTextUsageTextClass(selectedAccount.status, selectedAccount.usage?.secondaryRemainingPercent)"
|
|
479
|
+
x-text="formatPercent(selectedAccount.usage?.secondaryRemainingPercent)"></span>
|
|
299
480
|
</div>
|
|
300
481
|
</div>
|
|
301
|
-
</div>
|
|
302
|
-
</article>
|
|
303
|
-
|
|
304
|
-
<article role="tabpanel" id="tab-settings" :hidden="view !== 'settings'">
|
|
305
|
-
<div class="panel">
|
|
306
|
-
<h3>Routing settings</h3>
|
|
307
|
-
<p class="text-muted">Toggle routing features. When both options are off, accounts are selected by
|
|
308
|
-
balancing usage evenly.</p>
|
|
309
482
|
|
|
310
|
-
<fieldset
|
|
311
|
-
<legend>
|
|
312
|
-
<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
x-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
<label for="sticky-threads-toggle">Enable sticky threads (reuse the same upstream account per conversation)</label>
|
|
319
|
-
<div id="sticky-threads-help" class="text-muted" style="margin-top: 6px">
|
|
320
|
-
When enabled, requests with a prompt cache key stay pinned to the same upstream account unless the
|
|
321
|
-
pinned account becomes unavailable.
|
|
322
|
-
</div>
|
|
323
|
-
</fieldset>
|
|
324
|
-
|
|
325
|
-
<fieldset style="margin-top: 12px">
|
|
326
|
-
<legend>Reset priority</legend>
|
|
327
|
-
<input
|
|
328
|
-
id="reset-priority-toggle"
|
|
329
|
-
type="checkbox"
|
|
330
|
-
x-model="settings.preferEarlierResetAccounts"
|
|
331
|
-
aria-describedby="reset-priority-help"
|
|
332
|
-
>
|
|
333
|
-
<label for="reset-priority-toggle">Prefer accounts that reset earlier first</label>
|
|
334
|
-
<div id="reset-priority-help" class="text-muted" style="margin-top: 6px">
|
|
335
|
-
When enabled, the load balancer prefers accounts whose secondary quota resets sooner, then balances
|
|
336
|
-
usage.
|
|
483
|
+
<fieldset class="token-fieldset">
|
|
484
|
+
<legend>Tokens</legend>
|
|
485
|
+
<div class="summary-list">
|
|
486
|
+
<div><span>Access token</span><span x-text="formatAccessTokenLabel(selectedAccount.auth)"></span>
|
|
487
|
+
</div>
|
|
488
|
+
<div><span>Refresh token</span><span x-text="formatRefreshTokenLabel(selectedAccount.auth)"></span>
|
|
489
|
+
</div>
|
|
490
|
+
<div><span>Id token</span><span x-text="formatIdTokenLabel(selectedAccount.auth)"></span></div>
|
|
337
491
|
</div>
|
|
338
492
|
</fieldset>
|
|
339
493
|
|
|
340
494
|
<div class="inline-actions" style="margin-top: 12px">
|
|
341
|
-
<
|
|
342
|
-
<
|
|
343
|
-
</
|
|
495
|
+
<template x-if="selectedAccount.status === 'deactivated'">
|
|
496
|
+
<button @click="startReauthFlow">Re-authenticate</button>
|
|
497
|
+
</template>
|
|
498
|
+
<template x-if="selectedAccount.status === 'paused'">
|
|
499
|
+
<button @click="resumeSelectedAccount">Resume</button>
|
|
500
|
+
</template>
|
|
501
|
+
<template x-if="selectedAccount.status !== 'deactivated' && selectedAccount.status !== 'paused'">
|
|
502
|
+
<button @click="pauseSelectedAccount">Pause</button>
|
|
503
|
+
</template>
|
|
504
|
+
<button @click="deleteSelectedAccount">Delete</button>
|
|
344
505
|
</div>
|
|
345
506
|
</div>
|
|
346
|
-
</article>
|
|
347
|
-
</section>
|
|
348
|
-
</div>
|
|
349
|
-
<div class="status-bar">
|
|
350
|
-
<template x-for="item in statusItems" :key="item">
|
|
351
|
-
<p class="status-bar-field" x-text="item"></p>
|
|
352
|
-
</template>
|
|
353
|
-
</div>
|
|
354
|
-
<div class="dialog-backdrop auth-dialog-backdrop" :class="{ 'is-open': authDialog.open }"
|
|
355
|
-
:aria-hidden="!authDialog.open"></div>
|
|
356
|
-
<div class="window active is-bright auth-dialog" role="dialog" aria-modal="true"
|
|
357
|
-
aria-labelledby="add-account-title" :aria-hidden="!authDialog.open" :class="{ 'is-open': authDialog.open }">
|
|
358
|
-
<div class="title-bar">
|
|
359
|
-
<div class="title-bar-text" id="add-account-title">Add account</div>
|
|
360
|
-
<div class="title-bar-controls">
|
|
361
|
-
<button aria-label="Close" @click="closeAddAccountDialog"></button>
|
|
362
507
|
</div>
|
|
363
|
-
</
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<div class="auth-dialog__option-meta text-muted">Use when running in Docker or remote environments.
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
</fieldset>
|
|
384
|
-
</div>
|
|
385
|
-
</template>
|
|
386
|
-
<template x-if="authDialog.stage === 'browser'">
|
|
387
|
-
<div class="auth-dialog__panel">
|
|
388
|
-
<p class="auth-dialog__lead">Open the authorization page to complete sign-in.</p>
|
|
389
|
-
<div class="auth-dialog__detail">
|
|
390
|
-
<div class="auth-dialog__label">Authorization URL</div>
|
|
391
|
-
<div class="auth-dialog__value auth-dialog__link" x-text="authDialog.authorizationUrl"></div>
|
|
392
|
-
</div>
|
|
393
|
-
<div class="auth-dialog__status">
|
|
394
|
-
<span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
|
|
395
|
-
<span x-text="authDialog.statusLabel"></span>
|
|
396
|
-
</div>
|
|
397
|
-
</div>
|
|
398
|
-
</template>
|
|
399
|
-
<template x-if="authDialog.stage === 'device'">
|
|
400
|
-
<div class="auth-dialog__panel">
|
|
401
|
-
<p class="auth-dialog__lead">Use the device code to sign in. Enter the code at the verification URL.</p>
|
|
402
|
-
<ol class="auth-dialog__steps">
|
|
403
|
-
<li>Open the verification URL.</li>
|
|
404
|
-
<li>Enter the user code shown below.</li>
|
|
405
|
-
<li>Return here to finish the sign-in.</li>
|
|
406
|
-
</ol>
|
|
407
|
-
<div class="auth-dialog__device-grid">
|
|
408
|
-
<div>
|
|
409
|
-
<div class="auth-dialog__label">User code</div>
|
|
410
|
-
<div class="auth-dialog__code-value" x-text="authDialog.userCode"></div>
|
|
411
|
-
</div>
|
|
412
|
-
<div>
|
|
413
|
-
<div class="auth-dialog__label">Verification URL</div>
|
|
414
|
-
<div class="auth-dialog__value auth-dialog__link" x-text="authDialog.verificationUrl"></div>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
<div class="auth-dialog__device-meta text-muted">
|
|
418
|
-
Expires in <span x-text="formatCountdown(authDialog.remainingSeconds)"></span>
|
|
508
|
+
</article>
|
|
509
|
+
|
|
510
|
+
<article role="tabpanel" id="tab-settings" :hidden="view !== 'settings'">
|
|
511
|
+
<div class="panel">
|
|
512
|
+
<h3>Routing settings</h3>
|
|
513
|
+
<p class="text-muted">Toggle routing features. When both options are off, accounts are selected by
|
|
514
|
+
balancing usage evenly.</p>
|
|
515
|
+
|
|
516
|
+
<fieldset class="settings-fieldset">
|
|
517
|
+
<legend>Sticky threads</legend>
|
|
518
|
+
<input id="sticky-threads-toggle" type="checkbox" x-model="settings.stickyThreadsEnabled"
|
|
519
|
+
aria-describedby="sticky-threads-help">
|
|
520
|
+
<label for="sticky-threads-toggle">Enable sticky threads (reuse the same upstream account per
|
|
521
|
+
conversation)</label>
|
|
522
|
+
<div id="sticky-threads-help" class="text-muted" style="margin-top: 6px">
|
|
523
|
+
When enabled, requests with a prompt cache key stay pinned to the same upstream account unless the
|
|
524
|
+
pinned account becomes unavailable.
|
|
419
525
|
</div>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
526
|
+
</fieldset>
|
|
527
|
+
|
|
528
|
+
<fieldset class="settings-fieldset">
|
|
529
|
+
<legend>Reset priority</legend>
|
|
530
|
+
<input id="reset-priority-toggle" type="checkbox" x-model="settings.preferEarlierResetAccounts"
|
|
531
|
+
aria-describedby="reset-priority-help">
|
|
532
|
+
<label for="reset-priority-toggle">Prefer accounts that reset earlier first</label>
|
|
533
|
+
<div id="reset-priority-help" class="text-muted" style="margin-top: 6px">
|
|
534
|
+
When enabled, the load balancer prefers accounts whose secondary quota resets sooner, then balances
|
|
535
|
+
usage.
|
|
423
536
|
</div>
|
|
537
|
+
</fieldset>
|
|
538
|
+
|
|
539
|
+
<div class="inline-actions" style="margin-top: 12px">
|
|
540
|
+
<button type="button" @click="saveSettings" :disabled="settings.isSaving">
|
|
541
|
+
<span>Save</span>
|
|
542
|
+
</button>
|
|
424
543
|
</div>
|
|
425
|
-
</
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
544
|
+
</div>
|
|
545
|
+
</article>
|
|
546
|
+
</section>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="status-bar">
|
|
549
|
+
<template x-for="item in statusItems" :key="item">
|
|
550
|
+
<p class="status-bar-field" x-text="item"></p>
|
|
551
|
+
</template>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="dialog-backdrop auth-dialog-backdrop" :class="{ 'is-open': authDialog.open }"
|
|
554
|
+
:aria-hidden="!authDialog.open"></div>
|
|
555
|
+
<div class="window active is-bright auth-dialog" role="dialog" aria-modal="true" aria-labelledby="add-account-title"
|
|
556
|
+
:aria-hidden="!authDialog.open" :class="{ 'is-open': authDialog.open }">
|
|
557
|
+
<div class="title-bar">
|
|
558
|
+
<div class="title-bar-text" id="add-account-title">Add account</div>
|
|
559
|
+
<div class="title-bar-controls">
|
|
560
|
+
<button aria-label="Close" @click="closeAddAccountDialog"></button>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="window-body has-space auth-dialog__body">
|
|
564
|
+
<template x-if="authDialog.stage === 'intro'">
|
|
565
|
+
<div class="auth-dialog__intro">
|
|
566
|
+
<p class="auth-dialog__lead">Sign in with ChatGPT OAuth. API keys are not supported.</p>
|
|
567
|
+
<fieldset class="auth-dialog__methods">
|
|
568
|
+
<legend>Sign-in method</legend>
|
|
569
|
+
<div class="auth-dialog__option" @click="authDialog.selectedMethod = 'browser'">
|
|
570
|
+
<input id="auth-method-browser" type="radio" name="auth-method" value="browser"
|
|
571
|
+
x-model="authDialog.selectedMethod">
|
|
572
|
+
<label for="auth-method-browser">Browser login (PKCE)</label>
|
|
573
|
+
<div class="auth-dialog__option-meta text-muted">Requires local callback on port 1455.</div>
|
|
430
574
|
</div>
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
<
|
|
435
|
-
<
|
|
575
|
+
<div class="auth-dialog__option" @click="authDialog.selectedMethod = 'device'">
|
|
576
|
+
<input id="auth-method-device" type="radio" name="auth-method" value="device"
|
|
577
|
+
x-model="authDialog.selectedMethod">
|
|
578
|
+
<label for="auth-method-device">Device code</label>
|
|
579
|
+
<div class="auth-dialog__option-meta text-muted">Use when running in Docker or remote environments.
|
|
580
|
+
</div>
|
|
436
581
|
</div>
|
|
437
|
-
</
|
|
582
|
+
</fieldset>
|
|
438
583
|
</div>
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
<button type="button" @click="resetAuthDialogState">Change method</button>
|
|
452
|
-
<button type="button" @click="copyToClipboard(authDialog.userCode, 'User code')"
|
|
453
|
-
:disabled="!authDialog.userCode">Copy code</button>
|
|
454
|
-
<button type="button" @click="openVerificationUrl" :disabled="!authDialog.verificationUrl">Open link
|
|
455
|
-
</button>
|
|
456
|
-
</div>
|
|
457
|
-
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'success'">
|
|
458
|
-
<button type="button" class="default" @click="closeAddAccountDialog">Done</button>
|
|
459
|
-
</div>
|
|
460
|
-
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'error'">
|
|
461
|
-
<button type="button" @click="resetAuthDialogState">Try again</button>
|
|
462
|
-
<button type="button" class="default" @click="closeAddAccountDialog">Close</button>
|
|
584
|
+
</template>
|
|
585
|
+
<template x-if="authDialog.stage === 'browser'">
|
|
586
|
+
<div class="auth-dialog__panel">
|
|
587
|
+
<p class="auth-dialog__lead">Open the authorization page to complete sign-in.</p>
|
|
588
|
+
<div class="auth-dialog__detail">
|
|
589
|
+
<div class="auth-dialog__label">Authorization URL</div>
|
|
590
|
+
<div class="url-copy-row">
|
|
591
|
+
<a class="auth-dialog__value auth-dialog__link" :href="authDialog.authorizationUrl" target="_blank"
|
|
592
|
+
x-text="authDialog.authorizationUrl"></a>
|
|
593
|
+
<button type="button" class="copy-btn"
|
|
594
|
+
@click="copyToClipboard(authDialog.authorizationUrl, 'Authorization URL', $event)">Copy</button>
|
|
595
|
+
</div>
|
|
463
596
|
</div>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
@click="closeMessageBox"></div>
|
|
468
|
-
<div class="window active is-bright message-dialog" role="dialog" aria-modal="true"
|
|
469
|
-
aria-labelledby="message-box-title" aria-describedby="message-box-body" :aria-hidden="!messageBox.open"
|
|
470
|
-
:class="{ 'is-open': messageBox.open }">
|
|
471
|
-
<div class="title-bar">
|
|
472
|
-
<div class="title-bar-text" id="message-box-title" x-text="messageBox.title"></div>
|
|
473
|
-
<div class="title-bar-controls">
|
|
474
|
-
<button aria-label="Close" @click="closeMessageBox"></button>
|
|
597
|
+
<div class="auth-dialog__status">
|
|
598
|
+
<span class="auth-dialog__status-text" x-text="authDialog.statusLabel"></span>
|
|
599
|
+
<span class="spinner animate" aria-hidden="true" x-show="authDialog.status === 'pending'"></span>
|
|
475
600
|
</div>
|
|
476
601
|
</div>
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
602
|
+
</template>
|
|
603
|
+
<template x-if="authDialog.stage === 'device'">
|
|
604
|
+
<div class="auth-dialog__panel">
|
|
605
|
+
<p class="auth-dialog__lead">Use the device code to sign in. Enter the code at the verification URL.</p>
|
|
606
|
+
<ol class="auth-dialog__steps">
|
|
607
|
+
<li>Open the verification URL.</li>
|
|
608
|
+
<li>Enter the user code shown below.</li>
|
|
609
|
+
<li>Return here to finish the sign-in.</li>
|
|
610
|
+
</ol>
|
|
611
|
+
<div class="auth-dialog__device-grid">
|
|
612
|
+
<div>
|
|
613
|
+
<div class="auth-dialog__label">User code</div>
|
|
614
|
+
<div class="auth-dialog__code-value">
|
|
615
|
+
<span x-text="authDialog.userCode"></span>
|
|
616
|
+
<button type="button" class="copy-btn"
|
|
617
|
+
@click="copyToClipboard(authDialog.userCode, 'User code', $event)">Copy</button>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
<div>
|
|
621
|
+
<div class="auth-dialog__label">Verification URL</div>
|
|
622
|
+
<div class="url-copy-row">
|
|
623
|
+
<a class="auth-dialog__value auth-dialog__link" :href="authDialog.verificationUrl" target="_blank"
|
|
624
|
+
x-text="authDialog.verificationUrl"></a>
|
|
625
|
+
<button type="button" class="copy-btn"
|
|
626
|
+
@click="copyToClipboard(authDialog.verificationUrl, 'Verification URL', $event)">Copy</button>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="auth-dialog__status">
|
|
631
|
+
<span class="auth-dialog__status-text" x-text="authDialog.statusLabel"></span>
|
|
632
|
+
<div class="auth-dialog__status-meta" x-show="authDialog.status === 'pending'">
|
|
633
|
+
<span class="spinner animate" aria-hidden="true"></span>
|
|
634
|
+
<span class="text-muted">Expires in <span
|
|
635
|
+
x-text="formatCountdown(authDialog.remainingSeconds)"></span></span>
|
|
486
636
|
</div>
|
|
487
637
|
</div>
|
|
488
638
|
</div>
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
639
|
+
</template>
|
|
640
|
+
<template x-if="authDialog.stage === 'success'">
|
|
641
|
+
<div class="auth-dialog__panel">
|
|
642
|
+
<p class="auth-dialog__lead">Account linked successfully.</p>
|
|
643
|
+
<p class="text-muted">Return to the dashboard and select the new account.</p>
|
|
644
|
+
</div>
|
|
645
|
+
</template>
|
|
646
|
+
<template x-if="authDialog.stage === 'error'">
|
|
647
|
+
<div class="auth-dialog__panel auth-dialog__error">
|
|
648
|
+
<p class="auth-dialog__lead">Authorization failed.</p>
|
|
649
|
+
<p class="text-muted" x-text="authDialog.errorMessage"></p>
|
|
650
|
+
</div>
|
|
651
|
+
</template>
|
|
652
|
+
</div>
|
|
653
|
+
<footer class="auth-dialog__actions">
|
|
654
|
+
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'intro'">
|
|
655
|
+
<button type="button" @click="closeAddAccountDialog">Cancel</button>
|
|
656
|
+
<button type="button" class="default" @click="startOAuth" :disabled="authDialog.isLoading">Start sign-in
|
|
657
|
+
</button>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'browser'">
|
|
660
|
+
<button type="button" @click="resetAuthDialogState">Change method</button>
|
|
661
|
+
<button type="button" class="default" @click="openAuthorizationUrl"
|
|
662
|
+
:disabled="!authDialog.authorizationUrl">Open sign-in page</button>
|
|
663
|
+
</div>
|
|
664
|
+
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'device'">
|
|
665
|
+
<button type="button" @click="resetAuthDialogState">Change method</button>
|
|
666
|
+
<button type="button" class="default" @click="openVerificationUrl" :disabled="!authDialog.verificationUrl">Open
|
|
667
|
+
link</button>
|
|
668
|
+
</div>
|
|
669
|
+
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'success'">
|
|
670
|
+
<button type="button" class="default" @click="closeAddAccountDialog">Done</button>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="auth-dialog__actions-row" x-show="authDialog.stage === 'error'">
|
|
673
|
+
<button type="button" @click="resetAuthDialogState">Try again</button>
|
|
674
|
+
<button type="button" class="default" @click="closeAddAccountDialog">Close</button>
|
|
675
|
+
</div>
|
|
676
|
+
</footer>
|
|
677
|
+
</div>
|
|
678
|
+
<div class="dialog-backdrop" :class="{ 'is-open': messageBox.open }" :aria-hidden="!messageBox.open"
|
|
679
|
+
@click="closeMessageBox"></div>
|
|
680
|
+
<div class="window active is-bright message-dialog" role="dialog" aria-modal="true"
|
|
681
|
+
aria-labelledby="message-box-title" aria-describedby="message-box-body" :aria-hidden="!messageBox.open"
|
|
682
|
+
:class="{ 'is-open': messageBox.open }">
|
|
683
|
+
<div class="title-bar">
|
|
684
|
+
<div class="title-bar-text" id="message-box-title" x-text="messageBox.title"></div>
|
|
685
|
+
<div class="title-bar-controls">
|
|
686
|
+
<button aria-label="Close" @click="closeMessageBox"></button>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
<div class="window-body has-space message-dialog__body">
|
|
690
|
+
<div class="message-dialog__content">
|
|
691
|
+
<div class="message-dialog__icon" :class="messageBox.iconTone ? `is-${messageBox.iconTone}` : ''"
|
|
692
|
+
aria-hidden="true" x-show="messageBox.iconTone"></div>
|
|
693
|
+
<div class="message-dialog__copy">
|
|
694
|
+
<p id="message-box-body" x-text="messageBox.message"></p>
|
|
695
|
+
<template x-if="messageBox.details">
|
|
696
|
+
<pre class="code-box message-dialog__details" x-text="messageBox.details"></pre>
|
|
697
|
+
</template>
|
|
698
|
+
</div>
|
|
497
699
|
</div>
|
|
498
700
|
</div>
|
|
701
|
+
<footer class="message-dialog__actions">
|
|
702
|
+
<button x-show="messageBox.mode === 'confirm'" type="button" @click="cancelMessageBox"
|
|
703
|
+
x-text="messageBox.cancelLabel"></button>
|
|
704
|
+
<button x-show="messageBox.mode === 'confirm'" type="button" class="default" @click="confirmMessageBox"
|
|
705
|
+
x-text="messageBox.confirmLabel"></button>
|
|
706
|
+
<button x-show="messageBox.mode !== 'confirm'" type="button" class="default" @click="closeMessageBox">OK</button>
|
|
707
|
+
</footer>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</main>
|
|
499
711
|
</div>
|
|
500
712
|
<script defer src="/dashboard/index.js"></script>
|
|
501
713
|
<script defer src="https://unpkg.com/alpinejs@3.13.2/dist/cdn.min.js"></script>
|
|
502
714
|
</body>
|
|
503
715
|
|
|
504
|
-
</html>
|
|
716
|
+
</html>
|