@pikoloo/codex-proxy 1.0.6

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