@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/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.5',
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
- if (!this.kiloEnabled) return 'gpt-5.4-mini';
52
- const model = this.kiloModels.find(m => m.id === this.haikuKiloModel);
53
- return model ? model.name : this.haikuKiloModel;
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 setClaudeCodeProxyTestConfig() {
522
- const { ok, data, error } = await this.api('/claude/config/set', {
523
- method: 'POST',
524
- body: JSON.stringify({
525
- apiUrl: 'http://localhost:8081',
526
- apiKey: 'test'
527
- })
528
- });
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('Updated Claude Code settings.json (API URL + API key).', 'success');
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);
@@ -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.6
17
+ ║ Codex Claude Proxy v1.0.7
18
18
  ║ (Direct API Mode) ║
19
19
  ╠══════════════════════════════════════════════════════════════╣
20
20
  ║ Server: http://${HOST}:${PORT} ║
@@ -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
- if (CLAUDE_MODEL_MAP[model]) {
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.startsWith('claude-')) {
87
- const cleanModel = modelLower.replace(/^claude-/, '');
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
- for (const [key, value] of Object.entries(CLAUDE_MODEL_MAP)) {
94
- if (modelLower.includes(key.toLowerCase())) {
95
- return value;
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
@@ -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 { handleGetHaikuModel, handleSetHaikuModel, handleGetKiloModels, handleGetAccountStrategy, handleSetAccountStrategy } from './settings-route.js';
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 };