@pikoloo/codex-proxy 1.0.6 → 1.0.7
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/README.md +12 -5
- package/images/dashboard-screenshot.png +0 -0
- package/images/readme-cover.png +0 -0
- package/images/settings-screenshot.png +0 -0
- package/package.json +2 -1
- package/public/css/style.css +247 -0
- package/public/index.html +311 -15
- package/public/js/app.js +275 -14
- package/src/format-converter.js +5 -1
- package/src/index.js +1 -1
- package/src/model-mapper.js +145 -22
- package/src/routes/api-routes.js +21 -1
- package/src/routes/chat-route.js +76 -3
- package/src/routes/messages-route.js +175 -5
- package/src/routes/metrics-route.js +43 -0
- package/src/routes/settings-route.js +148 -2
- package/src/security.js +2 -1
- package/src/server-settings.js +40 -4
- package/src/server.js +27 -2
- package/src/usage-metrics.js +472 -0
package/public/js/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
document.addEventListener('alpine:init', () => {
|
|
2
|
-
const validTabs = ['dashboard', 'accounts', 'logs', 'settings'];
|
|
2
|
+
const validTabs = ['dashboard', 'metrics', 'accounts', '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,7 +7,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
Alpine.data('app', () => ({
|
|
10
|
-
version: '1.0.
|
|
10
|
+
version: '1.0.7',
|
|
11
11
|
connectionStatus: 'connecting',
|
|
12
12
|
activeTab: initialTab(),
|
|
13
13
|
sidebarOpen: window.innerWidth >= 1024,
|
|
@@ -18,12 +18,44 @@ document.addEventListener('alpine:init', () => {
|
|
|
18
18
|
accounts: [],
|
|
19
19
|
searchQuery: '',
|
|
20
20
|
stats: { total: 0, active: 0, expired: 0, planType: '-' },
|
|
21
|
+
metricsRange: '24h',
|
|
22
|
+
metricsStatusFilter: '',
|
|
23
|
+
metricsLoading: false,
|
|
24
|
+
metricsError: '',
|
|
25
|
+
metricsSummary: {
|
|
26
|
+
totals: {
|
|
27
|
+
requestCount: 0,
|
|
28
|
+
successCount: 0,
|
|
29
|
+
errorCount: 0,
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
outputTokens: 0,
|
|
32
|
+
cacheReadInputTokens: 0,
|
|
33
|
+
totalTokens: 0,
|
|
34
|
+
averageDurationMs: 0
|
|
35
|
+
},
|
|
36
|
+
byModel: [],
|
|
37
|
+
byAccount: [],
|
|
38
|
+
timeline: []
|
|
39
|
+
},
|
|
40
|
+
metricsRecent: [],
|
|
41
|
+
metricsStorage: null,
|
|
21
42
|
|
|
22
43
|
haikuKiloModel: 'minimax/minimax-m2.5:free',
|
|
44
|
+
modelMappings: { opus: 'gpt-5.5', sonnet: 'gpt-5.5', haiku: 'gpt-5.4-mini' },
|
|
45
|
+
modelMappingDefaults: { opus: 'gpt-5.5', sonnet: 'gpt-5.5', haiku: 'gpt-5.4-mini' },
|
|
46
|
+
reasoningMappings: { opus: 'high', sonnet: 'medium', haiku: 'low' },
|
|
47
|
+
reasoningMappingDefaults: { opus: 'high', sonnet: 'medium', haiku: 'low' },
|
|
48
|
+
openAiModelOptions: [],
|
|
49
|
+
reasoningLevelOptions: [],
|
|
50
|
+
modelMappingSaving: null,
|
|
51
|
+
reasoningMappingSaving: null,
|
|
23
52
|
accountStrategy: 'sticky',
|
|
24
53
|
multiAccountRotationEnabled: false,
|
|
25
54
|
haikuModelSaving: false,
|
|
26
55
|
strategySaving: false,
|
|
56
|
+
configureClaudeOnStartup: false,
|
|
57
|
+
claudeProxyConfiguring: false,
|
|
58
|
+
claudeProxyStartupSaving: false,
|
|
27
59
|
kiloEnabled: false,
|
|
28
60
|
kiloModels: [],
|
|
29
61
|
kiloModelsLoading: false,
|
|
@@ -48,9 +80,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
48
80
|
haikuTesting: false,
|
|
49
81
|
|
|
50
82
|
haikuModelLabel() {
|
|
51
|
-
|
|
52
|
-
const model = this.kiloModels.find(m => m.id === this.haikuKiloModel);
|
|
53
|
-
return model ? model.name : this.haikuKiloModel;
|
|
83
|
+
return this.modelOptionName(this.modelMappings?.haiku || 'gpt-5.4-mini');
|
|
54
84
|
},
|
|
55
85
|
|
|
56
86
|
async testHaikuChat() {
|
|
@@ -73,6 +103,7 @@ document.addEventListener('alpine:init', () => {
|
|
|
73
103
|
},
|
|
74
104
|
|
|
75
105
|
configPath: '~/.codex-claude-proxy/accounts.json',
|
|
106
|
+
serverUrl: window.location.origin,
|
|
76
107
|
|
|
77
108
|
logs: [],
|
|
78
109
|
logSearchQuery: '',
|
|
@@ -94,6 +125,31 @@ document.addEventListener('alpine:init', () => {
|
|
|
94
125
|
return this.accounts.filter(a => a.email.toLowerCase().includes(q));
|
|
95
126
|
},
|
|
96
127
|
|
|
128
|
+
get metricsTotals() {
|
|
129
|
+
return this.metricsSummary?.totals || {
|
|
130
|
+
requestCount: 0,
|
|
131
|
+
successCount: 0,
|
|
132
|
+
errorCount: 0,
|
|
133
|
+
inputTokens: 0,
|
|
134
|
+
outputTokens: 0,
|
|
135
|
+
cacheReadInputTokens: 0,
|
|
136
|
+
totalTokens: 0,
|
|
137
|
+
averageDurationMs: 0
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
get metricsTimelineMax() {
|
|
142
|
+
return Math.max(1, ...this.metricsSummary.timeline.map((entry) => Number(entry.totalTokens) || 0));
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
get metricsModelMax() {
|
|
146
|
+
return Math.max(1, ...this.metricsSummary.byModel.map((entry) => Number(entry.totalTokens) || 0));
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
get metricsAccountMax() {
|
|
150
|
+
return Math.max(1, ...this.metricsSummary.byAccount.map((entry) => Number(entry.totalTokens) || 0));
|
|
151
|
+
},
|
|
152
|
+
|
|
97
153
|
init() {
|
|
98
154
|
this.updateTime();
|
|
99
155
|
setInterval(() => this.updateTime(), 1000);
|
|
@@ -101,8 +157,11 @@ document.addEventListener('alpine:init', () => {
|
|
|
101
157
|
this.checkHealth();
|
|
102
158
|
setInterval(() => this.checkHealth(), 30000);
|
|
103
159
|
this.startLogStream();
|
|
160
|
+
this.loadModelMappingsSetting();
|
|
104
161
|
this.loadHaikuModelSetting();
|
|
105
162
|
this.loadAccountStrategySetting();
|
|
163
|
+
this.loadClaudeProxySetting();
|
|
164
|
+
this.loadMetrics();
|
|
106
165
|
|
|
107
166
|
window.addEventListener('resize', () => {
|
|
108
167
|
this.sidebarOpen = window.innerWidth >= 1024;
|
|
@@ -124,6 +183,9 @@ document.addEventListener('alpine:init', () => {
|
|
|
124
183
|
setActiveTab(tab) {
|
|
125
184
|
if (!validTabs.includes(tab)) return;
|
|
126
185
|
this.activeTab = tab;
|
|
186
|
+
if (tab === 'metrics') {
|
|
187
|
+
this.loadMetrics();
|
|
188
|
+
}
|
|
127
189
|
const nextUrl = new URL(window.location.href);
|
|
128
190
|
if (tab === 'dashboard') {
|
|
129
191
|
nextUrl.searchParams.delete('tab');
|
|
@@ -174,6 +236,96 @@ document.addEventListener('alpine:init', () => {
|
|
|
174
236
|
this.loading = false;
|
|
175
237
|
},
|
|
176
238
|
|
|
239
|
+
refreshCurrentView() {
|
|
240
|
+
if (this.activeTab === 'metrics') {
|
|
241
|
+
return this.loadMetrics();
|
|
242
|
+
}
|
|
243
|
+
return this.refreshAccounts();
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async loadMetrics() {
|
|
247
|
+
this.metricsLoading = true;
|
|
248
|
+
this.metricsError = '';
|
|
249
|
+
const params = new URLSearchParams({ range: this.metricsRange });
|
|
250
|
+
if (this.metricsStatusFilter) {
|
|
251
|
+
params.set('status', this.metricsStatusFilter);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const [summary, recent, storage] = await Promise.all([
|
|
255
|
+
this.api(`/api/metrics/summary?${params.toString()}`),
|
|
256
|
+
this.api(`/api/metrics/recent?${params.toString()}&limit=50`),
|
|
257
|
+
this.api('/api/metrics/storage')
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
if (summary.ok && summary.data?.summary) {
|
|
261
|
+
this.metricsSummary = summary.data.summary;
|
|
262
|
+
} else {
|
|
263
|
+
this.metricsError = summary.data?.error || summary.error || 'Failed to load metrics';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (recent.ok && Array.isArray(recent.data?.events)) {
|
|
267
|
+
this.metricsRecent = recent.data.events;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (storage.ok && storage.data?.storage) {
|
|
271
|
+
this.metricsStorage = storage.data.storage;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.metricsLoading = false;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
setMetricsRange(range) {
|
|
278
|
+
if (this.metricsRange === range) return;
|
|
279
|
+
this.metricsRange = range;
|
|
280
|
+
this.loadMetrics();
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
setMetricsStatusFilter(status) {
|
|
284
|
+
if (this.metricsStatusFilter === status) return;
|
|
285
|
+
this.metricsStatusFilter = status;
|
|
286
|
+
this.loadMetrics();
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
metricBarWidth(value, maxValue) {
|
|
290
|
+
const valueNumber = Number(value) || 0;
|
|
291
|
+
const maxNumber = Number(maxValue) || 1;
|
|
292
|
+
return Math.max(2, Math.min(100, Math.round((valueNumber / maxNumber) * 100)));
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
formatTokenCount(value) {
|
|
296
|
+
const number = Number(value) || 0;
|
|
297
|
+
if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`;
|
|
298
|
+
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
|
|
299
|
+
return String(number);
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
formatDuration(value) {
|
|
303
|
+
const number = Number(value) || 0;
|
|
304
|
+
if (number >= 1000) return `${(number / 1000).toFixed(1)}s`;
|
|
305
|
+
return `${number}ms`;
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
formatBytes(value) {
|
|
309
|
+
const number = Number(value) || 0;
|
|
310
|
+
if (number >= 1024 * 1024) return `${(number / (1024 * 1024)).toFixed(1)} MB`;
|
|
311
|
+
if (number >= 1024) return `${(number / 1024).toFixed(1)} KB`;
|
|
312
|
+
return `${number} B`;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
formatMetricTime(value) {
|
|
316
|
+
if (!value) return '-';
|
|
317
|
+
const date = new Date(value);
|
|
318
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
319
|
+
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
metricsStatusClass(status) {
|
|
323
|
+
const code = Number(status) || 0;
|
|
324
|
+
if (code >= 200 && code < 400) return 'metrics-status-success';
|
|
325
|
+
if (code >= 400) return 'metrics-status-error';
|
|
326
|
+
return 'metrics-status-muted';
|
|
327
|
+
},
|
|
328
|
+
|
|
177
329
|
async refreshAllQuotaData() {
|
|
178
330
|
if (!this.accounts.length) return;
|
|
179
331
|
const { ok, data } = await this.api('/accounts/quota/all');
|
|
@@ -493,6 +645,86 @@ document.addEventListener('alpine:init', () => {
|
|
|
493
645
|
}
|
|
494
646
|
},
|
|
495
647
|
|
|
648
|
+
modelOptionName(modelId) {
|
|
649
|
+
const option = this.openAiModelOptions.find((model) => model.id === modelId);
|
|
650
|
+
return option ? option.name : modelId;
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
modelMappingLabel(alias) {
|
|
654
|
+
const labels = { opus: 'Opus', sonnet: 'Sonnet', haiku: 'Haiku' };
|
|
655
|
+
return labels[alias] || alias;
|
|
656
|
+
},
|
|
657
|
+
|
|
658
|
+
async loadModelMappingsSetting() {
|
|
659
|
+
const { ok, data } = await this.api('/settings/model-mappings');
|
|
660
|
+
if (!ok || !data?.modelMappings) return;
|
|
661
|
+
|
|
662
|
+
this.modelMappings = data.modelMappings;
|
|
663
|
+
this.modelMappingDefaults = data.defaults || this.modelMappingDefaults;
|
|
664
|
+
this.reasoningMappings = data.reasoningMappings || this.reasoningMappings;
|
|
665
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
666
|
+
this.openAiModelOptions = Array.isArray(data.models) ? data.models : [];
|
|
667
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : [];
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
async setModelMapping(alias, model) {
|
|
671
|
+
if (this.modelMappingSaving || !alias || !model) return;
|
|
672
|
+
|
|
673
|
+
const previous = this.modelMappings[alias];
|
|
674
|
+
if (previous === model) return;
|
|
675
|
+
|
|
676
|
+
this.modelMappings = { ...this.modelMappings, [alias]: model };
|
|
677
|
+
this.modelMappingSaving = alias;
|
|
678
|
+
const { ok, data } = await this.api('/settings/model-mappings', {
|
|
679
|
+
method: 'POST',
|
|
680
|
+
body: JSON.stringify({ modelMappings: { [alias]: model } })
|
|
681
|
+
});
|
|
682
|
+
this.modelMappingSaving = null;
|
|
683
|
+
|
|
684
|
+
if (ok && data?.modelMappings) {
|
|
685
|
+
this.modelMappings = data.modelMappings;
|
|
686
|
+
this.modelMappingDefaults = data.defaults || this.modelMappingDefaults;
|
|
687
|
+
this.reasoningMappings = data.reasoningMappings || this.reasoningMappings;
|
|
688
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
689
|
+
this.openAiModelOptions = Array.isArray(data.models) ? data.models : this.openAiModelOptions;
|
|
690
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : this.reasoningLevelOptions;
|
|
691
|
+
this.showToast(`${this.modelMappingLabel(alias)} now maps to ${this.modelOptionName(data.modelMappings[alias])}`, 'success');
|
|
692
|
+
} else {
|
|
693
|
+
this.modelMappings = { ...this.modelMappings, [alias]: previous };
|
|
694
|
+
this.showToast(data?.error || 'Failed to update model mapping', 'error');
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
reasoningOptionName(reasoningId) {
|
|
699
|
+
const option = this.reasoningLevelOptions.find((level) => level.id === reasoningId);
|
|
700
|
+
return option ? option.name : reasoningId;
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
async setReasoningMapping(alias, reasoning) {
|
|
704
|
+
if (this.reasoningMappingSaving || !alias || !reasoning) return;
|
|
705
|
+
|
|
706
|
+
const previous = this.reasoningMappings[alias];
|
|
707
|
+
if (previous === reasoning) return;
|
|
708
|
+
|
|
709
|
+
this.reasoningMappings = { ...this.reasoningMappings, [alias]: reasoning };
|
|
710
|
+
this.reasoningMappingSaving = alias;
|
|
711
|
+
const { ok, data } = await this.api('/settings/model-mappings', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
body: JSON.stringify({ reasoningMappings: { [alias]: reasoning } })
|
|
714
|
+
});
|
|
715
|
+
this.reasoningMappingSaving = null;
|
|
716
|
+
|
|
717
|
+
if (ok && data?.reasoningMappings) {
|
|
718
|
+
this.reasoningMappings = data.reasoningMappings;
|
|
719
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
720
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : this.reasoningLevelOptions;
|
|
721
|
+
this.showToast(`${this.modelMappingLabel(alias)} reasoning set to ${this.reasoningOptionName(data.reasoningMappings[alias])}`, 'success');
|
|
722
|
+
} else {
|
|
723
|
+
this.reasoningMappings = { ...this.reasoningMappings, [alias]: previous };
|
|
724
|
+
this.showToast(data?.error || 'Failed to update reasoning level', 'error');
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
|
|
496
728
|
async loadAccountStrategySetting() {
|
|
497
729
|
const { ok, data } = await this.api('/settings/account-strategy');
|
|
498
730
|
if (ok && data?.accountStrategy) {
|
|
@@ -518,22 +750,51 @@ document.addEventListener('alpine:init', () => {
|
|
|
518
750
|
}
|
|
519
751
|
},
|
|
520
752
|
|
|
521
|
-
async
|
|
522
|
-
const { ok, data
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
753
|
+
async loadClaudeProxySetting() {
|
|
754
|
+
const { ok, data } = await this.api('/settings/claude-proxy');
|
|
755
|
+
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
756
|
+
this.configureClaudeOnStartup = data.configureClaudeOnStartup;
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
async configureClaudeProxy() {
|
|
761
|
+
if (this.claudeProxyConfiguring) return;
|
|
762
|
+
this.claudeProxyConfiguring = true;
|
|
763
|
+
const { ok, data, error } = await this.api('/claude/config/proxy', { method: 'POST' });
|
|
764
|
+
this.claudeProxyConfiguring = false;
|
|
529
765
|
|
|
530
766
|
if (ok && data?.success) {
|
|
531
|
-
this.showToast('
|
|
767
|
+
this.showToast(data.message || 'Claude Code configured to use this proxy.', 'success');
|
|
532
768
|
} else {
|
|
533
769
|
this.showToast(data?.error || error || 'Failed to update Claude Code settings.json', 'error');
|
|
534
770
|
}
|
|
535
771
|
},
|
|
536
772
|
|
|
773
|
+
async setConfigureClaudeOnStartup(enabled) {
|
|
774
|
+
if (this.claudeProxyStartupSaving) return;
|
|
775
|
+
const previous = this.configureClaudeOnStartup;
|
|
776
|
+
this.configureClaudeOnStartup = enabled;
|
|
777
|
+
this.claudeProxyStartupSaving = true;
|
|
778
|
+
const { ok, data, error } = await this.api('/settings/claude-proxy', {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
body: JSON.stringify({ configureClaudeOnStartup: enabled })
|
|
781
|
+
});
|
|
782
|
+
this.claudeProxyStartupSaving = false;
|
|
783
|
+
|
|
784
|
+
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
785
|
+
this.configureClaudeOnStartup = data.configureClaudeOnStartup;
|
|
786
|
+
this.showToast(
|
|
787
|
+
data.configureClaudeOnStartup
|
|
788
|
+
? 'Claude Code will be configured on proxy startup.'
|
|
789
|
+
: 'Startup Claude Code configuration disabled.',
|
|
790
|
+
'success'
|
|
791
|
+
);
|
|
792
|
+
} else {
|
|
793
|
+
this.configureClaudeOnStartup = previous;
|
|
794
|
+
this.showToast(data?.error || error || 'Failed to update startup setting', 'error');
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
|
|
537
798
|
showToast(message, type = 'success') {
|
|
538
799
|
this.toast = { message, type };
|
|
539
800
|
setTimeout(() => { this.toast = null; }, 3000);
|
package/src/format-converter.js
CHANGED
|
@@ -64,7 +64,7 @@ function extractSystemPrompt(system) {
|
|
|
64
64
|
* Convert Anthropic Messages API request to OpenAI Responses API format
|
|
65
65
|
*/
|
|
66
66
|
export function convertAnthropicToResponsesAPI(anthropicRequest) {
|
|
67
|
-
const { model, messages, system, tools, tool_choice } = anthropicRequest;
|
|
67
|
+
const { model, messages, system, tools, tool_choice, reasoningLevel } = anthropicRequest;
|
|
68
68
|
|
|
69
69
|
// [CRITICAL] Clean cache_control from all messages FIRST
|
|
70
70
|
// Claude Code CLI sends cache_control fields that the API rejects
|
|
@@ -89,6 +89,10 @@ export function convertAnthropicToResponsesAPI(anthropicRequest) {
|
|
|
89
89
|
request.instructions = '';
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
if (reasoningLevel) {
|
|
93
|
+
request.reasoning = { effort: reasoningLevel };
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
return request;
|
|
93
97
|
}
|
|
94
98
|
|
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.0.7 ║
|
|
18
18
|
║ (Direct API Mode) ║
|
|
19
19
|
╠══════════════════════════════════════════════════════════════╣
|
|
20
20
|
║ Server: http://${HOST}:${PORT} ║
|
package/src/model-mapper.js
CHANGED
|
@@ -9,6 +9,43 @@ const DEFAULT_OPENAI_MODEL = 'gpt-5.5';
|
|
|
9
9
|
const DEFAULT_SMALL_OPENAI_MODEL = 'gpt-5.4-mini';
|
|
10
10
|
const LATEST_CODEX_MODEL = 'gpt-5.3-codex';
|
|
11
11
|
const KILO_ENABLED_ENV = 'CODEX_CLAUDE_PROXY_ENABLE_KILO';
|
|
12
|
+
const CLAUDE_MODEL_ALIASES = ['opus', 'sonnet', 'haiku'];
|
|
13
|
+
const OPENAI_MODEL_OPTIONS = [
|
|
14
|
+
{ id: 'gpt-5.5', name: 'GPT-5.5' },
|
|
15
|
+
{ id: 'gpt-5.5-2026-04-23', name: 'GPT-5.5 Snapshot' },
|
|
16
|
+
{ id: 'gpt-5.4', name: 'GPT-5.4' },
|
|
17
|
+
{ id: 'gpt-5.4-2026-03-05', name: 'GPT-5.4 Snapshot' },
|
|
18
|
+
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' },
|
|
19
|
+
{ id: 'gpt-5.4-nano', name: 'GPT-5.4 Nano' },
|
|
20
|
+
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' },
|
|
21
|
+
{ id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex' },
|
|
22
|
+
{ id: 'gpt-5.2', name: 'GPT-5.2' },
|
|
23
|
+
{ id: 'gpt-5.1', name: 'GPT-5.1' },
|
|
24
|
+
{ id: 'gpt-5', name: 'GPT-5' },
|
|
25
|
+
{ id: 'gpt-5.1-codex-max', name: 'GPT-5.1 Codex Max' },
|
|
26
|
+
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' },
|
|
27
|
+
{ id: 'gpt-5-codex', name: 'GPT-5 Codex' },
|
|
28
|
+
{ id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini' },
|
|
29
|
+
{ id: 'gpt-5-codex-mini', name: 'GPT-5 Codex Mini' }
|
|
30
|
+
];
|
|
31
|
+
const OPENAI_MODEL_IDS = new Set(OPENAI_MODEL_OPTIONS.map((model) => model.id));
|
|
32
|
+
const REASONING_LEVEL_OPTIONS = [
|
|
33
|
+
{ id: 'low', name: 'Low', description: 'Fast responses with lighter reasoning' },
|
|
34
|
+
{ id: 'medium', name: 'Medium', description: 'Balanced speed and reasoning depth' },
|
|
35
|
+
{ id: 'high', name: 'High', description: 'Greater reasoning depth for complex work' },
|
|
36
|
+
{ id: 'xhigh', name: 'Extra High', description: 'Extra high reasoning depth for complex work' }
|
|
37
|
+
];
|
|
38
|
+
const REASONING_LEVEL_IDS = new Set(REASONING_LEVEL_OPTIONS.map((level) => level.id));
|
|
39
|
+
const DEFAULT_MODEL_MAPPINGS = {
|
|
40
|
+
opus: DEFAULT_OPENAI_MODEL,
|
|
41
|
+
sonnet: DEFAULT_OPENAI_MODEL,
|
|
42
|
+
haiku: DEFAULT_SMALL_OPENAI_MODEL
|
|
43
|
+
};
|
|
44
|
+
const DEFAULT_REASONING_MAPPINGS = {
|
|
45
|
+
opus: 'high',
|
|
46
|
+
sonnet: 'medium',
|
|
47
|
+
haiku: 'low'
|
|
48
|
+
};
|
|
12
49
|
|
|
13
50
|
const CLAUDE_MODEL_MAP = {
|
|
14
51
|
// Current Claude 4.6 models (Feb 2026)
|
|
@@ -64,41 +101,114 @@ const CLAUDE_MODEL_MAP = {
|
|
|
64
101
|
'gpt-5-codex-mini': 'gpt-5-codex-mini'
|
|
65
102
|
};
|
|
66
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Normalizes persisted Claude alias mappings against supported GPT targets.
|
|
106
|
+
* @param {Record<string, string>} modelMappings
|
|
107
|
+
* @returns {{ opus: string, sonnet: string, haiku: string }}
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeModelMappings(modelMappings = {}) {
|
|
110
|
+
const normalized = { ...DEFAULT_MODEL_MAPPINGS };
|
|
111
|
+
if (!modelMappings || typeof modelMappings !== 'object' || Array.isArray(modelMappings)) {
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const alias of CLAUDE_MODEL_ALIASES) {
|
|
116
|
+
const candidate = modelMappings[alias];
|
|
117
|
+
if (typeof candidate === 'string' && OPENAI_MODEL_IDS.has(candidate)) {
|
|
118
|
+
normalized[alias] = candidate;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalizes persisted Claude alias reasoning mappings against supported efforts.
|
|
127
|
+
* @param {Record<string, string>} reasoningMappings
|
|
128
|
+
* @returns {{ opus: string, sonnet: string, haiku: string }}
|
|
129
|
+
*/
|
|
130
|
+
export function normalizeReasoningMappings(reasoningMappings = {}) {
|
|
131
|
+
const normalized = { ...DEFAULT_REASONING_MAPPINGS };
|
|
132
|
+
if (!reasoningMappings || typeof reasoningMappings !== 'object' || Array.isArray(reasoningMappings)) {
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const alias of CLAUDE_MODEL_ALIASES) {
|
|
137
|
+
const candidate = reasoningMappings[alias];
|
|
138
|
+
if (typeof candidate === 'string' && REASONING_LEVEL_IDS.has(candidate)) {
|
|
139
|
+
normalized[alias] = candidate;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return normalized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inferClaudeAlias(modelLower) {
|
|
147
|
+
for (const alias of CLAUDE_MODEL_ALIASES) {
|
|
148
|
+
if (modelLower === alias || modelLower.includes(alias)) {
|
|
149
|
+
return alias;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
67
155
|
/**
|
|
68
156
|
* Maps a Claude/Anthropic model name to the upstream model identifier.
|
|
69
157
|
* Falls back to the current OpenAI flagship model for unknown models.
|
|
70
158
|
* @param {string} model
|
|
159
|
+
* @param {{ modelMappings?: Record<string, string> }} settings
|
|
71
160
|
* @returns {string}
|
|
72
161
|
*/
|
|
73
|
-
export function mapClaudeModel(model) {
|
|
162
|
+
export function mapClaudeModel(model, settings = getServerSettings()) {
|
|
74
163
|
if (!model) return DEFAULT_OPENAI_MODEL;
|
|
75
164
|
|
|
76
|
-
|
|
77
|
-
return CLAUDE_MODEL_MAP[model];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const modelLower = model.toLowerCase();
|
|
165
|
+
const modelLower = String(model).toLowerCase();
|
|
81
166
|
|
|
82
167
|
if (modelLower.startsWith('gpt-')) {
|
|
83
168
|
return modelLower;
|
|
84
169
|
}
|
|
85
170
|
|
|
86
|
-
if (modelLower
|
|
87
|
-
|
|
88
|
-
if (cleanModel.includes('opus')) return DEFAULT_OPENAI_MODEL;
|
|
89
|
-
if (cleanModel.includes('sonnet')) return DEFAULT_OPENAI_MODEL;
|
|
90
|
-
if (cleanModel.includes('haiku')) return DEFAULT_SMALL_OPENAI_MODEL;
|
|
171
|
+
if (modelLower === 'codex') {
|
|
172
|
+
return LATEST_CODEX_MODEL;
|
|
91
173
|
}
|
|
92
174
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
if (modelLower === 'kilo') {
|
|
176
|
+
return 'kilo';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mappedAlias = inferClaudeAlias(modelLower);
|
|
180
|
+
if (mappedAlias) {
|
|
181
|
+
return normalizeModelMappings(settings?.modelMappings)[mappedAlias];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (CLAUDE_MODEL_MAP[modelLower]) {
|
|
185
|
+
return CLAUDE_MODEL_MAP[modelLower];
|
|
97
186
|
}
|
|
98
187
|
|
|
99
188
|
return DEFAULT_OPENAI_MODEL;
|
|
100
189
|
}
|
|
101
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Resolves the configured reasoning effort for Claude aliases.
|
|
193
|
+
* Direct GPT, codex, kilo, and unknown model requests do not force a reasoning level.
|
|
194
|
+
* @param {string} model
|
|
195
|
+
* @param {{ reasoningMappings?: Record<string, string> }} settings
|
|
196
|
+
* @returns {string|null}
|
|
197
|
+
*/
|
|
198
|
+
export function mapClaudeReasoningLevel(model, settings = getServerSettings()) {
|
|
199
|
+
if (!model) return null;
|
|
200
|
+
|
|
201
|
+
const modelLower = String(model).toLowerCase();
|
|
202
|
+
if (modelLower.startsWith('gpt-') || modelLower === 'codex' || modelLower === 'kilo') {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const mappedAlias = inferClaudeAlias(modelLower);
|
|
207
|
+
if (!mappedAlias) return null;
|
|
208
|
+
|
|
209
|
+
return normalizeReasoningMappings(settings?.reasoningMappings)[mappedAlias];
|
|
210
|
+
}
|
|
211
|
+
|
|
102
212
|
/**
|
|
103
213
|
* Returns true if the mapped model should be routed through Kilo.
|
|
104
214
|
* @param {string} mappedModel
|
|
@@ -117,40 +227,53 @@ export function isKiloEnabled() {
|
|
|
117
227
|
* The setting stores the full Kilo model ID (e.g. 'minimax/minimax-m2.5:free').
|
|
118
228
|
* @returns {string}
|
|
119
229
|
*/
|
|
120
|
-
export function resolveKiloModel() {
|
|
121
|
-
const settings = getServerSettings();
|
|
230
|
+
export function resolveKiloModel(settings = getServerSettings()) {
|
|
122
231
|
return settings.haikuKiloModel || 'minimax/minimax-m2.5:free';
|
|
123
232
|
}
|
|
124
233
|
|
|
125
234
|
/**
|
|
126
235
|
* Resolves all model routing info from a requested model name.
|
|
127
236
|
* @param {string} requestedModel
|
|
128
|
-
* @returns {{ mappedModel: string, isKilo: boolean, kiloTarget: string|null, upstreamModel: string }}
|
|
237
|
+
* @returns {{ mappedModel: string, isKilo: boolean, kiloTarget: string|null, upstreamModel: string, reasoningLevel: string|null }}
|
|
129
238
|
*/
|
|
130
|
-
export function resolveModelRouting(requestedModel) {
|
|
131
|
-
const mappedModel = mapClaudeModel(requestedModel || DEFAULT_OPENAI_MODEL);
|
|
239
|
+
export function resolveModelRouting(requestedModel, settings = getServerSettings()) {
|
|
240
|
+
const mappedModel = mapClaudeModel(requestedModel || DEFAULT_OPENAI_MODEL, settings);
|
|
241
|
+
const reasoningLevel = mapClaudeReasoningLevel(requestedModel, settings);
|
|
132
242
|
const isKilo = isKiloModel(mappedModel);
|
|
133
|
-
const kiloTarget = isKilo ? resolveKiloModel() : null;
|
|
243
|
+
const kiloTarget = isKilo ? resolveKiloModel(settings) : null;
|
|
134
244
|
const upstreamModel = isKilo ? kiloTarget : mappedModel;
|
|
135
|
-
return { mappedModel, isKilo, kiloTarget, upstreamModel };
|
|
245
|
+
return { mappedModel, isKilo, kiloTarget, upstreamModel, reasoningLevel };
|
|
136
246
|
}
|
|
137
247
|
|
|
138
248
|
export {
|
|
139
249
|
CLAUDE_MODEL_MAP,
|
|
140
250
|
DEFAULT_OPENAI_MODEL,
|
|
141
251
|
DEFAULT_SMALL_OPENAI_MODEL,
|
|
252
|
+
DEFAULT_MODEL_MAPPINGS,
|
|
253
|
+
DEFAULT_REASONING_MAPPINGS,
|
|
254
|
+
CLAUDE_MODEL_ALIASES,
|
|
255
|
+
OPENAI_MODEL_OPTIONS,
|
|
256
|
+
REASONING_LEVEL_OPTIONS,
|
|
142
257
|
LATEST_CODEX_MODEL,
|
|
143
258
|
KILO_ENABLED_ENV
|
|
144
259
|
};
|
|
145
260
|
|
|
146
261
|
export default {
|
|
147
262
|
mapClaudeModel,
|
|
263
|
+
mapClaudeReasoningLevel,
|
|
148
264
|
isKiloModel,
|
|
149
265
|
resolveKiloModel,
|
|
150
266
|
resolveModelRouting,
|
|
151
267
|
CLAUDE_MODEL_MAP,
|
|
152
268
|
DEFAULT_OPENAI_MODEL,
|
|
153
269
|
DEFAULT_SMALL_OPENAI_MODEL,
|
|
270
|
+
DEFAULT_MODEL_MAPPINGS,
|
|
271
|
+
DEFAULT_REASONING_MAPPINGS,
|
|
272
|
+
CLAUDE_MODEL_ALIASES,
|
|
273
|
+
OPENAI_MODEL_OPTIONS,
|
|
274
|
+
REASONING_LEVEL_OPTIONS,
|
|
275
|
+
normalizeModelMappings,
|
|
276
|
+
normalizeReasoningMappings,
|
|
154
277
|
LATEST_CODEX_MODEL,
|
|
155
278
|
KILO_ENABLED_ENV,
|
|
156
279
|
isKiloEnabled
|
package/src/routes/api-routes.js
CHANGED
|
@@ -15,8 +15,19 @@ import { getStatus, ACCOUNTS_FILE } from '../account-manager.js';
|
|
|
15
15
|
import { handleMessages } from './messages-route.js';
|
|
16
16
|
import { handleChatCompletion, handleCountTokens } from './chat-route.js';
|
|
17
17
|
import { handleListModels, handleAccountModels, handleAccountUsage } from './models-route.js';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
handleGetHaikuModel,
|
|
20
|
+
handleSetHaikuModel,
|
|
21
|
+
handleGetKiloModels,
|
|
22
|
+
handleGetModelMappings,
|
|
23
|
+
handleSetModelMappings,
|
|
24
|
+
handleGetAccountStrategy,
|
|
25
|
+
handleSetAccountStrategy,
|
|
26
|
+
handleGetClaudeProxySetting,
|
|
27
|
+
handleSetClaudeProxySetting
|
|
28
|
+
} from './settings-route.js';
|
|
19
29
|
import { handleGetLogs, handleStreamLogs } from './logs-route.js';
|
|
30
|
+
import { handleGetMetricsRecent, handleGetMetricsStorage, handleGetMetricsSummary } from './metrics-route.js';
|
|
20
31
|
import { handleGetClaudeConfig, handleSetProxyMode, handleSetDirectMode, handleSetClaudeApiEndpoint } from './claude-config-route.js';
|
|
21
32
|
import {
|
|
22
33
|
handleListAccounts,
|
|
@@ -64,8 +75,12 @@ export function registerApiRoutes(app, { port }) {
|
|
|
64
75
|
app.get('/settings/haiku-model', handleGetHaikuModel);
|
|
65
76
|
app.post('/settings/haiku-model', handleSetHaikuModel);
|
|
66
77
|
app.get('/settings/kilo-models', handleGetKiloModels);
|
|
78
|
+
app.get('/settings/model-mappings', handleGetModelMappings);
|
|
79
|
+
app.post('/settings/model-mappings', handleSetModelMappings);
|
|
67
80
|
app.get('/settings/account-strategy', handleGetAccountStrategy);
|
|
68
81
|
app.post('/settings/account-strategy', handleSetAccountStrategy);
|
|
82
|
+
app.get('/settings/claude-proxy', handleGetClaudeProxySetting);
|
|
83
|
+
app.post('/settings/claude-proxy', handleSetClaudeProxySetting);
|
|
69
84
|
|
|
70
85
|
// ─── Account Management ───────────────────────────────────────────────────
|
|
71
86
|
app.get('/accounts', handleListAccounts);
|
|
@@ -93,6 +108,11 @@ export function registerApiRoutes(app, { port }) {
|
|
|
93
108
|
// ─── Logs ──────────────────────────────────────────────────────────────────
|
|
94
109
|
app.get('/api/logs', handleGetLogs);
|
|
95
110
|
app.get('/api/logs/stream', handleStreamLogs);
|
|
111
|
+
|
|
112
|
+
// ─── Metrics ───────────────────────────────────────────────────────────────
|
|
113
|
+
app.get('/api/metrics/summary', handleGetMetricsSummary);
|
|
114
|
+
app.get('/api/metrics/recent', handleGetMetricsRecent);
|
|
115
|
+
app.get('/api/metrics/storage', handleGetMetricsStorage);
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
export default { registerApiRoutes };
|