@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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/main.mjs +341 -2
  3. 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.2.3";
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: 100vh; }
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",