codex-claude-proxy 1.0.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +274 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +201 -0
- package/docs/OPENCLAW.md +338 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +44 -0
- package/public/css/style.css +791 -0
- package/public/index.html +783 -0
- package/public/js/app.js +511 -0
- package/src/account-manager.js +483 -0
- package/src/claude-config.js +143 -0
- package/src/cli/accounts.js +413 -0
- package/src/cli/index.js +66 -0
- package/src/direct-api.js +123 -0
- package/src/format-converter.js +331 -0
- package/src/index.js +41 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +270 -0
- package/src/kilo-streamer.js +198 -0
- package/src/model-api.js +189 -0
- package/src/oauth.js +554 -0
- package/src/response-streamer.js +329 -0
- package/src/routes/api-routes.js +1035 -0
- package/src/server-settings.js +48 -0
- package/src/server.js +30 -0
- package/src/utils/logger.js +156 -0
package/public/js/app.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
document.addEventListener('alpine:init', () => {
|
|
2
|
+
Alpine.data('app', () => ({
|
|
3
|
+
version: '2.0.0',
|
|
4
|
+
connectionStatus: 'connecting',
|
|
5
|
+
activeTab: 'dashboard',
|
|
6
|
+
sidebarOpen: window.innerWidth >= 1024,
|
|
7
|
+
loading: false,
|
|
8
|
+
toast: null,
|
|
9
|
+
currentTime: '',
|
|
10
|
+
|
|
11
|
+
accounts: [],
|
|
12
|
+
searchQuery: '',
|
|
13
|
+
stats: { total: 0, active: 0, expired: 0, planType: '-' },
|
|
14
|
+
|
|
15
|
+
haikuKiloModel: 'glm-5',
|
|
16
|
+
haikuModelSaving: false,
|
|
17
|
+
|
|
18
|
+
showAddModal: false,
|
|
19
|
+
showDeleteModal: false,
|
|
20
|
+
deleteTarget: '',
|
|
21
|
+
showQuotaModalView: false,
|
|
22
|
+
selectedAccount: null,
|
|
23
|
+
|
|
24
|
+
oauthManualMode: false,
|
|
25
|
+
oauthManualUrl: '',
|
|
26
|
+
oauthManualVerifier: '',
|
|
27
|
+
oauthManualCode: '',
|
|
28
|
+
|
|
29
|
+
testPrompt: 'Say hello',
|
|
30
|
+
testResponse: '',
|
|
31
|
+
testing: false,
|
|
32
|
+
|
|
33
|
+
haikuTestPrompt: 'Say hello',
|
|
34
|
+
haikuTestResponse: '',
|
|
35
|
+
haikuTesting: false,
|
|
36
|
+
|
|
37
|
+
haikuModelLabel() {
|
|
38
|
+
return this.haikuKiloModel === 'minimax-2.5' ? 'MiniMax M2.5' : 'GLM-5';
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async testHaikuChat() {
|
|
42
|
+
if (!this.haikuTestPrompt.trim()) return;
|
|
43
|
+
this.haikuTesting = true;
|
|
44
|
+
this.haikuTestResponse = '';
|
|
45
|
+
const { ok, data } = await this.api('/v1/chat/completions', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
model: 'claude-haiku-4',
|
|
49
|
+
messages: [{ role: 'user', content: this.haikuTestPrompt }]
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
this.haikuTesting = false;
|
|
53
|
+
if (ok && data.choices) {
|
|
54
|
+
this.haikuTestResponse = data.choices[0].message.content;
|
|
55
|
+
} else {
|
|
56
|
+
this.haikuTestResponse = data?.error?.message || 'Request failed';
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
configPath: '~/.codex-claude-proxy/accounts.json',
|
|
61
|
+
|
|
62
|
+
logs: [],
|
|
63
|
+
logSearchQuery: '',
|
|
64
|
+
logFilters: { INFO: true, SUCCESS: true, WARN: true, ERROR: true, DEBUG: false },
|
|
65
|
+
logEventSource: null,
|
|
66
|
+
|
|
67
|
+
get filteredLogs() {
|
|
68
|
+
const query = this.logSearchQuery.trim().toLowerCase();
|
|
69
|
+
return this.logs.filter(log => {
|
|
70
|
+
if (!this.logFilters[log.level]) return false;
|
|
71
|
+
if (query && !log.message.toLowerCase().includes(query)) return false;
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
get filteredAccounts() {
|
|
77
|
+
if (!this.searchQuery) return this.accounts;
|
|
78
|
+
const q = this.searchQuery.toLowerCase();
|
|
79
|
+
return this.accounts.filter(a => a.email.toLowerCase().includes(q));
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
init() {
|
|
83
|
+
this.updateTime();
|
|
84
|
+
setInterval(() => this.updateTime(), 1000);
|
|
85
|
+
this.refreshAccounts();
|
|
86
|
+
this.checkHealth();
|
|
87
|
+
setInterval(() => this.checkHealth(), 30000);
|
|
88
|
+
this.startLogStream();
|
|
89
|
+
this.loadHaikuModelSetting();
|
|
90
|
+
|
|
91
|
+
window.addEventListener('resize', () => {
|
|
92
|
+
this.sidebarOpen = window.innerWidth >= 1024;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
window.addEventListener('message', (event) => {
|
|
96
|
+
if (event.data && event.data.type === 'oauth-success') {
|
|
97
|
+
this.showToast(`Account ${event.data.email} added!`, 'success');
|
|
98
|
+
this.showAddModal = false;
|
|
99
|
+
this.refreshAccounts();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
updateTime() {
|
|
105
|
+
this.currentTime = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
setActiveTab(tab) {
|
|
109
|
+
this.activeTab = tab;
|
|
110
|
+
if (window.innerWidth < 1024) {
|
|
111
|
+
this.sidebarOpen = false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async api(endpoint, options = {}) {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(endpoint, {
|
|
118
|
+
headers: { 'Content-Type': 'application/json' },
|
|
119
|
+
...options
|
|
120
|
+
});
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
return { ok: response.ok, data };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { ok: false, error: error.message };
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async checkHealth() {
|
|
129
|
+
const { ok } = await this.api('/health');
|
|
130
|
+
this.connectionStatus = ok ? 'connected' : 'disconnected';
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async refreshAccounts() {
|
|
134
|
+
this.loading = true;
|
|
135
|
+
const { ok, data } = await this.api('/accounts');
|
|
136
|
+
|
|
137
|
+
if (ok && data.accounts) {
|
|
138
|
+
this.accounts = data.accounts;
|
|
139
|
+
this.stats = {
|
|
140
|
+
total: data.total || data.accounts.length,
|
|
141
|
+
active: data.accounts.filter(a => a.isActive).length,
|
|
142
|
+
expired: data.accounts.filter(a => a.tokenExpired).length,
|
|
143
|
+
planType: data.accounts.find(a => a.isActive)?.planType || '-'
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await this.refreshAllQuotaData();
|
|
147
|
+
}
|
|
148
|
+
this.loading = false;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
async refreshAllQuotaData() {
|
|
152
|
+
if (!this.accounts.length) return;
|
|
153
|
+
const { ok, data } = await this.api('/accounts/quota/all');
|
|
154
|
+
if (!ok || !data?.accounts) return;
|
|
155
|
+
|
|
156
|
+
const quotaMap = new Map(
|
|
157
|
+
data.accounts.map((entry) => [entry.email, entry.quota || null])
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
this.accounts = this.accounts.map((account) => ({
|
|
161
|
+
...account,
|
|
162
|
+
quota: quotaMap.has(account.email) ? quotaMap.get(account.email) : account.quota
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
if (this.selectedAccount?.email) {
|
|
166
|
+
const refreshed = this.accounts.find((account) => account.email === this.selectedAccount.email);
|
|
167
|
+
if (refreshed) this.selectedAccount = refreshed;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
getRemainingPercentage(account) {
|
|
172
|
+
const usage = account?.quota?.usage;
|
|
173
|
+
if (!usage) return null;
|
|
174
|
+
|
|
175
|
+
const percentage = Number(usage.percentage);
|
|
176
|
+
const usedFromTotal = Number(usage.totalTokenUsage);
|
|
177
|
+
const remainingFromApi = Number(usage.remaining);
|
|
178
|
+
|
|
179
|
+
let used = null;
|
|
180
|
+
if (Number.isFinite(percentage)) {
|
|
181
|
+
used = percentage;
|
|
182
|
+
} else if (Number.isFinite(usedFromTotal)) {
|
|
183
|
+
used = usedFromTotal;
|
|
184
|
+
} else if (Number.isFinite(remainingFromApi)) {
|
|
185
|
+
used = 100 - remainingFromApi;
|
|
186
|
+
} else if (usage.limitReached === true || usage.allowed === false) {
|
|
187
|
+
used = 100;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!Number.isFinite(used)) return null;
|
|
191
|
+
const clampedUsed = Math.max(0, Math.min(100, used));
|
|
192
|
+
return Math.max(0, Math.round(100 - clampedUsed));
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
isQuotaExhausted(account) {
|
|
196
|
+
const remaining = this.getRemainingPercentage(account);
|
|
197
|
+
if (remaining === null) return false;
|
|
198
|
+
const usage = account?.quota?.usage;
|
|
199
|
+
return remaining <= 0 || usage?.limitReached === true || usage?.allowed === false;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
quotaBarClass(account) {
|
|
203
|
+
const remaining = this.getRemainingPercentage(account);
|
|
204
|
+
if (remaining === null) return 'bg-gray-500';
|
|
205
|
+
if (remaining > 50) return 'bg-neon-green';
|
|
206
|
+
if (remaining > 20) return 'bg-yellow-500';
|
|
207
|
+
return 'bg-red-500';
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
quotaTextClass(account) {
|
|
211
|
+
const remaining = this.getRemainingPercentage(account);
|
|
212
|
+
if (remaining === null) return 'text-gray-500';
|
|
213
|
+
return remaining <= 20 ? 'text-red-400' : 'text-gray-400';
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
quotaLabel(account) {
|
|
217
|
+
const remaining = this.getRemainingPercentage(account);
|
|
218
|
+
if (remaining === null) return '-';
|
|
219
|
+
return `${remaining}%`;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
getQuotaResetAt(account) {
|
|
223
|
+
const usage = account?.quota?.usage;
|
|
224
|
+
if (!usage) return null;
|
|
225
|
+
|
|
226
|
+
if (usage.resetAt) return usage.resetAt;
|
|
227
|
+
|
|
228
|
+
const epoch = Number(usage?.raw?.rate_limit?.primary_window?.reset_at);
|
|
229
|
+
if (Number.isFinite(epoch)) {
|
|
230
|
+
return new Date(epoch * 1000).toISOString();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const resetAfter = Number(
|
|
234
|
+
usage.resetAfterSeconds ?? usage?.raw?.rate_limit?.primary_window?.reset_after_seconds
|
|
235
|
+
);
|
|
236
|
+
if (Number.isFinite(resetAfter) && resetAfter > 0) {
|
|
237
|
+
return new Date(Date.now() + resetAfter * 1000).toISOString();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
quotaResetAtLabel(account) {
|
|
244
|
+
const resetAt = this.getQuotaResetAt(account);
|
|
245
|
+
if (!resetAt) return null;
|
|
246
|
+
const date = new Date(resetAt);
|
|
247
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
248
|
+
return date.toLocaleString();
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
quotaResetSummary(account) {
|
|
252
|
+
const resetAt = this.getQuotaResetAt(account);
|
|
253
|
+
if (!resetAt) return null;
|
|
254
|
+
|
|
255
|
+
const resetMs = new Date(resetAt).getTime();
|
|
256
|
+
if (!Number.isFinite(resetMs)) return null;
|
|
257
|
+
|
|
258
|
+
const deltaSec = Math.max(0, Math.floor((resetMs - Date.now()) / 1000));
|
|
259
|
+
if (deltaSec === 0) return 'Reset due now';
|
|
260
|
+
|
|
261
|
+
const days = Math.floor(deltaSec / 86400);
|
|
262
|
+
const hours = Math.floor((deltaSec % 86400) / 3600);
|
|
263
|
+
const minutes = Math.floor((deltaSec % 3600) / 60);
|
|
264
|
+
|
|
265
|
+
if (days > 0) return `Resets in ${days}d ${hours}h`;
|
|
266
|
+
if (hours > 0) return `Resets in ${hours}h ${minutes}m`;
|
|
267
|
+
return `Resets in ${minutes}m`;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async startOAuth() {
|
|
271
|
+
await this.api('/accounts/oauth/cleanup', { method: 'POST' });
|
|
272
|
+
const { ok, data } = await this.api('/accounts/add', { method: 'POST' });
|
|
273
|
+
|
|
274
|
+
if (ok && data.oauth_url) {
|
|
275
|
+
const width = 500, height = 700;
|
|
276
|
+
const left = (screen.width - width) / 2;
|
|
277
|
+
const top = (screen.height - height) / 2;
|
|
278
|
+
window.open(data.oauth_url, 'ChatGPT Login', `width=${width},height=${height},left=${left},top=${top}`);
|
|
279
|
+
|
|
280
|
+
const checkAdded = setInterval(async () => {
|
|
281
|
+
const { ok, data } = await this.api('/accounts');
|
|
282
|
+
if (ok && data.accounts?.length > this.accounts.length) {
|
|
283
|
+
clearInterval(checkAdded);
|
|
284
|
+
this.showAddModal = false;
|
|
285
|
+
this.refreshAccounts();
|
|
286
|
+
}
|
|
287
|
+
}, 2000);
|
|
288
|
+
|
|
289
|
+
setTimeout(() => clearInterval(checkAdded), 120000);
|
|
290
|
+
} else {
|
|
291
|
+
this.showToast(data?.message || 'Failed to start OAuth', 'error');
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async startManualOAuth() {
|
|
296
|
+
await this.api('/accounts/oauth/cleanup', { method: 'POST' });
|
|
297
|
+
const { ok, data } = await this.api('/accounts/add', { method: 'POST' });
|
|
298
|
+
|
|
299
|
+
if (ok && data.oauth_url) {
|
|
300
|
+
this.oauthManualUrl = data.oauth_url;
|
|
301
|
+
this.oauthManualVerifier = data.verifier;
|
|
302
|
+
this.oauthManualCode = '';
|
|
303
|
+
this.oauthManualMode = true;
|
|
304
|
+
} else {
|
|
305
|
+
this.showToast(data?.message || 'Failed to start OAuth', 'error');
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async submitManualOAuth() {
|
|
310
|
+
if (!this.oauthManualCode) return;
|
|
311
|
+
|
|
312
|
+
const { ok, data } = await this.api('/accounts/add/manual', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
code: this.oauthManualCode,
|
|
316
|
+
verifier: this.oauthManualVerifier
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (ok && data.success) {
|
|
321
|
+
this.showToast(data.message, 'success');
|
|
322
|
+
this.showAddModal = false;
|
|
323
|
+
this.oauthManualMode = false;
|
|
324
|
+
this.refreshAccounts();
|
|
325
|
+
} else {
|
|
326
|
+
this.showToast(data?.error || 'Failed to add account', 'error');
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async copyToClipboard(text) {
|
|
331
|
+
try {
|
|
332
|
+
await navigator.clipboard.writeText(text);
|
|
333
|
+
this.showToast('Copied to clipboard', 'success');
|
|
334
|
+
} catch (e) {
|
|
335
|
+
this.showToast('Failed to copy', 'error');
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
async importFromCodex() {
|
|
340
|
+
const { ok, data } = await this.api('/accounts/import', { method: 'POST' });
|
|
341
|
+
if (ok && data.success) {
|
|
342
|
+
this.showToast(data.message, 'success');
|
|
343
|
+
this.showAddModal = false;
|
|
344
|
+
this.refreshAccounts();
|
|
345
|
+
} else {
|
|
346
|
+
this.showToast(data?.message || 'Import failed', 'error');
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
async switchAccount(email) {
|
|
351
|
+
const { ok, data } = await this.api('/accounts/switch', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: JSON.stringify({ email })
|
|
354
|
+
});
|
|
355
|
+
if (ok && data.success) {
|
|
356
|
+
this.showToast(data.message, 'success');
|
|
357
|
+
this.refreshAccounts();
|
|
358
|
+
} else {
|
|
359
|
+
this.showToast(data?.message || 'Failed to switch', 'error');
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
async refreshToken(email) {
|
|
364
|
+
const { ok, data } = await this.api(`/accounts/${encodeURIComponent(email)}/refresh`, { method: 'POST' });
|
|
365
|
+
if (ok && data.success) {
|
|
366
|
+
this.showToast(data.message, 'success');
|
|
367
|
+
this.refreshAccounts();
|
|
368
|
+
} else {
|
|
369
|
+
this.showToast(data?.message || 'Refresh failed', 'error');
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
async refreshAllTokens() {
|
|
374
|
+
this.showToast('Refreshing all tokens...', 'info');
|
|
375
|
+
const { ok, data } = await this.api('/accounts/refresh/all', { method: 'POST' });
|
|
376
|
+
if (ok) {
|
|
377
|
+
this.showToast(data.message, 'success');
|
|
378
|
+
this.refreshAccounts();
|
|
379
|
+
} else {
|
|
380
|
+
this.showToast(data?.message || 'Failed', 'error');
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
confirmDelete(email) {
|
|
385
|
+
this.deleteTarget = email;
|
|
386
|
+
this.showDeleteModal = true;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
async executeDelete() {
|
|
390
|
+
const { ok, data } = await this.api(`/accounts/${encodeURIComponent(this.deleteTarget)}`, { method: 'DELETE' });
|
|
391
|
+
this.showDeleteModal = false;
|
|
392
|
+
if (ok && data.success) {
|
|
393
|
+
this.showToast(data.message, 'success');
|
|
394
|
+
this.refreshAccounts();
|
|
395
|
+
} else {
|
|
396
|
+
this.showToast(data?.message || 'Delete failed', 'error');
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
showQuotaModal(acc) {
|
|
401
|
+
this.selectedAccount = acc;
|
|
402
|
+
this.showQuotaModalView = true;
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
async testChat() {
|
|
406
|
+
if (!this.testPrompt.trim()) return;
|
|
407
|
+
this.testing = true;
|
|
408
|
+
this.testResponse = '';
|
|
409
|
+
const { ok, data } = await this.api('/v1/chat/completions', {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
model: 'gpt-5.2',
|
|
413
|
+
messages: [{ role: 'user', content: this.testPrompt }]
|
|
414
|
+
})
|
|
415
|
+
});
|
|
416
|
+
this.testing = false;
|
|
417
|
+
if (ok && data.choices) {
|
|
418
|
+
this.testResponse = data.choices[0].message.content;
|
|
419
|
+
} else {
|
|
420
|
+
this.testResponse = data?.error?.message || 'Request failed';
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
async loadHaikuModelSetting() {
|
|
425
|
+
const { ok, data } = await this.api('/settings/haiku-model');
|
|
426
|
+
if (ok && data?.haikuKiloModel) {
|
|
427
|
+
this.haikuKiloModel = data.haikuKiloModel;
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
async setHaikuModel(model) {
|
|
432
|
+
if (this.haikuModelSaving || this.haikuKiloModel === model) return;
|
|
433
|
+
this.haikuModelSaving = true;
|
|
434
|
+
const { ok, data } = await this.api('/settings/haiku-model', {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
body: JSON.stringify({ haikuKiloModel: model })
|
|
437
|
+
});
|
|
438
|
+
this.haikuModelSaving = false;
|
|
439
|
+
if (ok && data?.haikuKiloModel) {
|
|
440
|
+
this.haikuKiloModel = data.haikuKiloModel;
|
|
441
|
+
this.showToast(`Haiku routed to ${data.haikuKiloModel.toUpperCase()}`, 'success');
|
|
442
|
+
} else {
|
|
443
|
+
this.showToast(data?.error || 'Failed to update Haiku model', 'error');
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
showToast(message, type = 'success') {
|
|
448
|
+
this.toast = { message, type };
|
|
449
|
+
setTimeout(() => { this.toast = null; }, 3000);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
startLogStream() {
|
|
453
|
+
if (this.logEventSource) this.logEventSource.close();
|
|
454
|
+
|
|
455
|
+
this.logEventSource = new EventSource('/api/logs/stream?history=true');
|
|
456
|
+
this.logEventSource.onmessage = (event) => {
|
|
457
|
+
try {
|
|
458
|
+
const log = JSON.parse(event.data);
|
|
459
|
+
this.logs.unshift(log);
|
|
460
|
+
|
|
461
|
+
if (this.logs.length > 500) {
|
|
462
|
+
this.logs = this.logs.slice(0, 500);
|
|
463
|
+
}
|
|
464
|
+
} catch (e) {}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
this.logEventSource.onerror = () => {
|
|
468
|
+
setTimeout(() => this.startLogStream(), 3000);
|
|
469
|
+
};
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
clearLogs() {
|
|
473
|
+
this.logs = [];
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
formatLogMessage(message) {
|
|
477
|
+
if (!message) return '';
|
|
478
|
+
const match = message.match(/^\[(\w+)\]\s*/);
|
|
479
|
+
if (match) {
|
|
480
|
+
return message.replace(match[0], '');
|
|
481
|
+
}
|
|
482
|
+
return message;
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
getLogDetails(message) {
|
|
486
|
+
if (!message) return null;
|
|
487
|
+
const details = {};
|
|
488
|
+
|
|
489
|
+
const patterns = [
|
|
490
|
+
['model', /model=([^\s|,]+)/],
|
|
491
|
+
['account', /account=([^\s|,]+)/],
|
|
492
|
+
['stream', /stream=(true|false)/],
|
|
493
|
+
['messages', /messages=(\d+)/],
|
|
494
|
+
['tools', /tools=(\d+)/],
|
|
495
|
+
['tokens', /tokens=(\d+)/],
|
|
496
|
+
['duration', /(\d+)ms/],
|
|
497
|
+
['status', /status=(\d+)/],
|
|
498
|
+
['error', /error=([^\s|]+)/]
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
for (const [key, pattern] of patterns) {
|
|
502
|
+
const match = message.match(pattern);
|
|
503
|
+
if (match) {
|
|
504
|
+
details[key] = match[1];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Object.keys(details).length > 0 ? details : null;
|
|
509
|
+
}
|
|
510
|
+
}));
|
|
511
|
+
});
|