@pikoloo/codex-proxy 1.0.7 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +28 -11
  3. package/bin/cli.js +15 -15
  4. package/docs/ACCOUNT.md +104 -0
  5. package/docs/API.md +21 -29
  6. package/docs/ARCHITECTURE.md +9 -9
  7. package/docs/CLAUDE_INTEGRATION.md +3 -3
  8. package/docs/OAUTH.md +13 -13
  9. package/docs/OPENCLAW.md +1 -1
  10. package/docs/legal.md +6 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/readme-cover.png +0 -0
  13. package/images/settings-screenshot.png +0 -0
  14. package/package.json +19 -10
  15. package/public/css/style.css +802 -22
  16. package/public/index.html +236 -338
  17. package/public/js/app.js +140 -118
  18. package/src/account-manager.js +210 -292
  19. package/src/cli/account.js +236 -0
  20. package/src/direct-api.js +7 -9
  21. package/src/index.js +7 -7
  22. package/src/middleware/credentials.js +6 -47
  23. package/src/oauth.js +2 -1
  24. package/src/routes/{accounts-route.js → account-route.js} +25 -109
  25. package/src/routes/api-routes.js +18 -30
  26. package/src/routes/chat-route.js +3 -3
  27. package/src/routes/messages-route.js +37 -199
  28. package/src/routes/models-route.js +11 -21
  29. package/src/routes/settings-route.js +1 -41
  30. package/src/security.js +1 -1
  31. package/src/server-settings.js +30 -38
  32. package/src/utils/logger.js +14 -1
  33. package/docs/ACCOUNTS.md +0 -202
  34. package/images/demo-screenshot.png +0 -0
  35. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  36. package/src/account-rotation/index.js +0 -93
  37. package/src/account-rotation/rate-limits.js +0 -293
  38. package/src/account-rotation/strategies/base-strategy.js +0 -48
  39. package/src/account-rotation/strategies/index.js +0 -31
  40. package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
  41. package/src/account-rotation/strategies/sticky-strategy.js +0 -97
  42. package/src/cli/accounts.js +0 -557
