@pikoloo/codex-proxy 1.0.6 → 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/README.md +12 -5
- 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 +11 -3
- package/public/css/style.css +1097 -40
- package/public/index.html +439 -184
- package/public/js/app.js +384 -66
- package/src/account-rotation/index.js +64 -27
- 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 +19 -3
- package/src/routes/chat-route.js +77 -4
- package/src/routes/messages-route.js +189 -21
- package/src/routes/metrics-route.js +43 -0
- package/src/routes/settings-route.js +127 -21
- package/src/security.js +2 -1
- package/src/server-settings.js +40 -5
- package/src/server.js +27 -2
- package/src/usage-metrics.js +472 -0
- 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
|
@@ -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,10 +7,9 @@ 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
|
-
sidebarOpen: window.innerWidth >= 1024,
|
|
14
13
|
loading: false,
|
|
15
14
|
toast: null,
|
|
16
15
|
currentTime: '',
|
|
@@ -18,12 +17,41 @@ document.addEventListener('alpine:init', () => {
|
|
|
18
17
|
accounts: [],
|
|
19
18
|
searchQuery: '',
|
|
20
19
|
stats: { total: 0, active: 0, expired: 0, planType: '-' },
|
|
20
|
+
metricsRange: '24h',
|
|
21
|
+
metricsStatusFilter: '',
|
|
22
|
+
metricsLoading: false,
|
|
23
|
+
metricsError: '',
|
|
24
|
+
metricsSummary: {
|
|
25
|
+
totals: {
|
|
26
|
+
requestCount: 0,
|
|
27
|
+
successCount: 0,
|
|
28
|
+
errorCount: 0,
|
|
29
|
+
inputTokens: 0,
|
|
30
|
+
outputTokens: 0,
|
|
31
|
+
cacheReadInputTokens: 0,
|
|
32
|
+
totalTokens: 0,
|
|
33
|
+
averageDurationMs: 0
|
|
34
|
+
},
|
|
35
|
+
byModel: [],
|
|
36
|
+
byAccount: [],
|
|
37
|
+
timeline: []
|
|
38
|
+
},
|
|
39
|
+
metricsRecent: [],
|
|
40
|
+
metricsStorage: null,
|
|
21
41
|
|
|
22
42
|
haikuKiloModel: 'minimax/minimax-m2.5:free',
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
modelMappings: { opus: 'gpt-5.5', sonnet: 'gpt-5.5', haiku: 'gpt-5.4-mini' },
|
|
44
|
+
modelMappingDefaults: { opus: 'gpt-5.5', sonnet: 'gpt-5.5', haiku: 'gpt-5.4-mini' },
|
|
45
|
+
reasoningMappings: { opus: 'high', sonnet: 'medium', haiku: 'low' },
|
|
46
|
+
reasoningMappingDefaults: { opus: 'high', sonnet: 'medium', haiku: 'low' },
|
|
47
|
+
openAiModelOptions: [],
|
|
48
|
+
reasoningLevelOptions: [],
|
|
49
|
+
modelMappingSaving: null,
|
|
50
|
+
reasoningMappingSaving: null,
|
|
25
51
|
haikuModelSaving: false,
|
|
26
|
-
|
|
52
|
+
configureClaudeOnStartup: false,
|
|
53
|
+
claudeProxyConfiguring: false,
|
|
54
|
+
claudeProxyStartupSaving: false,
|
|
27
55
|
kiloEnabled: false,
|
|
28
56
|
kiloModels: [],
|
|
29
57
|
kiloModelsLoading: false,
|
|
@@ -41,43 +69,83 @@ document.addEventListener('alpine:init', () => {
|
|
|
41
69
|
|
|
42
70
|
testPrompt: 'Say hello',
|
|
43
71
|
testResponse: '',
|
|
72
|
+
testStatus: 'idle',
|
|
73
|
+
testError: '',
|
|
74
|
+
testMeta: null,
|
|
44
75
|
testing: false,
|
|
45
76
|
|
|
46
77
|
haikuTestPrompt: 'Say hello',
|
|
47
78
|
haikuTestResponse: '',
|
|
79
|
+
haikuTestStatus: 'idle',
|
|
80
|
+
haikuTestError: '',
|
|
81
|
+
haikuTestMeta: null,
|
|
48
82
|
haikuTesting: false,
|
|
49
83
|
|
|
50
84
|
haikuModelLabel() {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
return this.modelOptionName(this.modelMappings?.haiku || 'gpt-5.4-mini');
|
|
86
|
+
},
|
|
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';
|
|
54
106
|
},
|
|
55
107
|
|
|
56
108
|
async testHaikuChat() {
|
|
57
109
|
if (!this.haikuTestPrompt.trim()) return;
|
|
110
|
+
const startedAt = Date.now();
|
|
58
111
|
this.haikuTesting = true;
|
|
59
112
|
this.haikuTestResponse = '';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
72
138
|
}
|
|
73
139
|
},
|
|
74
140
|
|
|
75
141
|
configPath: '~/.codex-claude-proxy/accounts.json',
|
|
142
|
+
serverUrl: window.location.origin,
|
|
76
143
|
|
|
77
144
|
logs: [],
|
|
78
145
|
logSearchQuery: '',
|
|
79
146
|
logFilters: { INFO: true, SUCCESS: true, WARN: true, ERROR: true, DEBUG: false },
|
|
80
147
|
logEventSource: null,
|
|
148
|
+
logStreamStatus: 'connecting',
|
|
81
149
|
|
|
82
150
|
get filteredLogs() {
|
|
83
151
|
const query = this.logSearchQuery.trim().toLowerCase();
|
|
@@ -88,12 +156,44 @@ document.addEventListener('alpine:init', () => {
|
|
|
88
156
|
});
|
|
89
157
|
},
|
|
90
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
|
+
|
|
91
166
|
get filteredAccounts() {
|
|
92
167
|
if (!this.searchQuery) return this.accounts;
|
|
93
168
|
const q = this.searchQuery.toLowerCase();
|
|
94
169
|
return this.accounts.filter(a => a.email.toLowerCase().includes(q));
|
|
95
170
|
},
|
|
96
171
|
|
|
172
|
+
get metricsTotals() {
|
|
173
|
+
return this.metricsSummary?.totals || {
|
|
174
|
+
requestCount: 0,
|
|
175
|
+
successCount: 0,
|
|
176
|
+
errorCount: 0,
|
|
177
|
+
inputTokens: 0,
|
|
178
|
+
outputTokens: 0,
|
|
179
|
+
cacheReadInputTokens: 0,
|
|
180
|
+
totalTokens: 0,
|
|
181
|
+
averageDurationMs: 0
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
get metricsTimelineMax() {
|
|
186
|
+
return Math.max(1, ...this.metricsSummary.timeline.map((entry) => Number(entry.totalTokens) || 0));
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
get metricsModelMax() {
|
|
190
|
+
return Math.max(1, ...this.metricsSummary.byModel.map((entry) => Number(entry.totalTokens) || 0));
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
get metricsAccountMax() {
|
|
194
|
+
return Math.max(1, ...this.metricsSummary.byAccount.map((entry) => Number(entry.totalTokens) || 0));
|
|
195
|
+
},
|
|
196
|
+
|
|
97
197
|
init() {
|
|
98
198
|
this.updateTime();
|
|
99
199
|
setInterval(() => this.updateTime(), 1000);
|
|
@@ -101,12 +201,10 @@ document.addEventListener('alpine:init', () => {
|
|
|
101
201
|
this.checkHealth();
|
|
102
202
|
setInterval(() => this.checkHealth(), 30000);
|
|
103
203
|
this.startLogStream();
|
|
204
|
+
this.loadModelMappingsSetting();
|
|
104
205
|
this.loadHaikuModelSetting();
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
window.addEventListener('resize', () => {
|
|
108
|
-
this.sidebarOpen = window.innerWidth >= 1024;
|
|
109
|
-
});
|
|
206
|
+
this.loadClaudeProxySetting();
|
|
207
|
+
this.loadMetrics();
|
|
110
208
|
|
|
111
209
|
window.addEventListener('message', (event) => {
|
|
112
210
|
if (event.data && event.data.type === 'oauth-success') {
|
|
@@ -124,6 +222,9 @@ document.addEventListener('alpine:init', () => {
|
|
|
124
222
|
setActiveTab(tab) {
|
|
125
223
|
if (!validTabs.includes(tab)) return;
|
|
126
224
|
this.activeTab = tab;
|
|
225
|
+
if (tab === 'metrics') {
|
|
226
|
+
this.loadMetrics();
|
|
227
|
+
}
|
|
127
228
|
const nextUrl = new URL(window.location.href);
|
|
128
229
|
if (tab === 'dashboard') {
|
|
129
230
|
nextUrl.searchParams.delete('tab');
|
|
@@ -133,9 +234,6 @@ document.addEventListener('alpine:init', () => {
|
|
|
133
234
|
nextUrl.hash = '';
|
|
134
235
|
}
|
|
135
236
|
window.history.replaceState({}, '', nextUrl);
|
|
136
|
-
if (window.innerWidth < 1024) {
|
|
137
|
-
this.sidebarOpen = false;
|
|
138
|
-
}
|
|
139
237
|
},
|
|
140
238
|
|
|
141
239
|
async api(endpoint, options = {}) {
|
|
@@ -174,6 +272,105 @@ document.addEventListener('alpine:init', () => {
|
|
|
174
272
|
this.loading = false;
|
|
175
273
|
},
|
|
176
274
|
|
|
275
|
+
refreshCurrentView() {
|
|
276
|
+
if (this.activeTab === 'metrics') {
|
|
277
|
+
return this.loadMetrics();
|
|
278
|
+
}
|
|
279
|
+
return this.refreshAccounts();
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async loadMetrics() {
|
|
283
|
+
this.metricsLoading = true;
|
|
284
|
+
this.metricsError = '';
|
|
285
|
+
const params = new URLSearchParams({ range: this.metricsRange });
|
|
286
|
+
if (this.metricsStatusFilter) {
|
|
287
|
+
params.set('status', this.metricsStatusFilter);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const [summary, recent, storage] = await Promise.all([
|
|
291
|
+
this.api(`/api/metrics/summary?${params.toString()}`),
|
|
292
|
+
this.api(`/api/metrics/recent?${params.toString()}&limit=50`),
|
|
293
|
+
this.api('/api/metrics/storage')
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
if (summary.ok && summary.data?.summary) {
|
|
297
|
+
this.metricsSummary = summary.data.summary;
|
|
298
|
+
} else {
|
|
299
|
+
this.metricsError = summary.data?.error || summary.error || 'Failed to load metrics';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (recent.ok && Array.isArray(recent.data?.events)) {
|
|
303
|
+
this.metricsRecent = recent.data.events;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (storage.ok && storage.data?.storage) {
|
|
307
|
+
this.metricsStorage = storage.data.storage;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
this.metricsLoading = false;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
setMetricsRange(range) {
|
|
314
|
+
if (this.metricsRange === range) return;
|
|
315
|
+
this.metricsRange = range;
|
|
316
|
+
this.loadMetrics();
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
setMetricsStatusFilter(status) {
|
|
320
|
+
if (this.metricsStatusFilter === status) return;
|
|
321
|
+
this.metricsStatusFilter = status;
|
|
322
|
+
this.loadMetrics();
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
metricBarWidth(value, maxValue) {
|
|
326
|
+
const valueNumber = Number(value) || 0;
|
|
327
|
+
const maxNumber = Number(maxValue) || 1;
|
|
328
|
+
return Math.max(2, Math.min(100, Math.round((valueNumber / maxNumber) * 100)));
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
formatTokenCount(value) {
|
|
332
|
+
const number = Number(value) || 0;
|
|
333
|
+
if (number >= 1000000) return `${(number / 1000000).toFixed(1)}M`;
|
|
334
|
+
if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
|
|
335
|
+
return String(number);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
formatDuration(value) {
|
|
339
|
+
const number = Number(value) || 0;
|
|
340
|
+
if (number >= 1000) return `${(number / 1000).toFixed(1)}s`;
|
|
341
|
+
return `${number}ms`;
|
|
342
|
+
},
|
|
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
|
+
|
|
353
|
+
formatBytes(value) {
|
|
354
|
+
const number = Number(value) || 0;
|
|
355
|
+
if (number >= 1024 * 1024) return `${(number / (1024 * 1024)).toFixed(1)} MB`;
|
|
356
|
+
if (number >= 1024) return `${(number / 1024).toFixed(1)} KB`;
|
|
357
|
+
return `${number} B`;
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
formatMetricTime(value) {
|
|
361
|
+
if (!value) return '-';
|
|
362
|
+
const date = new Date(value);
|
|
363
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
364
|
+
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
metricsStatusClass(status) {
|
|
368
|
+
const code = Number(status) || 0;
|
|
369
|
+
if (code >= 200 && code < 400) return 'metrics-status-success';
|
|
370
|
+
if (code >= 400) return 'metrics-status-error';
|
|
371
|
+
return 'metrics-status-muted';
|
|
372
|
+
},
|
|
373
|
+
|
|
177
374
|
async refreshAllQuotaData() {
|
|
178
375
|
if (!this.accounts.length) return;
|
|
179
376
|
const { ok, data } = await this.api('/accounts/quota/all');
|
|
@@ -430,20 +627,34 @@ document.addEventListener('alpine:init', () => {
|
|
|
430
627
|
|
|
431
628
|
async testChat() {
|
|
432
629
|
if (!this.testPrompt.trim()) return;
|
|
630
|
+
const startedAt = Date.now();
|
|
433
631
|
this.testing = true;
|
|
434
632
|
this.testResponse = '';
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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;
|
|
447
658
|
}
|
|
448
659
|
},
|
|
449
660
|
|
|
@@ -493,47 +704,131 @@ document.addEventListener('alpine:init', () => {
|
|
|
493
704
|
}
|
|
494
705
|
},
|
|
495
706
|
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
707
|
+
modelOptionName(modelId) {
|
|
708
|
+
const option = this.openAiModelOptions.find((model) => model.id === modelId);
|
|
709
|
+
return option ? option.name : modelId;
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
modelMappingLabel(alias) {
|
|
713
|
+
const labels = { opus: 'Opus', sonnet: 'Sonnet', haiku: 'Haiku' };
|
|
714
|
+
return labels[alias] || alias;
|
|
502
715
|
},
|
|
503
716
|
|
|
504
|
-
async
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
717
|
+
async loadModelMappingsSetting() {
|
|
718
|
+
const { ok, data } = await this.api('/settings/model-mappings');
|
|
719
|
+
if (!ok || !data?.modelMappings) return;
|
|
720
|
+
|
|
721
|
+
this.modelMappings = data.modelMappings;
|
|
722
|
+
this.modelMappingDefaults = data.defaults || this.modelMappingDefaults;
|
|
723
|
+
this.reasoningMappings = data.reasoningMappings || this.reasoningMappings;
|
|
724
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
725
|
+
this.openAiModelOptions = Array.isArray(data.models) ? data.models : [];
|
|
726
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : [];
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
async setModelMapping(alias, model) {
|
|
730
|
+
if (this.modelMappingSaving || !alias || !model) return;
|
|
731
|
+
|
|
732
|
+
const previous = this.modelMappings[alias];
|
|
733
|
+
if (previous === model) return;
|
|
734
|
+
|
|
735
|
+
this.modelMappings = { ...this.modelMappings, [alias]: model };
|
|
736
|
+
this.modelMappingSaving = alias;
|
|
737
|
+
const { ok, data } = await this.api('/settings/model-mappings', {
|
|
508
738
|
method: 'POST',
|
|
509
|
-
body: JSON.stringify({
|
|
739
|
+
body: JSON.stringify({ modelMappings: { [alias]: model } })
|
|
510
740
|
});
|
|
511
|
-
this.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
this.
|
|
515
|
-
this.
|
|
741
|
+
this.modelMappingSaving = null;
|
|
742
|
+
|
|
743
|
+
if (ok && data?.modelMappings) {
|
|
744
|
+
this.modelMappings = data.modelMappings;
|
|
745
|
+
this.modelMappingDefaults = data.defaults || this.modelMappingDefaults;
|
|
746
|
+
this.reasoningMappings = data.reasoningMappings || this.reasoningMappings;
|
|
747
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
748
|
+
this.openAiModelOptions = Array.isArray(data.models) ? data.models : this.openAiModelOptions;
|
|
749
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : this.reasoningLevelOptions;
|
|
750
|
+
this.showToast(`${this.modelMappingLabel(alias)} now maps to ${this.modelOptionName(data.modelMappings[alias])}`, 'success');
|
|
516
751
|
} else {
|
|
517
|
-
this.
|
|
752
|
+
this.modelMappings = { ...this.modelMappings, [alias]: previous };
|
|
753
|
+
this.showToast(data?.error || 'Failed to update model mapping', 'error');
|
|
518
754
|
}
|
|
519
755
|
},
|
|
520
756
|
|
|
521
|
-
|
|
522
|
-
const
|
|
757
|
+
reasoningOptionName(reasoningId) {
|
|
758
|
+
const option = this.reasoningLevelOptions.find((level) => level.id === reasoningId);
|
|
759
|
+
return option ? option.name : reasoningId;
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
async setReasoningMapping(alias, reasoning) {
|
|
763
|
+
if (this.reasoningMappingSaving || !alias || !reasoning) return;
|
|
764
|
+
|
|
765
|
+
const previous = this.reasoningMappings[alias];
|
|
766
|
+
if (previous === reasoning) return;
|
|
767
|
+
|
|
768
|
+
this.reasoningMappings = { ...this.reasoningMappings, [alias]: reasoning };
|
|
769
|
+
this.reasoningMappingSaving = alias;
|
|
770
|
+
const { ok, data } = await this.api('/settings/model-mappings', {
|
|
523
771
|
method: 'POST',
|
|
524
|
-
body: JSON.stringify({
|
|
525
|
-
apiUrl: 'http://localhost:8081',
|
|
526
|
-
apiKey: 'test'
|
|
527
|
-
})
|
|
772
|
+
body: JSON.stringify({ reasoningMappings: { [alias]: reasoning } })
|
|
528
773
|
});
|
|
774
|
+
this.reasoningMappingSaving = null;
|
|
775
|
+
|
|
776
|
+
if (ok && data?.reasoningMappings) {
|
|
777
|
+
this.reasoningMappings = data.reasoningMappings;
|
|
778
|
+
this.reasoningMappingDefaults = data.reasoningDefaults || this.reasoningMappingDefaults;
|
|
779
|
+
this.reasoningLevelOptions = Array.isArray(data.reasoningLevels) ? data.reasoningLevels : this.reasoningLevelOptions;
|
|
780
|
+
this.showToast(`${this.modelMappingLabel(alias)} reasoning set to ${this.reasoningOptionName(data.reasoningMappings[alias])}`, 'success');
|
|
781
|
+
} else {
|
|
782
|
+
this.reasoningMappings = { ...this.reasoningMappings, [alias]: previous };
|
|
783
|
+
this.showToast(data?.error || 'Failed to update reasoning level', 'error');
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
|
|
787
|
+
async loadClaudeProxySetting() {
|
|
788
|
+
const { ok, data } = await this.api('/settings/claude-proxy');
|
|
789
|
+
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
790
|
+
this.configureClaudeOnStartup = data.configureClaudeOnStartup;
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
async configureClaudeProxy() {
|
|
795
|
+
if (this.claudeProxyConfiguring) return;
|
|
796
|
+
this.claudeProxyConfiguring = true;
|
|
797
|
+
const { ok, data, error } = await this.api('/claude/config/proxy', { method: 'POST' });
|
|
798
|
+
this.claudeProxyConfiguring = false;
|
|
529
799
|
|
|
530
800
|
if (ok && data?.success) {
|
|
531
|
-
this.showToast('
|
|
801
|
+
this.showToast(data.message || 'Claude Code configured to use this proxy.', 'success');
|
|
532
802
|
} else {
|
|
533
803
|
this.showToast(data?.error || error || 'Failed to update Claude Code settings.json', 'error');
|
|
534
804
|
}
|
|
535
805
|
},
|
|
536
806
|
|
|
807
|
+
async setConfigureClaudeOnStartup(enabled) {
|
|
808
|
+
if (this.claudeProxyStartupSaving) return;
|
|
809
|
+
const previous = this.configureClaudeOnStartup;
|
|
810
|
+
this.configureClaudeOnStartup = enabled;
|
|
811
|
+
this.claudeProxyStartupSaving = true;
|
|
812
|
+
const { ok, data, error } = await this.api('/settings/claude-proxy', {
|
|
813
|
+
method: 'POST',
|
|
814
|
+
body: JSON.stringify({ configureClaudeOnStartup: enabled })
|
|
815
|
+
});
|
|
816
|
+
this.claudeProxyStartupSaving = false;
|
|
817
|
+
|
|
818
|
+
if (ok && typeof data?.configureClaudeOnStartup === 'boolean') {
|
|
819
|
+
this.configureClaudeOnStartup = data.configureClaudeOnStartup;
|
|
820
|
+
this.showToast(
|
|
821
|
+
data.configureClaudeOnStartup
|
|
822
|
+
? 'Claude Code will be configured on proxy startup.'
|
|
823
|
+
: 'Startup Claude Code configuration disabled.',
|
|
824
|
+
'success'
|
|
825
|
+
);
|
|
826
|
+
} else {
|
|
827
|
+
this.configureClaudeOnStartup = previous;
|
|
828
|
+
this.showToast(data?.error || error || 'Failed to update startup setting', 'error');
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
|
|
537
832
|
showToast(message, type = 'success') {
|
|
538
833
|
this.toast = { message, type };
|
|
539
834
|
setTimeout(() => { this.toast = null; }, 3000);
|
|
@@ -541,11 +836,16 @@ document.addEventListener('alpine:init', () => {
|
|
|
541
836
|
|
|
542
837
|
startLogStream() {
|
|
543
838
|
if (this.logEventSource) this.logEventSource.close();
|
|
544
|
-
|
|
839
|
+
|
|
840
|
+
this.logStreamStatus = 'connecting';
|
|
545
841
|
this.logEventSource = new EventSource('/api/logs/stream?history=true');
|
|
842
|
+
this.logEventSource.onopen = () => {
|
|
843
|
+
this.logStreamStatus = 'connected';
|
|
844
|
+
};
|
|
546
845
|
this.logEventSource.onmessage = (event) => {
|
|
547
846
|
try {
|
|
548
847
|
const log = JSON.parse(event.data);
|
|
848
|
+
this.logStreamStatus = 'connected';
|
|
549
849
|
this.logs.unshift(log);
|
|
550
850
|
|
|
551
851
|
if (this.logs.length > 500) {
|
|
@@ -555,6 +855,8 @@ document.addEventListener('alpine:init', () => {
|
|
|
555
855
|
};
|
|
556
856
|
|
|
557
857
|
this.logEventSource.onerror = () => {
|
|
858
|
+
this.logStreamStatus = 'disconnected';
|
|
859
|
+
if (this.logEventSource) this.logEventSource.close();
|
|
558
860
|
setTimeout(() => this.startLogStream(), 3000);
|
|
559
861
|
};
|
|
560
862
|
},
|
|
@@ -572,6 +874,22 @@ document.addEventListener('alpine:init', () => {
|
|
|
572
874
|
return message;
|
|
573
875
|
},
|
|
574
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
|
+
|
|
575
893
|
getLogDetails(message) {
|
|
576
894
|
if (!message) return null;
|
|
577
895
|
const details = {};
|