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