package/public/js/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  document.addEventListener('alpine:init', () => {
2
- const validTabs = ['dashboard', 'metrics', 'accounts', 'logs', 'settings'];
2
+ const validTabs = ['dashboard', 'metrics', 'account', 'logs', 'settings'];
3
3
  const initialTab = () => {
4
4
  const params = new URLSearchParams(window.location.search);
5
5
  const requested = params.get('tab') || window.location.hash.replace(/^#/, '');
@@ -7,16 +7,14 @@ document.addEventListener('alpine:init', () => {
7
7
  };
8
8
 
9
9
  Alpine.data('app', () => ({
10
- version: '1.0.7',
10
+ version: '1.2.2',
11
11
  connectionStatus: 'connecting',
12
12
  activeTab: initialTab(),
13
- sidebarOpen: window.innerWidth >= 1024,
14
13
  loading: false,
15
14
  toast: null,
16
15
  currentTime: '',
17
16
 
18
17
  accounts: [],
19
- searchQuery: '',
20
18
  stats: { total: 0, active: 0, expired: 0, planType: '-' },
21
19
  metricsRange: '24h',
22
20
  metricsStatusFilter: '',
@@ -49,10 +47,7 @@ document.addEventListener('alpine:init', () => {
49
47
  reasoningLevelOptions: [],
50
48
  modelMappingSaving: null,
51
49
  reasoningMappingSaving: null,
52
- accountStrategy: 'sticky',
53
- multiAccountRotationEnabled: false,
54
50
  haikuModelSaving: false,
55
- strategySaving: false,
56
51
  configureClaudeOnStartup: false,
57
52
  claudeProxyConfiguring: false,
58
53
  claudeProxyStartupSaving: false,
@@ -73,42 +68,83 @@ document.addEventListener('alpine:init', () => {
73
68
 
74
69
  testPrompt: 'Say hello',
75
70
  testResponse: '',
71
+ testStatus: 'idle',
72
+ testError: '',
73
+ testMeta: null,
76
74
  testing: false,
77
75
 
78
76
  haikuTestPrompt: 'Say hello',
79
77
  haikuTestResponse: '',
78
+ haikuTestStatus: 'idle',
79
+ haikuTestError: '',
80
+ haikuTestMeta: null,
80
81
  haikuTesting: false,
81
82
 
82
83
  haikuModelLabel() {
83
84
  return this.modelOptionName(this.modelMappings?.haiku || 'gpt-5.4-mini');
84
85
  },
85
86
 
87
+ get testStatusText() {
88
+ const labels = {
89
+ idle: 'Ready',
90
+ running: 'Sending request',
91
+ success: 'Response received',
92
+ error: 'Request failed'
93
+ };
94
+ return labels[this.testStatus] || 'Ready';
95
+ },
96
+
97
+ get haikuTestStatusText() {
98
+ const labels = {
99
+ idle: 'Ready',
100
+ running: 'Sending Haiku request',
101
+ success: 'Response received',
102
+ error: 'Request failed'
103
+ };
104
+ return labels[this.haikuTestStatus] || 'Ready';
105
+ },
106
+
86
107
  async testHaikuChat() {
87
108
  if (!this.haikuTestPrompt.trim()) return;
109
+ const startedAt = Date.now();
88
110
  this.haikuTesting = true;
89
111
  this.haikuTestResponse = '';
90
- const { ok, data } = await this.api('/v1/chat/completions', {
91
- method: 'POST',
92
- body: JSON.stringify({
93
- model: 'claude-haiku-4',
94
- messages: [{ role: 'user', content: this.haikuTestPrompt }]
95
- })
96
- });
97
- this.haikuTesting = false;
98
- if (ok && data.choices) {
99
- this.haikuTestResponse = data.choices[0].message.content;
100
- } else {
101
- this.haikuTestResponse = data?.error?.message || 'Request failed';
112
+ this.haikuTestStatus = 'running';
113
+ this.haikuTestError = '';
114
+ this.haikuTestMeta = null;
115
+
116
+ try {
117
+ const { ok, data, error } = await this.api('/v1/chat/completions', {
118
+ method: 'POST',
119
+ body: JSON.stringify({
120
+ model: 'claude-haiku-4',
121
+ messages: [{ role: 'user', content: this.haikuTestPrompt }]
122
+ })
123
+ });
124
+ const durationMs = Date.now() - startedAt;
125
+ this.haikuTestMeta = { durationMs, usage: data?.usage || null };
126
+
127
+ if (ok && data.choices) {
128
+ this.haikuTestResponse = data.choices[0].message.content;
129
+ this.haikuTestStatus = 'success';
130
+ } else {
131
+ this.haikuTestError = data?.error?.message || error || 'Request failed';
132
+ this.haikuTestResponse = this.haikuTestError;
133
+ this.haikuTestStatus = 'error';
134
+ }
135
+ } finally {
136
+ this.haikuTesting = false;
102
137
  }
103
138
  },
104
139
 
105
- configPath: '~/.codex-claude-proxy/accounts.json',
140
+ configPath: '~/.codex-claude-proxy/account.json',
106
141
  serverUrl: window.location.origin,
107
142
 
108
143
  logs: [],
109
144
  logSearchQuery: '',
110
145
  logFilters: { INFO: true, SUCCESS: true, WARN: true, ERROR: true, DEBUG: false },
111
146
  logEventSource: null,
147
+ logStreamStatus: 'connecting',
112
148
 
113
149
  get filteredLogs() {
114
150
  const query = this.logSearchQuery.trim().toLowerCase();
@@ -119,10 +155,11 @@ document.addEventListener('alpine:init', () => {
119
155
  });
120
156
  },
121
157
 
122
- get filteredAccounts() {
123
- if (!this.searchQuery) return this.accounts;
124
- const q = this.searchQuery.toLowerCase();
125
- return this.accounts.filter(a => a.email.toLowerCase().includes(q));
158
+ get logLevelCounts() {
159
+ return this.logs.reduce((counts, log) => {
160
+ counts[log.level] = (counts[log.level] || 0) + 1;
161
+ return counts;
162
+ }, { INFO: 0, SUCCESS: 0, WARN: 0, ERROR: 0, DEBUG: 0 });
126
163
  },
127
164
 
128
165
  get metricsTotals() {
@@ -159,14 +196,9 @@ document.addEventListener('alpine:init', () => {
159
196
  this.startLogStream();
160
197
  this.loadModelMappingsSetting();
161
198
  this.loadHaikuModelSetting();
162
- this.loadAccountStrategySetting();
163
199
  this.loadClaudeProxySetting();
164
200
  this.loadMetrics();
165
201
 
166
- window.addEventListener('resize', () => {
167
- this.sidebarOpen = window.innerWidth >= 1024;
168
- });
169
-
170
202
  window.addEventListener('message', (event) => {
171
203
  if (event.data && event.data.type === 'oauth-success') {
172
204
  this.showToast(`Account ${event.data.email} added!`, 'success');
@@ -195,9 +227,6 @@ document.addEventListener('alpine:init', () => {
195
227
  nextUrl.hash = '';
196
228
  }
197
229
  window.history.replaceState({}, '', nextUrl);
198
- if (window.innerWidth < 1024) {
199
- this.sidebarOpen = false;
200
- }
201
230
  },
202
231
 
203
232
  async api(endpoint, options = {}) {
@@ -220,15 +249,15 @@ document.addEventListener('alpine:init', () => {
220
249
 
221
250
  async refreshAccounts() {
222
251
  this.loading = true;
223
- const { ok, data } = await this.api('/accounts');
252
+ const { ok, data } = await this.api('/account');
224
253
 
225
- if (ok && data.accounts) {
226
- this.accounts = data.accounts;
254
+ if (ok) {
255
+ this.accounts = data.account ? [data.account] : [];
227
256
  this.stats = {
228
- total: data.total || data.accounts.length,
229
- active: data.accounts.filter(a => a.isActive).length,
230
- expired: data.accounts.filter(a => a.tokenExpired).length,
231
- planType: data.accounts.find(a => a.isActive)?.planType || '-'
257
+ total: data.total || this.accounts.length,
258
+ active: this.accounts.filter(a => a.isActive).length,
259
+ expired: this.accounts.filter(a => a.tokenExpired).length,
260
+ planType: this.accounts.find(a => a.isActive)?.planType || '-'
232
261
  };
233
262
 
234
263
  await this.refreshAllQuotaData();
@@ -305,6 +334,15 @@ document.addEventListener('alpine:init', () => {
305
334
  return `${number}ms`;
306
335
  },
307
336
 
337
+ formatUsageSummary(usage) {
338
+ if (!usage) return '';
339
+ const input = Number(usage.prompt_tokens ?? usage.input_tokens) || 0;
340
+ const output = Number(usage.completion_tokens ?? usage.output_tokens) || 0;
341
+ const total = Number(usage.total_tokens ?? (input + output)) || 0;
342
+ if (total <= 0) return '';
343
+ return `${this.formatTokenCount(total)} tokens (${this.formatTokenCount(input)} in, ${this.formatTokenCount(output)} out)`;
344
+ },
345
+
308
346
  formatBytes(value) {
309
347
  const number = Number(value) || 0;
310
348
  if (number >= 1024 * 1024) return `${(number / (1024 * 1024)).toFixed(1)} MB`;
@@ -328,16 +366,12 @@ document.addEventListener('alpine:init', () => {
328
366
 
329
367
  async refreshAllQuotaData() {
330
368
  if (!this.accounts.length) return;
331
- const { ok, data } = await this.api('/accounts/quota/all');
332
- if (!ok || !data?.accounts) return;
333
-
334
- const quotaMap = new Map(
335
- data.accounts.map((entry) => [entry.email, entry.quota || null])
336
- );
369
+ const { ok, data } = await this.api('/account/quota');
370
+ if (!ok || !data?.email) return;
337
371
 
338
372
  this.accounts = this.accounts.map((account) => ({
339
373
  ...account,
340
- quota: quotaMap.has(account.email) ? quotaMap.get(account.email) : account.quota
374
+ quota: account.email === data.email ? data.quota || null : account.quota
341
375
  }));
342
376
 
343
377
  if (this.selectedAccount?.email) {
@@ -446,8 +480,8 @@ document.addEventListener('alpine:init', () => {
446
480
  },
447
481
 
448
482
  async startOAuth() {
449
- await this.api('/accounts/oauth/cleanup', { method: 'POST' });
450
- const { ok, data } = await this.api('/accounts/add', { method: 'POST' });
483
+ await this.api('/account/oauth/cleanup', { method: 'POST' });
484
+ const { ok, data } = await this.api('/account/add', { method: 'POST' });
451
485
 
452
486
  if (ok && data.oauth_url) {
453
487
  const width = 500, height = 700;
@@ -456,8 +490,8 @@ document.addEventListener('alpine:init', () => {
456
490
  window.open(data.oauth_url, 'ChatGPT Login', `width=${width},height=${height},left=${left},top=${top}`);
457
491
 
458
492
  const checkAdded = setInterval(async () => {
459
- const { ok, data } = await this.api('/accounts');
460
- if (ok && data.accounts?.length > this.accounts.length) {
493
+ const { ok, data } = await this.api('/account');
494
+ if (ok && data.account) {
461
495
  clearInterval(checkAdded);
462
496
  this.showAddModal = false;
463
497
  this.refreshAccounts();
@@ -471,8 +505,8 @@ document.addEventListener('alpine:init', () => {
471
505
  },
472
506
 
473
507
  async startManualOAuth() {
474
- await this.api('/accounts/oauth/cleanup', { method: 'POST' });
475
- const { ok, data } = await this.api('/accounts/add', { method: 'POST' });
508
+ await this.api('/account/oauth/cleanup', { method: 'POST' });
509
+ const { ok, data } = await this.api('/account/add', { method: 'POST' });
476
510
 
477
511
  if (ok && data.oauth_url) {
478
512
  this.oauthManualUrl = data.oauth_url;
@@ -487,7 +521,7 @@ document.addEventListener('alpine:init', () => {
487
521
  async submitManualOAuth() {
488
522
  if (!this.oauthManualCode) return;
489
523
 
490
- const { ok, data } = await this.api('/accounts/add/manual', {
524
+ const { ok, data } = await this.api('/account/add/manual', {
491
525
  method: 'POST',
492
526
  body: JSON.stringify({
493
527
  code: this.oauthManualCode,
@@ -515,7 +549,7 @@ document.addEventListener('alpine:init', () => {
515
549
  },
516
550
 
517
551
  async importFromCodex() {
518
- const { ok, data } = await this.api('/accounts/import', { method: 'POST' });
552
+ const { ok, data } = await this.api('/account/import', { method: 'POST' });
519
553
  if (ok && data.success) {
520
554
  this.showToast(data.message, 'success');
521
555
  this.showAddModal = false;
@@ -525,21 +559,8 @@ document.addEventListener('alpine:init', () => {
525
559
  }
526
560
  },
527
561
 
528
- async switchAccount(email) {
529
- const { ok, data } = await this.api('/accounts/switch', {
530
- method: 'POST',
531
- body: JSON.stringify({ email })
532
- });
533
- if (ok && data.success) {
534
- this.showToast(data.message, 'success');
535
- this.refreshAccounts();
536
- } else {
537
- this.showToast(data?.message || 'Failed to switch', 'error');
538
- }
539
- },
540
-
541
562
  async refreshToken(email) {
542
- const { ok, data } = await this.api(`/accounts/${encodeURIComponent(email)}/refresh`, { method: 'POST' });
563
+ const { ok, data } = await this.api('/account/refresh', { method: 'POST' });
543
564
  if (ok && data.success) {
544
565
  this.showToast(data.message, 'success');
545
566
  this.refreshAccounts();
@@ -548,24 +569,13 @@ document.addEventListener('alpine:init', () => {
548
569
  }
549
570
  },
550
571
 
551
- async refreshAllTokens() {
552
- this.showToast('Refreshing all tokens...', 'info');
553
- const { ok, data } = await this.api('/accounts/refresh/all', { method: 'POST' });
554
- if (ok) {
555
- this.showToast(data.message, 'success');
556
- this.refreshAccounts();
557
- } else {
558
- this.showToast(data?.message || 'Failed', 'error');
559
- }
560
- },
561
-
562
572
  confirmDelete(email) {
563
573
  this.deleteTarget = email;
564
574
  this.showDeleteModal = true;
565
575
  },
566
576
 
567
577
  async executeDelete() {
568
- const { ok, data } = await this.api(`/accounts/${encodeURIComponent(this.deleteTarget)}`, { method: 'DELETE' });
578
+ const { ok, data } = await this.api('/account', { method: 'DELETE' });
569
579
  this.showDeleteModal = false;
570
580
  if (ok && data.success) {
571
581
  this.showToast(data.message, 'success');
@@ -582,20 +592,34 @@ document.addEventListener('alpine:init', () => {
582
592
 
583
593
  async testChat() {
584
594
  if (!this.testPrompt.trim()) return;
595
+ const startedAt = Date.now();
585
596
  this.testing = true;
586
597
  this.testResponse = '';
587
- const { ok, data } = await this.api('/v1/chat/completions', {
588
- method: 'POST',
589
- body: JSON.stringify({
590
- model: 'gpt-5.5',
591
- messages: [{ role: 'user', content: this.testPrompt }]
592
- })
593
- });
594
- this.testing = false;
595
- if (ok && data.choices) {
596
- this.testResponse = data.choices[0].message.content;
597
- } else {
598
- this.testResponse = data?.error?.message || 'Request failed';
598
+ this.testStatus = 'running';
599
+ this.testError = '';
600
+ this.testMeta = null;
601
+
602
+ try {
603
+ const { ok, data, error } = await this.api('/v1/chat/completions', {
604
+ method: 'POST',
605
+ body: JSON.stringify({
606
+ model: 'gpt-5.5',
607
+ messages: [{ role: 'user', content: this.testPrompt }]
608
+ })
609
+ });
610
+ const durationMs = Date.now() - startedAt;
611
+ this.testMeta = { durationMs, usage: data?.usage || null };
612
+
613
+ if (ok && data.choices) {
614
+ this.testResponse = data.choices[0].message.content;
615
+ this.testStatus = 'success';
616
+ } else {
617
+ this.testError = data?.error?.message || error || 'Request failed';
618
+ this.testResponse = this.testError;
619
+ this.testStatus = 'error';
620
+ }
621
+ } finally {
622
+ this.testing = false;
599
623
  }
600
624
  },
601
625
 
@@ -725,31 +749,6 @@ document.addEventListener('alpine:init', () => {
725
749
  }
726
750
  },
727
751
 
728
- async loadAccountStrategySetting() {
729
- const { ok, data } = await this.api('/settings/account-strategy');
730
- if (ok && data?.accountStrategy) {
731
- this.accountStrategy = data.accountStrategy;
732
- this.multiAccountRotationEnabled = data.rotationEnabled === true;
733
- }
734
- },
735
-
736
- async setAccountStrategy(strategy) {
737
- if (!this.multiAccountRotationEnabled || this.strategySaving || this.accountStrategy === strategy) return;
738
- this.strategySaving = true;
739
- const { ok, data } = await this.api('/settings/account-strategy', {
740
- method: 'POST',
741
- body: JSON.stringify({ accountStrategy: strategy })
742
- });
743
- this.strategySaving = false;
744
- if (ok && data?.accountStrategy) {
745
- this.accountStrategy = data.accountStrategy;
746
- this.multiAccountRotationEnabled = data.rotationEnabled === true;
747
- this.showToast(`Account strategy set to ${data.accountStrategy === 'sticky' ? 'Sticky' : 'Round-Robin'}`, 'success');
748
- } else {
749
- this.showToast(data?.error || 'Failed to update strategy', 'error');
750
- }
751
- },
752
-
753
752
  async loadClaudeProxySetting() {
754
753
  const { ok, data } = await this.api('/settings/claude-proxy');
755
754
  if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
@@ -802,11 +801,16 @@ document.addEventListener('alpine:init', () => {
802
801
 
803
802
  startLogStream() {
804
803
  if (this.logEventSource) this.logEventSource.close();
805
-
804
+
805
+ this.logStreamStatus = 'connecting';
806
806
  this.logEventSource = new EventSource('/api/logs/stream?history=true');
807
+ this.logEventSource.onopen = () => {
808
+ this.logStreamStatus = 'connected';
809
+ };
807
810
  this.logEventSource.onmessage = (event) => {
808
811
  try {
809
812
  const log = JSON.parse(event.data);
813
+ this.logStreamStatus = 'connected';
810
814
  this.logs.unshift(log);
811
815
 
812
816
  if (this.logs.length > 500) {
@@ -816,6 +820,8 @@ document.addEventListener('alpine:init', () => {
816
820
  };
817
821
 
818
822
  this.logEventSource.onerror = () => {
823
+ this.logStreamStatus = 'disconnected';
824
+ if (this.logEventSource) this.logEventSource.close();
819
825
  setTimeout(() => this.startLogStream(), 3000);
820
826
  };
821
827
  },
@@ -833,6 +839,22 @@ document.addEventListener('alpine:init', () => {
833
839
  return message;
834
840
  },
835
841
 
842
+ formatLogTime(timestamp) {
843
+ if (!timestamp) return '--:--:--';
844
+ const date = new Date(timestamp);
845
+ if (Number.isNaN(date.getTime())) return '--:--:--';
846
+ return date.toLocaleTimeString([], { hour12: false });
847
+ },
848
+
849
+ logStreamStatusText() {
850
+ const labels = {
851
+ connecting: 'Connecting',
852
+ connected: 'Live',
853
+ disconnected: 'Reconnecting'
854
+ };
855
+ return labels[this.logStreamStatus] || 'Unknown';
856
+ },
857
+
836
858
  getLogDetails(message) {
837
859
  if (!message) return null;
838
860
  const details = {};