@dianshuv/copilot-api 0.2.3 → 0.3.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 +1 -1
- package/dist/main.mjs +341 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ copilot-api start
|
|
|
97
97
|
| `/usage` | GET | Copilot usage stats |
|
|
98
98
|
| `/token` | GET | Current Copilot token |
|
|
99
99
|
| `/health` | GET | Health check |
|
|
100
|
-
| `/history` | GET | Request history Web UI (enabled by default) |
|
|
100
|
+
| `/history` | GET | Request history Web UI with token analytics (enabled by default) |
|
|
101
101
|
| `/history/api/*` | GET/DELETE | History API endpoints |
|
|
102
102
|
|
|
103
103
|
## Using with Claude Code
|
package/dist/main.mjs
CHANGED
|
@@ -1017,7 +1017,7 @@ const patchClaude = defineCommand({
|
|
|
1017
1017
|
|
|
1018
1018
|
//#endregion
|
|
1019
1019
|
//#region package.json
|
|
1020
|
-
var version = "0.
|
|
1020
|
+
var version = "0.3.0";
|
|
1021
1021
|
|
|
1022
1022
|
//#endregion
|
|
1023
1023
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -1533,6 +1533,37 @@ function getStats() {
|
|
|
1533
1533
|
activeSessions
|
|
1534
1534
|
};
|
|
1535
1535
|
}
|
|
1536
|
+
function getTokenStats() {
|
|
1537
|
+
const models = {};
|
|
1538
|
+
const timeline = [];
|
|
1539
|
+
for (const entry of historyState.entries) {
|
|
1540
|
+
if (!entry.response) continue;
|
|
1541
|
+
const model = entry.response.model || entry.request.model;
|
|
1542
|
+
const inputTokens = entry.response.usage.input_tokens;
|
|
1543
|
+
const outputTokens = entry.response.usage.output_tokens;
|
|
1544
|
+
const existing = models[model];
|
|
1545
|
+
if (existing) {
|
|
1546
|
+
existing.inputTokens += inputTokens;
|
|
1547
|
+
existing.outputTokens += outputTokens;
|
|
1548
|
+
existing.requestCount++;
|
|
1549
|
+
} else models[model] = {
|
|
1550
|
+
inputTokens,
|
|
1551
|
+
outputTokens,
|
|
1552
|
+
requestCount: 1
|
|
1553
|
+
};
|
|
1554
|
+
timeline.push({
|
|
1555
|
+
timestamp: entry.timestamp,
|
|
1556
|
+
model,
|
|
1557
|
+
inputTokens,
|
|
1558
|
+
outputTokens
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
timeline.sort((a, b) => a.timestamp - b.timestamp);
|
|
1562
|
+
return {
|
|
1563
|
+
models,
|
|
1564
|
+
timeline
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1536
1567
|
function exportHistory(format = "json") {
|
|
1537
1568
|
if (format === "json") return JSON.stringify({
|
|
1538
1569
|
sessions: Array.from(historyState.sessions.values()),
|
|
@@ -3360,6 +3391,11 @@ function handleDeleteSession(c) {
|
|
|
3360
3391
|
message: "Session deleted"
|
|
3361
3392
|
});
|
|
3362
3393
|
}
|
|
3394
|
+
function handleGetTokenStats(c) {
|
|
3395
|
+
if (!isHistoryEnabled()) return c.json({ error: "History recording is not enabled" }, 400);
|
|
3396
|
+
const stats = getTokenStats();
|
|
3397
|
+
return c.json(stats);
|
|
3398
|
+
}
|
|
3363
3399
|
|
|
3364
3400
|
//#endregion
|
|
3365
3401
|
//#region src/routes/history/ui/script.ts
|
|
@@ -3908,6 +3944,203 @@ setInterval(() => {
|
|
|
3908
3944
|
loadStats();
|
|
3909
3945
|
loadSessions();
|
|
3910
3946
|
}, 10000);
|
|
3947
|
+
|
|
3948
|
+
// Tab switching
|
|
3949
|
+
function switchTab(tab) {
|
|
3950
|
+
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
|
3951
|
+
document.querySelector('.tab-item[data-tab="' + tab + '"]').classList.add('active');
|
|
3952
|
+
|
|
3953
|
+
document.querySelectorAll('.tab-panel').forEach(p => p.style.display = 'none');
|
|
3954
|
+
const panel = document.getElementById('tab-' + tab);
|
|
3955
|
+
panel.style.display = tab === 'requests' ? 'flex' : 'block';
|
|
3956
|
+
|
|
3957
|
+
if (tab === 'tokens') {
|
|
3958
|
+
panel.setAttribute('data-loaded', 'true');
|
|
3959
|
+
loadTokenStats();
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
async function loadTokenStats() {
|
|
3964
|
+
const container = document.getElementById('tokens-table-container');
|
|
3965
|
+
container.innerHTML = '<div class="loading">Loading...</div>';
|
|
3966
|
+
|
|
3967
|
+
try {
|
|
3968
|
+
const res = await fetch('/history/api/token-stats');
|
|
3969
|
+
const data = await res.json();
|
|
3970
|
+
if (data.error) {
|
|
3971
|
+
container.innerHTML = '<div class="empty-state"><h3>History Not Enabled</h3><p>Start server with --history</p></div>';
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
const modelNames = Object.keys(data.models);
|
|
3976
|
+
if (modelNames.length === 0) {
|
|
3977
|
+
container.innerHTML = '<div class="empty-state"><h3>No token data</h3><p>Make some API requests first</p></div>';
|
|
3978
|
+
document.getElementById('chart-fallback').style.display = 'block';
|
|
3979
|
+
document.getElementById('chart-fallback').textContent = 'No data available for chart.';
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
// Sort models by total tokens descending
|
|
3984
|
+
modelNames.sort((a, b) => {
|
|
3985
|
+
const totalA = data.models[a].inputTokens + data.models[a].outputTokens;
|
|
3986
|
+
const totalB = data.models[b].inputTokens + data.models[b].outputTokens;
|
|
3987
|
+
return totalB - totalA;
|
|
3988
|
+
});
|
|
3989
|
+
|
|
3990
|
+
// Reset chart fallback state
|
|
3991
|
+
document.getElementById('chart-fallback').style.display = 'none';
|
|
3992
|
+
document.getElementById('token-chart').style.display = '';
|
|
3993
|
+
|
|
3994
|
+
// Render table
|
|
3995
|
+
let totalInput = 0, totalOutput = 0, totalReqs = 0;
|
|
3996
|
+
let rows = '';
|
|
3997
|
+
for (const model of modelNames) {
|
|
3998
|
+
const m = data.models[model];
|
|
3999
|
+
const total = m.inputTokens + m.outputTokens;
|
|
4000
|
+
totalInput += m.inputTokens;
|
|
4001
|
+
totalOutput += m.outputTokens;
|
|
4002
|
+
totalReqs += m.requestCount;
|
|
4003
|
+
rows += '<tr>'
|
|
4004
|
+
+ '<td>' + escapeHtml(model) + '</td>'
|
|
4005
|
+
+ '<td class="number">' + formatNumber(m.inputTokens) + '</td>'
|
|
4006
|
+
+ '<td class="number">' + formatNumber(m.outputTokens) + '</td>'
|
|
4007
|
+
+ '<td class="number">' + formatNumber(total) + '</td>'
|
|
4008
|
+
+ '<td class="number">' + m.requestCount + '</td>'
|
|
4009
|
+
+ '</tr>';
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
container.innerHTML = '<table class="tokens-table">'
|
|
4013
|
+
+ '<thead><tr><th>Model</th><th class="number">Input Tokens</th><th class="number">Output Tokens</th><th class="number">Total Tokens</th><th class="number">Requests</th></tr></thead>'
|
|
4014
|
+
+ '<tbody>' + rows + '</tbody>'
|
|
4015
|
+
+ '<tfoot><tr><td>Total</td>'
|
|
4016
|
+
+ '<td class="number">' + formatNumber(totalInput) + '</td>'
|
|
4017
|
+
+ '<td class="number">' + formatNumber(totalOutput) + '</td>'
|
|
4018
|
+
+ '<td class="number">' + formatNumber(totalInput + totalOutput) + '</td>'
|
|
4019
|
+
+ '<td class="number">' + totalReqs + '</td>'
|
|
4020
|
+
+ '</tr></tfoot></table>';
|
|
4021
|
+
|
|
4022
|
+
// Render chart
|
|
4023
|
+
renderTokenChart(data.timeline, modelNames);
|
|
4024
|
+
} catch (e) {
|
|
4025
|
+
container.innerHTML = '<div class="empty-state">Error: ' + e.message + '</div>';
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
let tokenChart = null;
|
|
4030
|
+
let tokenChartListenersAdded = false;
|
|
4031
|
+
|
|
4032
|
+
function renderTokenChart(timeline, modelNames) {
|
|
4033
|
+
if (typeof echarts === 'undefined') {
|
|
4034
|
+
document.getElementById('chart-fallback').style.display = 'block';
|
|
4035
|
+
document.getElementById('token-chart').style.display = 'none';
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
const chartDom = document.getElementById('token-chart');
|
|
4040
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
4041
|
+
|
|
4042
|
+
if (tokenChart) {
|
|
4043
|
+
tokenChart.dispose();
|
|
4044
|
+
}
|
|
4045
|
+
tokenChart = echarts.init(chartDom, isDark ? 'dark' : null);
|
|
4046
|
+
|
|
4047
|
+
// Group timeline by model and compute cumulative totals
|
|
4048
|
+
const seriesData = {};
|
|
4049
|
+
for (const name of modelNames) {
|
|
4050
|
+
seriesData[name] = [];
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
// Build cumulative data per model (timeline is already sorted by backend)
|
|
4054
|
+
const cumulative = {};
|
|
4055
|
+
for (const name of modelNames) {
|
|
4056
|
+
cumulative[name] = 0;
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
for (const point of timeline) {
|
|
4060
|
+
const total = point.inputTokens + point.outputTokens;
|
|
4061
|
+
cumulative[point.model] += total;
|
|
4062
|
+
seriesData[point.model].push([point.timestamp, cumulative[point.model]]);
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
const colors = ['#58a6ff', '#3fb950', '#f85149', '#d29922', '#a371f7', '#39c5cf', '#f778ba', '#79c0ff', '#7ee787', '#ffa657'];
|
|
4066
|
+
|
|
4067
|
+
const series = modelNames.map((name, i) => ({
|
|
4068
|
+
name: name,
|
|
4069
|
+
type: 'line',
|
|
4070
|
+
data: seriesData[name],
|
|
4071
|
+
smooth: true,
|
|
4072
|
+
symbol: 'circle',
|
|
4073
|
+
symbolSize: 4,
|
|
4074
|
+
lineStyle: { width: 2 },
|
|
4075
|
+
itemStyle: { color: colors[i % colors.length] },
|
|
4076
|
+
areaStyle: { opacity: 0.05 },
|
|
4077
|
+
}));
|
|
4078
|
+
|
|
4079
|
+
const style = getComputedStyle(document.documentElement);
|
|
4080
|
+
const textColor = style.getPropertyValue('--text').trim();
|
|
4081
|
+
const borderColor = style.getPropertyValue('--border').trim();
|
|
4082
|
+
const bgColor = style.getPropertyValue('--bg').trim();
|
|
4083
|
+
|
|
4084
|
+
const option = {
|
|
4085
|
+
backgroundColor: 'transparent',
|
|
4086
|
+
tooltip: {
|
|
4087
|
+
trigger: 'item',
|
|
4088
|
+
backgroundColor: bgColor,
|
|
4089
|
+
borderColor: borderColor,
|
|
4090
|
+
textStyle: { color: textColor, fontSize: 12 },
|
|
4091
|
+
formatter: function(params) {
|
|
4092
|
+
const d = new Date(params.data[0]);
|
|
4093
|
+
const time = d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
|
|
4094
|
+
return '<b>' + params.seriesName + '</b><br/>'
|
|
4095
|
+
+ time + '<br/>'
|
|
4096
|
+
+ 'Cumulative: ' + formatNumber(params.data[1]) + ' tokens';
|
|
4097
|
+
}
|
|
4098
|
+
},
|
|
4099
|
+
legend: {
|
|
4100
|
+
data: modelNames,
|
|
4101
|
+
textStyle: { color: textColor, fontSize: 12 },
|
|
4102
|
+
top: 0,
|
|
4103
|
+
},
|
|
4104
|
+
grid: {
|
|
4105
|
+
left: 60,
|
|
4106
|
+
right: 20,
|
|
4107
|
+
top: 40,
|
|
4108
|
+
bottom: 40,
|
|
4109
|
+
},
|
|
4110
|
+
xAxis: {
|
|
4111
|
+
type: 'time',
|
|
4112
|
+
axisLine: { lineStyle: { color: borderColor } },
|
|
4113
|
+
axisLabel: { color: textColor, fontSize: 11 },
|
|
4114
|
+
splitLine: { show: false },
|
|
4115
|
+
},
|
|
4116
|
+
yAxis: {
|
|
4117
|
+
type: 'value',
|
|
4118
|
+
axisLine: { lineStyle: { color: borderColor } },
|
|
4119
|
+
axisLabel: {
|
|
4120
|
+
color: textColor,
|
|
4121
|
+
fontSize: 11,
|
|
4122
|
+
formatter: function(v) { return formatNumber(v); }
|
|
4123
|
+
},
|
|
4124
|
+
splitLine: { lineStyle: { color: borderColor, opacity: 0.3 } },
|
|
4125
|
+
},
|
|
4126
|
+
series: series,
|
|
4127
|
+
};
|
|
4128
|
+
|
|
4129
|
+
tokenChart.setOption(option);
|
|
4130
|
+
|
|
4131
|
+
// Add global listeners only once
|
|
4132
|
+
if (!tokenChartListenersAdded) {
|
|
4133
|
+
tokenChartListenersAdded = true;
|
|
4134
|
+
window.addEventListener('resize', function() {
|
|
4135
|
+
if (tokenChart) tokenChart.resize();
|
|
4136
|
+
});
|
|
4137
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
|
|
4138
|
+
if (document.getElementById('tab-tokens').getAttribute('data-loaded') === 'true') {
|
|
4139
|
+
loadTokenStats();
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
3911
4144
|
`;
|
|
3912
4145
|
|
|
3913
4146
|
//#endregion
|
|
@@ -3948,10 +4181,39 @@ body {
|
|
|
3948
4181
|
color: var(--text);
|
|
3949
4182
|
line-height: 1.4;
|
|
3950
4183
|
font-size: 13px;
|
|
4184
|
+
height: 100vh;
|
|
4185
|
+
display: flex;
|
|
4186
|
+
flex-direction: column;
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
/* Tab bar */
|
|
4190
|
+
.tab-bar {
|
|
4191
|
+
display: flex;
|
|
4192
|
+
gap: 0;
|
|
4193
|
+
border-bottom: 1px solid var(--border);
|
|
4194
|
+
background: var(--bg-secondary);
|
|
4195
|
+
padding: 0 16px;
|
|
4196
|
+
flex-shrink: 0;
|
|
4197
|
+
}
|
|
4198
|
+
.tab-item {
|
|
4199
|
+
padding: 10px 20px;
|
|
4200
|
+
cursor: pointer;
|
|
4201
|
+
font-size: 13px;
|
|
4202
|
+
font-weight: 500;
|
|
4203
|
+
color: var(--text-muted);
|
|
4204
|
+
border-bottom: 2px solid transparent;
|
|
4205
|
+
transition: all 0.15s;
|
|
4206
|
+
user-select: none;
|
|
4207
|
+
}
|
|
4208
|
+
.tab-item:hover { color: var(--text); }
|
|
4209
|
+
.tab-item.active {
|
|
4210
|
+
color: var(--primary);
|
|
4211
|
+
border-bottom-color: var(--primary);
|
|
3951
4212
|
}
|
|
4213
|
+
.tab-panel { flex: 1; overflow: hidden; }
|
|
3952
4214
|
|
|
3953
4215
|
/* Layout */
|
|
3954
|
-
.layout { display: flex; height:
|
|
4216
|
+
.layout { display: flex; height: 100%; }
|
|
3955
4217
|
.sidebar {
|
|
3956
4218
|
width: 280px;
|
|
3957
4219
|
border-right: 1px solid var(--border);
|
|
@@ -4286,11 +4548,67 @@ input::placeholder { color: var(--text-dim); }
|
|
|
4286
4548
|
white-space: pre-wrap;
|
|
4287
4549
|
word-break: break-word;
|
|
4288
4550
|
}
|
|
4551
|
+
|
|
4552
|
+
/* Tokens tab */
|
|
4553
|
+
.tokens-container {
|
|
4554
|
+
height: 100%;
|
|
4555
|
+
display: flex;
|
|
4556
|
+
flex-direction: column;
|
|
4557
|
+
overflow-y: auto;
|
|
4558
|
+
}
|
|
4559
|
+
.tokens-header {
|
|
4560
|
+
padding: 12px 16px;
|
|
4561
|
+
border-bottom: 1px solid var(--border);
|
|
4562
|
+
background: var(--bg-secondary);
|
|
4563
|
+
}
|
|
4564
|
+
.tokens-header h1 { font-size: 16px; font-weight: 600; }
|
|
4565
|
+
.tokens-table {
|
|
4566
|
+
width: 100%;
|
|
4567
|
+
border-collapse: collapse;
|
|
4568
|
+
font-size: 13px;
|
|
4569
|
+
}
|
|
4570
|
+
.tokens-table th {
|
|
4571
|
+
text-align: left;
|
|
4572
|
+
padding: 10px 16px;
|
|
4573
|
+
border-bottom: 2px solid var(--border);
|
|
4574
|
+
color: var(--text-muted);
|
|
4575
|
+
font-size: 11px;
|
|
4576
|
+
text-transform: uppercase;
|
|
4577
|
+
letter-spacing: 0.5px;
|
|
4578
|
+
font-weight: 600;
|
|
4579
|
+
}
|
|
4580
|
+
.tokens-table td {
|
|
4581
|
+
padding: 10px 16px;
|
|
4582
|
+
border-bottom: 1px solid var(--border);
|
|
4583
|
+
}
|
|
4584
|
+
.tokens-table tr:hover td { background: var(--bg-secondary); }
|
|
4585
|
+
.tokens-table .number { text-align: right; font-family: 'SF Mono', Monaco, 'Courier New', monospace; }
|
|
4586
|
+
.tokens-table tfoot td {
|
|
4587
|
+
font-weight: 600;
|
|
4588
|
+
border-top: 2px solid var(--border);
|
|
4589
|
+
}
|
|
4590
|
+
.chart-section { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 16px; }
|
|
4591
|
+
.chart-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; }
|
|
4592
|
+
.chart-container { flex: 1; min-height: 400px; }
|
|
4593
|
+
.chart-fallback {
|
|
4594
|
+
padding: 40px 20px;
|
|
4595
|
+
text-align: center;
|
|
4596
|
+
color: var(--text-muted);
|
|
4597
|
+
background: var(--bg-secondary);
|
|
4598
|
+
border-radius: 8px;
|
|
4599
|
+
border: 1px solid var(--border);
|
|
4600
|
+
}
|
|
4289
4601
|
`;
|
|
4290
4602
|
|
|
4291
4603
|
//#endregion
|
|
4292
4604
|
//#region src/routes/history/ui/template.ts
|
|
4293
4605
|
const template = `
|
|
4606
|
+
<div class="tab-bar">
|
|
4607
|
+
<div class="tab-item active" onclick="switchTab('requests')" data-tab="requests">Requests</div>
|
|
4608
|
+
<div class="tab-item" onclick="switchTab('tokens')" data-tab="tokens">Tokens</div>
|
|
4609
|
+
</div>
|
|
4610
|
+
|
|
4611
|
+
<div id="tab-requests" class="tab-panel">
|
|
4294
4612
|
<div class="layout">
|
|
4295
4613
|
<!-- Sidebar: Sessions -->
|
|
4296
4614
|
<div class="sidebar">
|
|
@@ -4354,6 +4672,25 @@ const template = `
|
|
|
4354
4672
|
</div>
|
|
4355
4673
|
</div>
|
|
4356
4674
|
</div>
|
|
4675
|
+
</div>
|
|
4676
|
+
|
|
4677
|
+
<div id="tab-tokens" class="tab-panel" style="display:none" data-loaded="false">
|
|
4678
|
+
<div class="tokens-container">
|
|
4679
|
+
<div class="tokens-header">
|
|
4680
|
+
<h1>Token Analytics</h1>
|
|
4681
|
+
</div>
|
|
4682
|
+
<div id="tokens-table-container">
|
|
4683
|
+
<div class="loading">Loading...</div>
|
|
4684
|
+
</div>
|
|
4685
|
+
<div class="chart-section">
|
|
4686
|
+
<h2 class="chart-title">Cumulative Token Usage</h2>
|
|
4687
|
+
<div class="chart-container" id="token-chart"></div>
|
|
4688
|
+
<div class="chart-fallback" id="chart-fallback" style="display:none">
|
|
4689
|
+
ECharts library failed to load. Token chart is unavailable.
|
|
4690
|
+
</div>
|
|
4691
|
+
</div>
|
|
4692
|
+
</div>
|
|
4693
|
+
</div>
|
|
4357
4694
|
|
|
4358
4695
|
<!-- Raw JSON Modal -->
|
|
4359
4696
|
<div class="modal-overlay" id="raw-modal" onclick="closeRawModal(event)">
|
|
@@ -4382,6 +4719,7 @@ function getHistoryUI() {
|
|
|
4382
4719
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
4383
4720
|
<title>Copilot API - Request History</title>
|
|
4384
4721
|
<link rel="icon" href="data:,">
|
|
4722
|
+
<script defer src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"><\/script>
|
|
4385
4723
|
<style>${styles}</style>
|
|
4386
4724
|
</head>
|
|
4387
4725
|
<body>
|
|
@@ -4398,6 +4736,7 @@ historyRoutes.get("/api/entries", handleGetEntries);
|
|
|
4398
4736
|
historyRoutes.get("/api/entries/:id", handleGetEntry);
|
|
4399
4737
|
historyRoutes.delete("/api/entries", handleDeleteEntries);
|
|
4400
4738
|
historyRoutes.get("/api/stats", handleGetStats);
|
|
4739
|
+
historyRoutes.get("/api/token-stats", handleGetTokenStats);
|
|
4401
4740
|
historyRoutes.get("/api/export", handleExport);
|
|
4402
4741
|
historyRoutes.get("/api/sessions", handleGetSessions);
|
|
4403
4742
|
historyRoutes.get("/api/sessions/:id", handleGetSession);
|