@pikoloo/codex-proxy 1.0.7 → 1.1.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/docs/API.md +0 -15
- package/images/dashboard-screenshot.png +0 -0
- package/images/readme-cover.png +0 -0
- package/images/settings-screenshot.png +0 -0
- package/package.json +10 -3
- package/public/css/style.css +832 -22
- package/public/index.html +149 -190
- package/public/js/app.js +119 -62
- package/src/account-rotation/index.js +64 -27
- package/src/index.js +1 -1
- package/src/routes/api-routes.js +0 -4
- package/src/routes/chat-route.js +1 -1
- package/src/routes/messages-route.js +14 -16
- package/src/routes/settings-route.js +1 -41
- package/src/server-settings.js +29 -30
- package/src/utils/logger.js +14 -1
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/src/account-rotation/strategies/base-strategy.js +0 -48
- package/src/account-rotation/strategies/index.js +0 -31
- package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
- package/src/account-rotation/strategies/sticky-strategy.js +0 -97
package/public/js/app.js
CHANGED
|
@@ -10,7 +10,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
10
10
|
version: '1.0.7',
|
|
11
11
|
connectionStatus: 'connecting',
|
|
12
12
|
activeTab: initialTab(),
|
|
13
|
-
sidebarOpen: window.innerWidth >= 1024,
|
|
14
13
|
loading: false,
|
|
15
14
|
toast: null,
|
|
16
15
|
currentTime: '',
|
|
@@ -49,10 +48,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
49
48
|
reasoningLevelOptions: [],
|
|
50
49
|
modelMappingSaving: null,
|
|
51
50
|
reasoningMappingSaving: null,
|
|
52
|
-
accountStrategy: 'sticky',
|
|
53
|
-
multiAccountRotationEnabled: false,
|
|
54
51
|
haikuModelSaving: false,
|
|
55
|
-
strategySaving: false,
|
|
56
52
|
configureClaudeOnStartup: false,
|
|
57
53
|
claudeProxyConfiguring: false,
|
|
58
54
|
claudeProxyStartupSaving: false,
|
|
@@ -73,32 +69,72 @@ document.addEventListener('alpine:init', () => {
|
|
|
73
69
|
|
|
74
70
|
testPrompt: 'Say hello',
|
|
75
71
|
testResponse: '',
|
|
72
|
+
testStatus: 'idle',
|
|
73
|
+
testError: '',
|
|
74
|
+
testMeta: null,
|
|
76
75
|
testing: false,
|
|
77
76
|
|
|
78
77
|
haikuTestPrompt: 'Say hello',
|
|
79
78
|
haikuTestResponse: '',
|
|
79
|
+
haikuTestStatus: 'idle',
|
|
80
|
+
haikuTestError: '',
|
|
81
|
+
haikuTestMeta: null,
|
|
80
82
|
haikuTesting: false,
|
|
81
83
|
|
|
82
84
|
haikuModelLabel() {
|
|
83
85
|
return this.modelOptionName(this.modelMappings?.haiku || 'gpt-5.4-mini');
|
|
84
86
|
},
|
|
85
87
|
|
|
88
|
+
get testStatusText() {
|
|
89
|
+
const labels = {
|
|
90
|
+
idle: 'Ready',
|
|
91
|
+
running: 'Sending request',
|
|
92
|
+
success: 'Response received',
|
|
93
|
+
error: 'Request failed'
|
|
94
|
+
};
|
|
95
|
+
return labels[this.testStatus] || 'Ready';
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
get haikuTestStatusText() {
|
|
99
|
+
const labels = {
|
|
100
|
+
idle: 'Ready',
|
|
101
|
+
running: 'Sending Haiku request',
|
|
102
|
+
success: 'Response received',
|
|
103
|
+
error: 'Request failed'
|
|
104
|
+
};
|
|
105
|
+
return labels[this.haikuTestStatus] || 'Ready';
|
|
106
|
+
},
|
|
107
|
+
|
|
86
108
|
async testHaikuChat() {
|
|
87
109
|
if (!this.haikuTestPrompt.trim()) return;
|
|
110
|
+
const startedAt = Date.now();
|
|
88
111
|
this.haikuTesting = true;
|
|
89
112
|
this.haikuTestResponse = '';
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
this.haikuTestStatus = 'running';
|
|
114
|
+
this.haikuTestError = '';
|
|
115
|
+
this.haikuTestMeta = null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const { ok, data, error } = await this.api('/v1/chat/completions', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
model: 'claude-haiku-4',
|
|
122
|
+
messages: [{ role: 'user', content: this.haikuTestPrompt }]
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
const durationMs = Date.now() - startedAt;
|
|
126
|
+
this.haikuTestMeta = { durationMs, usage: data?.usage || null };
|
|
127
|
+
|
|
128
|
+
if (ok && data.choices) {
|
|
129
|
+
this.haikuTestResponse = data.choices[0].message.content;
|
|
130
|
+
this.haikuTestStatus = 'success';
|
|
131
|
+
} else {
|
|
132
|
+
this.haikuTestError = data?.error?.message || error || 'Request failed';
|
|
133
|
+
this.haikuTestResponse = this.haikuTestError;
|
|
134
|
+
this.haikuTestStatus = 'error';
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
this.haikuTesting = false;
|
|
102
138
|
}
|
|
103
139
|
},
|
|
104
140
|
|
|
@@ -109,6 +145,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
109
145
|
logSearchQuery: '',
|
|
110
146
|
logFilters: { INFO: true, SUCCESS: true, WARN: true, ERROR: true, DEBUG: false },
|
|
111
147
|
logEventSource: null,
|
|
148
|
+
logStreamStatus: 'connecting',
|
|
112
149
|
|
|
113
150
|
get filteredLogs() {
|
|
114
151
|
const query = this.logSearchQuery.trim().toLowerCase();
|
|
@@ -119,6 +156,13 @@ document.addEventListener('alpine:init', () => {
|
|
|
119
156
|
});
|
|
120
157
|
},
|
|
121
158
|
|
|
159
|
+
get logLevelCounts() {
|
|
160
|
+
return this.logs.reduce((counts, log) => {
|
|
161
|
+
counts[log.level] = (counts[log.level] || 0) + 1;
|
|
162
|
+
return counts;
|
|
163
|
+
}, { INFO: 0, SUCCESS: 0, WARN: 0, ERROR: 0, DEBUG: 0 });
|
|
164
|
+
},
|
|
165
|
+
|
|
122
166
|
get filteredAccounts() {
|
|
123
167
|
if (!this.searchQuery) return this.accounts;
|
|
124
168
|
const q = this.searchQuery.toLowerCase();
|
|
@@ -159,14 +203,9 @@ document.addEventListener('alpine:init', () => {
|
|
|
159
203
|
this.startLogStream();
|
|
160
204
|
this.loadModelMappingsSetting();
|
|
161
205
|
this.loadHaikuModelSetting();
|
|
162
|
-
this.loadAccountStrategySetting();
|
|
163
206
|
this.loadClaudeProxySetting();
|
|
164
207
|
this.loadMetrics();
|
|
165
208
|
|
|
166
|
-
window.addEventListener('resize', () => {
|
|
167
|
-
this.sidebarOpen = window.innerWidth >= 1024;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
209
|
window.addEventListener('message', (event) => {
|
|
171
210
|
if (event.data && event.data.type === 'oauth-success') {
|
|
172
211
|
this.showToast(`Account ${event.data.email} added!`, 'success');
|
|
@@ -195,9 +234,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
195
234
|
nextUrl.hash = '';
|
|
196
235
|
}
|
|
197
236
|
window.history.replaceState({}, '', nextUrl);
|
|
198
|
-
if (window.innerWidth < 1024) {
|
|
199
|
-
this.sidebarOpen = false;
|
|
200
|
-
}
|
|
201
237
|
},
|
|
202
238
|
|
|
203
239
|
async api(endpoint, options = {}) {
|
|
@@ -305,6 +341,15 @@ document.addEventListener('alpine:init', () => {
|
|
|
305
341
|
return `${number}ms`;
|
|
306
342
|
},
|
|
307
343
|
|
|
344
|
+
formatUsageSummary(usage) {
|
|
345
|
+
if (!usage) return '';
|
|
346
|
+
const input = Number(usage.prompt_tokens ?? usage.input_tokens) || 0;
|
|
347
|
+
const output = Number(usage.completion_tokens ?? usage.output_tokens) || 0;
|
|
348
|
+
const total = Number(usage.total_tokens ?? (input + output)) || 0;
|
|
349
|
+
if (total <= 0) return '';
|
|
350
|
+
return `${this.formatTokenCount(total)} tokens (${this.formatTokenCount(input)} in, ${this.formatTokenCount(output)} out)`;
|
|
351
|
+
},
|
|
352
|
+
|
|
308
353
|
formatBytes(value) {
|
|
309
354
|
const number = Number(value) || 0;
|
|
310
355
|
if (number >= 1024 * 1024) return `${(number / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -582,20 +627,34 @@ document.addEventListener('alpine:init', () => {
|
|
|
582
627
|
|
|
583
628
|
async testChat() {
|
|
584
629
|
if (!this.testPrompt.trim()) return;
|
|
630
|
+
const startedAt = Date.now();
|
|
585
631
|
this.testing = true;
|
|
586
632
|
this.testResponse = '';
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
633
|
+
this.testStatus = 'running';
|
|
634
|
+
this.testError = '';
|
|
635
|
+
this.testMeta = null;
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const { ok, data, error } = await this.api('/v1/chat/completions', {
|
|
639
|
+
method: 'POST',
|
|
640
|
+
body: JSON.stringify({
|
|
641
|
+
model: 'gpt-5.5',
|
|
642
|
+
messages: [{ role: 'user', content: this.testPrompt }]
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
const durationMs = Date.now() - startedAt;
|
|
646
|
+
this.testMeta = { durationMs, usage: data?.usage || null };
|
|
647
|
+
|
|
648
|
+
if (ok && data.choices) {
|
|
649
|
+
this.testResponse = data.choices[0].message.content;
|
|
650
|
+
this.testStatus = 'success';
|
|
651
|
+
} else {
|
|
652
|
+
this.testError = data?.error?.message || error || 'Request failed';
|
|
653
|
+
this.testResponse = this.testError;
|
|
654
|
+
this.testStatus = 'error';
|
|
655
|
+
}
|
|
656
|
+
} finally {
|
|
657
|
+
this.testing = false;
|
|
599
658
|
}
|
|
600
659
|
},
|
|
601
660
|
|
|
@@ -725,31 +784,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
725
784
|
}
|
|
726
785
|
},
|
|
727
786
|
|
|
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
787
|
async loadClaudeProxySetting() {
|
|
754
788
|
const { ok, data } = await this.api('/settings/claude-proxy');
|
|
755
789
|
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
@@ -802,11 +836,16 @@ document.addEventListener('alpine:init', () => {
|
|
|
802
836
|
|
|
803
837
|
startLogStream() {
|
|
804
838
|
if (this.logEventSource) this.logEventSource.close();
|
|
805
|
-
|
|
839
|
+
|
|
840
|
+
this.logStreamStatus = 'connecting';
|
|
806
841
|
this.logEventSource = new EventSource('/api/logs/stream?history=true');
|
|
842
|
+
this.logEventSource.onopen = () => {
|
|
843
|
+
this.logStreamStatus = 'connected';
|
|
844
|
+
};
|
|
807
845
|
this.logEventSource.onmessage = (event) => {
|
|
808
846
|
try {
|
|
809
847
|
const log = JSON.parse(event.data);
|
|
848
|
+
this.logStreamStatus = 'connected';
|
|
810
849
|
this.logs.unshift(log);
|
|
811
850
|
|
|
812
851
|
if (this.logs.length > 500) {
|
|
@@ -816,6 +855,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
816
855
|
};
|
|
817
856
|
|
|
818
857
|
this.logEventSource.onerror = () => {
|
|
858
|
+
this.logStreamStatus = 'disconnected';
|
|
859
|
+
if (this.logEventSource) this.logEventSource.close();
|
|
819
860
|
setTimeout(() => this.startLogStream(), 3000);
|
|
820
861
|
};
|
|
821
862
|
},
|
|
@@ -833,6 +874,22 @@ document.addEventListener('alpine:init', () => {
|
|
|
833
874
|
return message;
|
|
834
875
|
},
|
|
835
876
|
|
|
877
|
+
formatLogTime(timestamp) {
|
|
878
|
+
if (!timestamp) return '--:--:--';
|
|
879
|
+
const date = new Date(timestamp);
|
|
880
|
+
if (Number.isNaN(date.getTime())) return '--:--:--';
|
|
881
|
+
return date.toLocaleTimeString([], { hour12: false });
|
|
882
|
+
},
|
|
883
|
+
|
|
884
|
+
logStreamStatusText() {
|
|
885
|
+
const labels = {
|
|
886
|
+
connecting: 'Connecting',
|
|
887
|
+
connected: 'Live',
|
|
888
|
+
disconnected: 'Reconnecting'
|
|
889
|
+
};
|
|
890
|
+
return labels[this.logStreamStatus] || 'Unknown';
|
|
891
|
+
},
|
|
892
|
+
|
|
836
893
|
getLogDetails(message) {
|
|
837
894
|
if (!message) return null;
|
|
838
895
|
const details = {};
|
|
@@ -4,20 +4,20 @@ import {
|
|
|
4
4
|
clearInvalid,
|
|
5
5
|
isAllRateLimited,
|
|
6
6
|
getMinWaitTimeMs,
|
|
7
|
-
clearExpiredLimits
|
|
7
|
+
clearExpiredLimits,
|
|
8
|
+
isAccountCoolingDown
|
|
8
9
|
} from './rate-limits.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const MAX_WAIT_BEFORE_ERROR_MS = 120000;
|
|
11
12
|
|
|
12
13
|
export class AccountRotator {
|
|
13
|
-
constructor(accountManager
|
|
14
|
+
constructor(accountManager) {
|
|
14
15
|
this.accountManager = accountManager;
|
|
15
|
-
this.strategy = createStrategy(strategyName);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
selectAccount(modelId
|
|
18
|
+
selectAccount(modelId) {
|
|
19
19
|
const { accounts } = this.accountManager.listAccounts();
|
|
20
|
-
return
|
|
20
|
+
return selectAccount(accounts, modelId);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
markRateLimited(email, resetMs, modelId) {
|
|
@@ -48,23 +48,11 @@ export class AccountRotator {
|
|
|
48
48
|
return getMinWaitTimeMs(accounts, modelId);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
notifySuccess(account, modelId) {
|
|
52
|
-
if (this.strategy.notifySuccess) {
|
|
53
|
-
this.strategy.notifySuccess(account, modelId);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
51
|
+
notifySuccess(account, modelId) {}
|
|
56
52
|
|
|
57
|
-
notifyRateLimit(account, modelId) {
|
|
58
|
-
if (this.strategy.notifyRateLimit) {
|
|
59
|
-
this.strategy.notifyRateLimit(account, modelId);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
53
|
+
notifyRateLimit(account, modelId) {}
|
|
62
54
|
|
|
63
|
-
notifyFailure(account, modelId) {
|
|
64
|
-
if (this.strategy.notifyFailure) {
|
|
65
|
-
this.strategy.notifyFailure(account, modelId);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
55
|
+
notifyFailure(account, modelId) {}
|
|
68
56
|
|
|
69
57
|
clearExpiredLimits() {
|
|
70
58
|
const { accounts } = this.accountManager.listAccounts();
|
|
@@ -72,18 +60,67 @@ export class AccountRotator {
|
|
|
72
60
|
this.accountManager.save();
|
|
73
61
|
}
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function selectAccount(accounts, modelId) {
|
|
66
|
+
if (!accounts || accounts.length === 0) {
|
|
67
|
+
return { account: null, index: 0, waitMs: 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const activeIndex = accounts.findIndex((account) => account.isActive);
|
|
71
|
+
const startIndex = activeIndex >= 0 ? activeIndex : 0;
|
|
72
|
+
|
|
73
|
+
for (let offset = 0; offset < accounts.length; offset++) {
|
|
74
|
+
const index = (startIndex + offset) % accounts.length;
|
|
75
|
+
const account = accounts[index];
|
|
76
|
+
|
|
77
|
+
if (isAccountUsable(account, modelId)) {
|
|
78
|
+
account.lastUsed = Date.now();
|
|
79
|
+
return { account, index, waitMs: 0 };
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
const waitMs = getAccountWaitMs(accounts[startIndex], modelId);
|
|
84
|
+
if (waitMs > 0 && waitMs <= MAX_WAIT_BEFORE_ERROR_MS) {
|
|
85
|
+
return { account: null, index: startIndex, waitMs };
|
|
81
86
|
}
|
|
87
|
+
|
|
88
|
+
return { account: null, index: startIndex, waitMs: 0 };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isAccountUsable(account, modelId) {
|
|
92
|
+
if (!account) return false;
|
|
93
|
+
if (account.isInvalid) return false;
|
|
94
|
+
if (account.enabled === false) return false;
|
|
95
|
+
if (isAccountCoolingDown(account)) return false;
|
|
96
|
+
|
|
97
|
+
const waitMs = getModelRateLimitWaitMs(account, modelId);
|
|
98
|
+
return waitMs === 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getAccountWaitMs(account, modelId) {
|
|
102
|
+
if (!account) return 0;
|
|
103
|
+
if (account.isInvalid) return 0;
|
|
104
|
+
if (account.enabled === false) return 0;
|
|
105
|
+
if (isAccountCoolingDown(account)) return 0;
|
|
106
|
+
|
|
107
|
+
return getModelRateLimitWaitMs(account, modelId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getModelRateLimitWaitMs(account, modelId) {
|
|
111
|
+
if (!modelId || !account?.modelRateLimits?.[modelId]) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const limit = account.modelRateLimits[modelId];
|
|
116
|
+
if (!limit?.isRateLimited || !limit.resetTime || limit.resetTime <= Date.now()) {
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return limit.resetTime - Date.now();
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
export {
|
|
85
|
-
createStrategy,
|
|
86
|
-
STRATEGIES,
|
|
87
124
|
markRateLimited,
|
|
88
125
|
markInvalid,
|
|
89
126
|
clearInvalid,
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,7 @@ startServer({ port: PORT, host: HOST });
|
|
|
14
14
|
|
|
15
15
|
console.log(`
|
|
16
16
|
╔══════════════════════════════════════════════════════════════╗
|
|
17
|
-
║ Codex Claude Proxy v1.0
|
|
17
|
+
║ Codex Claude Proxy v1.1.0 ║
|
|
18
18
|
║ (Direct API Mode) ║
|
|
19
19
|
╠══════════════════════════════════════════════════════════════╣
|
|
20
20
|
║ Server: http://${HOST}:${PORT} ║
|
package/src/routes/api-routes.js
CHANGED
|
@@ -21,8 +21,6 @@ import {
|
|
|
21
21
|
handleGetKiloModels,
|
|
22
22
|
handleGetModelMappings,
|
|
23
23
|
handleSetModelMappings,
|
|
24
|
-
handleGetAccountStrategy,
|
|
25
|
-
handleSetAccountStrategy,
|
|
26
24
|
handleGetClaudeProxySetting,
|
|
27
25
|
handleSetClaudeProxySetting
|
|
28
26
|
} from './settings-route.js';
|
|
@@ -77,8 +75,6 @@ export function registerApiRoutes(app, { port }) {
|
|
|
77
75
|
app.get('/settings/kilo-models', handleGetKiloModels);
|
|
78
76
|
app.get('/settings/model-mappings', handleGetModelMappings);
|
|
79
77
|
app.post('/settings/model-mappings', handleSetModelMappings);
|
|
80
|
-
app.get('/settings/account-strategy', handleGetAccountStrategy);
|
|
81
|
-
app.post('/settings/account-strategy', handleSetAccountStrategy);
|
|
82
78
|
app.get('/settings/claude-proxy', handleGetClaudeProxySetting);
|
|
83
79
|
app.post('/settings/claude-proxy', handleSetClaudeProxySetting);
|
|
84
80
|
|
package/src/routes/chat-route.js
CHANGED
|
@@ -77,7 +77,7 @@ export async function handleChatCompletion(req, res) {
|
|
|
77
77
|
: await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
|
|
78
78
|
|
|
79
79
|
const duration = Date.now() - startTime;
|
|
80
|
-
logger.response(200, { model: upstreamModel,
|
|
80
|
+
logger.response(200, { model: upstreamModel, usage: response.usage, duration });
|
|
81
81
|
recordChatMetric({
|
|
82
82
|
body,
|
|
83
83
|
requestedModel,
|
|
@@ -5,8 +5,8 @@ import { sendAuthError, getCredentialsOrError, getCredentialsForAccount } from '
|
|
|
5
5
|
import { initSSEResponse, pipeSSEStream, handleStreamError } from '../middleware/sse.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { AccountRotator } from '../account-rotation/index.js';
|
|
8
|
-
import { listAccounts,
|
|
9
|
-
import {
|
|
8
|
+
import { listAccounts, save } from '../account-manager.js';
|
|
9
|
+
import { isMultiAccountRotationEnabled } from '../server-settings.js';
|
|
10
10
|
import { recordUsageEventSafe, tapUsageEventStream } from '../usage-metrics.js';
|
|
11
11
|
|
|
12
12
|
const MAX_RETRIES = 5;
|
|
@@ -14,20 +14,14 @@ const MAX_WAIT_BEFORE_ERROR_MS = 120000;
|
|
|
14
14
|
const SHORT_RATE_LIMIT_THRESHOLD_MS = 5000;
|
|
15
15
|
|
|
16
16
|
let accountRotator = null;
|
|
17
|
-
let currentStrategy = null;
|
|
18
17
|
|
|
19
18
|
function getAccountRotator() {
|
|
20
|
-
|
|
21
|
-
const strategy = settings.accountStrategy || 'sticky';
|
|
22
|
-
|
|
23
|
-
if (!accountRotator || currentStrategy !== strategy) {
|
|
19
|
+
if (!accountRotator) {
|
|
24
20
|
accountRotator = new AccountRotator({
|
|
25
21
|
listAccounts,
|
|
26
|
-
save
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
currentStrategy = strategy;
|
|
30
|
-
logger.info(`[Messages] Account strategy: ${strategy}`);
|
|
22
|
+
save
|
|
23
|
+
});
|
|
24
|
+
logger.info('[Messages] Account rotation enabled');
|
|
31
25
|
}
|
|
32
26
|
return accountRotator;
|
|
33
27
|
}
|
|
@@ -246,7 +240,9 @@ export async function handleMessages(req, res) {
|
|
|
246
240
|
async function _streamDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
|
|
247
241
|
initSSEResponse(res);
|
|
248
242
|
const sourceStream = sendMessageStream(anthropicRequest, creds.accessToken, creds.accountId, rotator, creds.email);
|
|
243
|
+
let finalUsage = null;
|
|
249
244
|
const stream = tapUsageEventStream(sourceStream, (usage) => {
|
|
245
|
+
finalUsage = usage;
|
|
250
246
|
recordMessageMetric({
|
|
251
247
|
body: anthropicRequest,
|
|
252
248
|
endpoint: '/v1/messages',
|
|
@@ -261,13 +257,13 @@ async function _streamDirectWithRotation(res, anthropicRequest, creds, responseM
|
|
|
261
257
|
});
|
|
262
258
|
});
|
|
263
259
|
await pipeSSEStream(res, stream);
|
|
264
|
-
logger.response(200, { model: anthropicRequest.model, duration: Date.now() - startTime });
|
|
260
|
+
logger.response(200, { model: anthropicRequest.model, usage: finalUsage, duration: Date.now() - startTime });
|
|
265
261
|
}
|
|
266
262
|
|
|
267
263
|
async function _sendDirectWithRotation(res, anthropicRequest, creds, responseModel, startTime, rotator) {
|
|
268
264
|
const response = await sendMessage(anthropicRequest, creds.accessToken, creds.accountId);
|
|
269
265
|
const duration = Date.now() - startTime;
|
|
270
|
-
logger.response(200, { model: anthropicRequest.model,
|
|
266
|
+
logger.response(200, { model: anthropicRequest.model, usage: response.usage, duration });
|
|
271
267
|
recordMessageMetric({
|
|
272
268
|
body: anthropicRequest,
|
|
273
269
|
endpoint: '/v1/messages',
|
|
@@ -287,7 +283,9 @@ async function _sendDirectWithRotation(res, anthropicRequest, creds, responseMod
|
|
|
287
283
|
async function _streamKilo(res, anthropicRequest, kiloTarget, responseModel, startTime) {
|
|
288
284
|
initSSEResponse(res);
|
|
289
285
|
const sourceStream = sendKiloMessageStream(anthropicRequest, kiloTarget);
|
|
286
|
+
let finalUsage = null;
|
|
290
287
|
const stream = tapUsageEventStream(sourceStream, (usage) => {
|
|
288
|
+
finalUsage = usage;
|
|
291
289
|
recordMessageMetric({
|
|
292
290
|
body: anthropicRequest,
|
|
293
291
|
endpoint: '/v1/messages',
|
|
@@ -302,13 +300,13 @@ async function _streamKilo(res, anthropicRequest, kiloTarget, responseModel, sta
|
|
|
302
300
|
});
|
|
303
301
|
});
|
|
304
302
|
await pipeSSEStream(res, stream);
|
|
305
|
-
logger.response(200, { model: kiloTarget, duration: Date.now() - startTime });
|
|
303
|
+
logger.response(200, { model: kiloTarget, usage: finalUsage, duration: Date.now() - startTime });
|
|
306
304
|
}
|
|
307
305
|
|
|
308
306
|
async function _sendKilo(res, anthropicRequest, kiloTarget, responseModel, startTime) {
|
|
309
307
|
const response = await sendKiloMessage(anthropicRequest, kiloTarget);
|
|
310
308
|
const duration = Date.now() - startTime;
|
|
311
|
-
logger.response(200, { model: kiloTarget,
|
|
309
|
+
logger.response(200, { model: kiloTarget, usage: response.usage, duration });
|
|
312
310
|
recordMessageMetric({
|
|
313
311
|
body: anthropicRequest,
|
|
314
312
|
endpoint: '/v1/messages',
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* Handles server settings endpoints:
|
|
4
4
|
* GET /settings/haiku-model
|
|
5
5
|
* POST /settings/haiku-model
|
|
6
|
-
* GET /settings/account-strategy
|
|
7
|
-
* POST /settings/account-strategy
|
|
8
6
|
* GET /settings/claude-proxy
|
|
9
7
|
* POST /settings/claude-proxy
|
|
10
8
|
* GET /settings/kilo-models
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
|
-
import { getServerSettings,
|
|
11
|
+
import { getServerSettings, setServerSettings } from '../server-settings.js';
|
|
14
12
|
import { fetchFreeModels } from '../kilo-models.js';
|
|
15
13
|
import {
|
|
16
14
|
CLAUDE_MODEL_ALIASES,
|
|
@@ -23,7 +21,6 @@ import {
|
|
|
23
21
|
normalizeReasoningMappings
|
|
24
22
|
} from '../model-mapper.js';
|
|
25
23
|
|
|
26
|
-
const VALID_STRATEGIES = ['sticky', 'round-robin'];
|
|
27
24
|
const VALID_OPENAI_MODEL_IDS = new Set(OPENAI_MODEL_OPTIONS.map((model) => model.id));
|
|
28
25
|
const VALID_REASONING_LEVEL_IDS = new Set(REASONING_LEVEL_OPTIONS.map((level) => level.id));
|
|
29
26
|
|
|
@@ -208,41 +205,6 @@ export function handleSetModelMappings(req, res) {
|
|
|
208
205
|
res.json(modelMappingsPayload(nextSettings.modelMappings, nextSettings.reasoningMappings));
|
|
209
206
|
}
|
|
210
207
|
|
|
211
|
-
/**
|
|
212
|
-
* GET /settings/account-strategy
|
|
213
|
-
* Returns the current account selection strategy.
|
|
214
|
-
*/
|
|
215
|
-
export function handleGetAccountStrategy(req, res) {
|
|
216
|
-
const settings = getServerSettings();
|
|
217
|
-
res.json({
|
|
218
|
-
success: true,
|
|
219
|
-
accountStrategy: settings.accountStrategy,
|
|
220
|
-
rotationEnabled: isMultiAccountRotationEnabled()
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* POST /settings/account-strategy
|
|
226
|
-
* Updates the account selection strategy.
|
|
227
|
-
*/
|
|
228
|
-
export function handleSetAccountStrategy(req, res) {
|
|
229
|
-
const { accountStrategy } = req.body || {};
|
|
230
|
-
|
|
231
|
-
if (!VALID_STRATEGIES.includes(accountStrategy)) {
|
|
232
|
-
return res.status(400).json({
|
|
233
|
-
success: false,
|
|
234
|
-
error: `Invalid accountStrategy. Use one of: ${VALID_STRATEGIES.join(', ')}`
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const settings = setServerSettings({ accountStrategy });
|
|
239
|
-
res.json({
|
|
240
|
-
success: true,
|
|
241
|
-
accountStrategy: settings.accountStrategy,
|
|
242
|
-
rotationEnabled: isMultiAccountRotationEnabled()
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
208
|
/**
|
|
247
209
|
* GET /settings/claude-proxy
|
|
248
210
|
* Returns Claude proxy configuration preferences.
|
|
@@ -282,8 +244,6 @@ export default {
|
|
|
282
244
|
handleGetKiloModels,
|
|
283
245
|
handleGetModelMappings,
|
|
284
246
|
handleSetModelMappings,
|
|
285
|
-
handleGetAccountStrategy,
|
|
286
|
-
handleSetAccountStrategy,
|
|
287
247
|
handleGetClaudeProxySetting,
|
|
288
248
|
handleSetClaudeProxySetting
|
|
289
249
|
};
|