@jeffreycao/copilot-api 1.10.0 → 1.10.2

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/pages/index.html CHANGED
@@ -205,8 +205,8 @@
205
205
  "
206
206
  >
207
207
  <option value="day">Day</option>
208
- <option value="week">Week</option>
209
- <option value="month">Month</option>
208
+ <option value="week">Last 7 days</option>
209
+ <option value="month">Last 30 days</option>
210
210
  </select>
211
211
  </div>
212
212
  <button
@@ -273,6 +273,8 @@
273
273
  const DEFAULT_ENDPOINT = "http://localhost:4141/usage";
274
274
  const DEFAULT_TOKEN_USAGE_PERIOD = "day";
275
275
  const DEFAULT_TOKEN_USAGE_EVENTS_PAGE_SIZE = 20;
276
+ const ALL_METRICS_VALUE = "__all__";
277
+ const ALL_MODELS_VALUE = "__all__";
276
278
  const API_KEY_STORAGE_KEY = "copilot-api.usage-viewer.x-api-key";
277
279
  const VALID_PERIODS = new Set(["day", "week", "month"]);
278
280
  const EMPTY_TOKEN_USAGE_TOTALS = {
@@ -291,11 +293,16 @@
291
293
  isEventsLoading: false,
292
294
  error: null,
293
295
  data: null,
296
+ tokenUsageDailySummary: null,
294
297
  tokenUsageSummary: null,
295
298
  tokenUsageEventsPage: null,
299
+ tokenUsageDailyError: null,
296
300
  tokenUsageSummaryError: null,
297
301
  tokenUsageEventsError: null,
298
302
  tokenUsagePeriod: DEFAULT_TOKEN_USAGE_PERIOD,
303
+ tokenUsageTrendDayIndex: null,
304
+ tokenUsageTrendMetric: ALL_METRICS_VALUE,
305
+ tokenUsageTrendModel: ALL_MODELS_VALUE,
299
306
  };
300
307
 
301
308
  // --- Rendering Logic ---
@@ -402,6 +409,14 @@
402
409
  return url.toString();
403
410
  }
404
411
 
412
+ function buildTokenUsageDailyUrl(usageEndpoint, period) {
413
+ const url = new URL(
414
+ deriveEndpointUrl(usageEndpoint, "token-usage/daily")
415
+ );
416
+ url.searchParams.set("period", period);
417
+ return url.toString();
418
+ }
419
+
405
420
  function buildTokenUsageEventsUrl(usageEndpoint, period, page) {
406
421
  const url = new URL(
407
422
  deriveEndpointUrl(usageEndpoint, "token-usage/events")
@@ -503,8 +518,8 @@
503
518
  function renderTokenUsageRangeText(period, range) {
504
519
  const labels = {
505
520
  day: "Day window",
506
- week: "Week window",
507
- month: "Month window",
521
+ week: "Last 7 days",
522
+ month: "Last 30 days",
508
523
  };
509
524
 
510
525
  if (!range) {
@@ -533,8 +548,10 @@
533
548
  || state.data
534
549
  || state.isTokenUsageLoading
535
550
  || state.isEventsLoading
551
+ || state.tokenUsageDailySummary
536
552
  || state.tokenUsageSummary
537
553
  || state.tokenUsageEventsPage
554
+ || state.tokenUsageDailyError
538
555
  || state.tokenUsageSummaryError
539
556
  || state.tokenUsageEventsError
540
557
  );
@@ -692,8 +709,10 @@
692
709
  const hasTokenUsageContent = Boolean(
693
710
  state.isTokenUsageLoading
694
711
  || state.isEventsLoading
712
+ || state.tokenUsageDailySummary
695
713
  || state.tokenUsageSummary
696
714
  || state.tokenUsageEventsPage
715
+ || state.tokenUsageDailyError
697
716
  || state.tokenUsageSummaryError
698
717
  || state.tokenUsageEventsError
699
718
  );
@@ -703,9 +722,11 @@
703
722
  }
704
723
 
705
724
  const summary = state.tokenUsageSummary;
725
+ const dailySummary = state.tokenUsageDailySummary;
706
726
  const eventsPage = state.tokenUsageEventsPage;
707
727
  const totals = summary?.totals || EMPTY_TOKEN_USAGE_TOTALS;
708
- const activeRange = summary?.range || eventsPage?.range || null;
728
+ const activeRange =
729
+ summary?.range || dailySummary?.range || eventsPage?.range || null;
709
730
  const totalPages = eventsPage ? Math.max(eventsPage.total_pages, 1) : 1;
710
731
  const eventsMeta = eventsPage
711
732
  ? `Page ${eventsPage.page} / ${totalPages} · ${formatNumber(
@@ -782,6 +803,11 @@
782
803
  )}
783
804
  </div>
784
805
 
806
+ ${renderTokenUsageDailyTrend(
807
+ dailySummary,
808
+ state.tokenUsageDailyError
809
+ )}
810
+
785
811
  <div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
786
812
  <div class="px-4 py-3 border-b flex items-center justify-between gap-3" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
787
813
  <div>
@@ -829,6 +855,321 @@
829
855
  `;
830
856
  }
831
857
 
858
+ function getTokenUsageTrendMetrics() {
859
+ return [
860
+ {
861
+ color: "var(--color-fg-lightest)",
862
+ key: "total_tokens",
863
+ label: "Total",
864
+ },
865
+ {
866
+ color: "var(--color-blue-accent)",
867
+ key: "input_tokens",
868
+ label: "Input",
869
+ },
870
+ {
871
+ color: "var(--color-green-accent)",
872
+ key: "output_tokens",
873
+ label: "Output",
874
+ },
875
+ {
876
+ color: "var(--color-aqua-accent)",
877
+ key: "cache_read_input_tokens",
878
+ label: "Cache Read",
879
+ },
880
+ {
881
+ color: "var(--color-yellow-accent)",
882
+ key: "cache_creation_input_tokens",
883
+ label: "Cache Write",
884
+ },
885
+ ];
886
+ }
887
+
888
+ function getDailyTrendTotals(day, selectedModel) {
889
+ if (selectedModel === ALL_MODELS_VALUE) {
890
+ return day.totals || EMPTY_TOKEN_USAGE_TOTALS;
891
+ }
892
+
893
+ return (
894
+ day.byModel?.find((model) => model.model === selectedModel)
895
+ || EMPTY_TOKEN_USAGE_TOTALS
896
+ );
897
+ }
898
+
899
+ function formatChartDate(value) {
900
+ const parts = String(value).split("-");
901
+ return parts.length === 3 ? `${parts[1]}/${parts[2]}` : value;
902
+ }
903
+
904
+ function renderTokenUsageDailyTrend(dailySummary, error) {
905
+ if (state.tokenUsagePeriod === "day") {
906
+ return "";
907
+ }
908
+
909
+ const models = dailySummary?.byModel || [];
910
+ const selectedModel = models.some(
911
+ (model) => model.model === state.tokenUsageTrendModel
912
+ )
913
+ ? state.tokenUsageTrendModel
914
+ : ALL_MODELS_VALUE;
915
+ const options = [
916
+ `<option value="${ALL_MODELS_VALUE}">All models</option>`,
917
+ ...models.map(
918
+ (model) =>
919
+ `<option value="${escapeHtml(model.model)}" ${
920
+ selectedModel === model.model ? "selected" : ""
921
+ }>${escapeHtml(model.model)}</option>`
922
+ ),
923
+ ].join("");
924
+
925
+ return `
926
+ <div class="mt-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
927
+ <div class="px-4 py-3 border-b flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between" style="background-color: var(--color-bg-soft); border-color: var(--color-bg-light-2);">
928
+ <p class="text-sm font-semibold" style="color: var(--color-fg-lightest);">Daily Trend</p>
929
+ <select
930
+ data-token-usage-trend-model
931
+ class="px-3 py-1.5 border input-focus text-xs"
932
+ style="background-color: var(--color-bg); border-color: var(--color-bg-light-2); color: var(--color-fg-light);"
933
+ >${options}</select>
934
+ </div>
935
+ ${
936
+ error
937
+ ? `<div class="p-4">${renderError(
938
+ error,
939
+ "Token usage daily trend failed"
940
+ )}</div>`
941
+ : ""
942
+ }
943
+ ${renderTokenUsageTrendChart(dailySummary, selectedModel)}
944
+ </div>
945
+ `;
946
+ }
947
+
948
+ function renderTokenUsageTrendChart(dailySummary, selectedModel) {
949
+ const days = dailySummary?.days || [];
950
+ const metrics = getTokenUsageTrendMetrics();
951
+ const visibleMetrics =
952
+ state.tokenUsageTrendMetric === ALL_METRICS_VALUE
953
+ ? metrics
954
+ : metrics.filter(
955
+ (metric) => metric.key === state.tokenUsageTrendMetric
956
+ );
957
+ const hasUsage = days.some(
958
+ (day) => getDailyTrendTotals(day, selectedModel).request_count > 0
959
+ );
960
+ const latestUsageIndex = days.reduce((latest, day, index) => {
961
+ return getDailyTrendTotals(day, selectedModel).request_count > 0
962
+ ? index
963
+ : latest;
964
+ }, -1);
965
+ const activeIndex =
966
+ Number.isInteger(state.tokenUsageTrendDayIndex)
967
+ && state.tokenUsageTrendDayIndex >= 0
968
+ && state.tokenUsageTrendDayIndex < days.length
969
+ ? state.tokenUsageTrendDayIndex
970
+ : latestUsageIndex >= 0
971
+ ? latestUsageIndex
972
+ : Math.max(0, days.length - 1);
973
+ const activeDay = days[activeIndex];
974
+ const activeTotals = activeDay
975
+ ? getDailyTrendTotals(activeDay, selectedModel)
976
+ : EMPTY_TOKEN_USAGE_TOTALS;
977
+
978
+ if (state.isTokenUsageLoading && !dailySummary) {
979
+ return `<div class="p-4 min-h-[14rem] text-sm animate-pulse" style="color: var(--color-gray);">Loading...</div>`;
980
+ }
981
+
982
+ if (!dailySummary || days.length === 0 || !hasUsage) {
983
+ return renderEmptyState(
984
+ "No token usage recorded for the selected period."
985
+ );
986
+ }
987
+
988
+ const width = 720;
989
+ const height = 220;
990
+ const padding = { bottom: 34, left: 58, right: 16, top: 16 };
991
+ const plotWidth = width - padding.left - padding.right;
992
+ const plotHeight = height - padding.top - padding.bottom;
993
+ const allValues = days.flatMap((day) => {
994
+ const totals = getDailyTrendTotals(day, selectedModel);
995
+ return visibleMetrics.map((metric) => totals[metric.key] || 0);
996
+ });
997
+ const maxValue = Math.max(1, ...allValues);
998
+ const xForIndex = (index) =>
999
+ days.length <= 1
1000
+ ? padding.left + plotWidth / 2
1001
+ : padding.left + (plotWidth * index) / (days.length - 1);
1002
+ const hitWidth =
1003
+ days.length <= 1 ? plotWidth : plotWidth / (days.length - 1);
1004
+ const yForValue = (value) =>
1005
+ padding.top
1006
+ + plotHeight
1007
+ - (Math.max(0, value) / maxValue) * plotHeight;
1008
+ const labelEvery = Math.max(1, Math.ceil(days.length / 6));
1009
+ const yTicks = [0, 0.25, 0.5, 0.75, 1].map((ratio) =>
1010
+ Math.round(maxValue * ratio)
1011
+ );
1012
+
1013
+ const grid = yTicks
1014
+ .map((tick) => {
1015
+ const y = yForValue(tick);
1016
+ return `
1017
+ <line x1="${padding.left}" x2="${
1018
+ width - padding.right
1019
+ }" y1="${y}" y2="${y}" stroke="var(--color-bg-light-2)" stroke-width="1" />
1020
+ <text x="${padding.left - 8}" y="${
1021
+ y + 4
1022
+ }" text-anchor="end" font-size="11" fill="var(--color-gray-accent)">${formatNumber(
1023
+ tick
1024
+ )}</text>
1025
+ `;
1026
+ })
1027
+ .join("");
1028
+ const xLabels = days
1029
+ .map((day, index) => {
1030
+ if (index % labelEvery !== 0 && index !== days.length - 1) {
1031
+ return "";
1032
+ }
1033
+ return `<text x="${xForIndex(index)}" y="${
1034
+ height - 10
1035
+ }" text-anchor="middle" font-size="11" fill="var(--color-gray-accent)">${escapeHtml(
1036
+ formatChartDate(day.date)
1037
+ )}</text>`;
1038
+ })
1039
+ .join("");
1040
+ const lines = visibleMetrics
1041
+ .map((metric) => {
1042
+ const points = days
1043
+ .map((day, index) => {
1044
+ const totals = getDailyTrendTotals(day, selectedModel);
1045
+ return `${xForIndex(index)},${yForValue(
1046
+ totals[metric.key] || 0
1047
+ )}`;
1048
+ })
1049
+ .join(" ");
1050
+ const circles = days
1051
+ .map((day, index) => {
1052
+ const totals = getDailyTrendTotals(day, selectedModel);
1053
+ const value = totals[metric.key] || 0;
1054
+ return `<circle cx="${xForIndex(index)}" cy="${yForValue(
1055
+ value
1056
+ )}" r="2.5" fill="${
1057
+ metric.color
1058
+ }" data-trend-day-index="${index}">
1059
+ <title>${escapeHtml(
1060
+ `${formatChartDate(day.date)} ${metric.label}: ${formatNumber(
1061
+ value
1062
+ )}`
1063
+ )}</title>
1064
+ </circle>`;
1065
+ })
1066
+ .join("");
1067
+ return `
1068
+ <polyline fill="none" points="${points}" stroke="${metric.color}" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25">
1069
+ <title>${escapeHtml(metric.label)}</title>
1070
+ </polyline>
1071
+ ${circles}
1072
+ `;
1073
+ })
1074
+ .join("");
1075
+ const activeMarker = activeDay
1076
+ ? `<line x1="${xForIndex(activeIndex)}" x2="${xForIndex(
1077
+ activeIndex
1078
+ )}" y1="${padding.top}" y2="${
1079
+ padding.top + plotHeight
1080
+ }" stroke="var(--color-bg-light-4)" stroke-dasharray="4 4" stroke-width="1" />`
1081
+ : "";
1082
+ const hitRects = days
1083
+ .map((day, index) => {
1084
+ const x = xForIndex(index);
1085
+ const hitStart = Math.max(padding.left, x - hitWidth / 2);
1086
+ const hitEnd = Math.min(
1087
+ width - padding.right,
1088
+ x + hitWidth / 2
1089
+ );
1090
+ const totals = getDailyTrendTotals(day, selectedModel);
1091
+ const title = [
1092
+ formatChartDate(day.date),
1093
+ ...visibleMetrics.map(
1094
+ (metric) =>
1095
+ `${metric.label}: ${formatNumber(
1096
+ totals[metric.key] || 0
1097
+ )}`
1098
+ ),
1099
+ ].join("\n");
1100
+ return `<rect x="${hitStart}" y="${padding.top}" width="${
1101
+ hitEnd - hitStart
1102
+ }" height="${plotHeight}" fill="transparent" style="cursor: pointer;" data-trend-day-index="${index}">
1103
+ <title>${escapeHtml(title)}</title>
1104
+ </rect>`;
1105
+ })
1106
+ .join("");
1107
+ const legend = [
1108
+ `<button type="button" data-trend-metric="${ALL_METRICS_VALUE}" class="inline-flex items-center border px-2 py-1 text-xs ${
1109
+ state.tokenUsageTrendMetric === ALL_METRICS_VALUE
1110
+ ? "font-semibold"
1111
+ : ""
1112
+ }" style="background-color: ${
1113
+ state.tokenUsageTrendMetric === ALL_METRICS_VALUE
1114
+ ? "var(--color-bg-light-1)"
1115
+ : "var(--color-bg)"
1116
+ }; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">All</button>`,
1117
+ ...metrics.map(
1118
+ (metric) => `
1119
+ <button type="button" data-trend-metric="${
1120
+ metric.key
1121
+ }" class="inline-flex items-center gap-1.5 border px-2 py-1 text-xs ${
1122
+ state.tokenUsageTrendMetric === metric.key
1123
+ ? "font-semibold"
1124
+ : ""
1125
+ }" style="background-color: ${
1126
+ state.tokenUsageTrendMetric === metric.key
1127
+ ? "var(--color-bg-light-1)"
1128
+ : "var(--color-bg)"
1129
+ }; border-color: var(--color-bg-light-2); color: var(--color-fg-light);">
1130
+ <span class="h-2 w-2 rounded-full" style="background-color: ${
1131
+ metric.color
1132
+ };"></span>
1133
+ ${escapeHtml(metric.label)}
1134
+ </button>
1135
+ `
1136
+ ),
1137
+ ].join("");
1138
+ const activeValues = activeDay
1139
+ ? `
1140
+ <div class="mt-2 flex flex-wrap gap-3 px-3 py-2 text-xs" style="background-color: var(--color-bg-soft); color: var(--color-fg-dark);">
1141
+ <span class="font-semibold" style="color: var(--color-fg-lightest);">${escapeHtml(
1142
+ formatChartDate(activeDay.date)
1143
+ )}</span>
1144
+ ${visibleMetrics
1145
+ .map(
1146
+ (metric) =>
1147
+ `<span>${escapeHtml(metric.label)}: ${formatNumber(
1148
+ activeTotals[metric.key] || 0
1149
+ )}</span>`
1150
+ )
1151
+ .join("")}
1152
+ </div>
1153
+ `
1154
+ : "";
1155
+
1156
+ return `
1157
+ <div class="p-4 ${state.isTokenUsageLoading ? "opacity-60" : ""}">
1158
+ <svg class="w-full h-56" viewBox="0 0 ${width} ${height}" role="img">
1159
+ ${grid}
1160
+ ${xLabels}
1161
+ ${activeMarker}
1162
+ ${lines}
1163
+ ${hitRects}
1164
+ </svg>
1165
+ <div class="mt-2 flex flex-wrap gap-3">
1166
+ ${legend}
1167
+ </div>
1168
+ ${activeValues}
1169
+ </div>
1170
+ `;
1171
+ }
1172
+
832
1173
  function renderTokenUsageMetric(label, value, accentColor) {
833
1174
  return `
834
1175
  <div class="p-4 border" style="background-color: var(--color-bg); border-color: var(--color-bg-light-2);">
@@ -1059,9 +1400,12 @@
1059
1400
  const endpoint = endpointUrlInput.value.trim();
1060
1401
  if (!endpoint) {
1061
1402
  state.data = null;
1403
+ state.tokenUsageDailySummary = null;
1062
1404
  state.tokenUsageSummary = null;
1063
1405
  state.tokenUsageEventsPage = null;
1064
1406
  state.error = "Endpoint URL cannot be empty.";
1407
+ state.tokenUsageDailyError =
1408
+ "Token usage requires a valid endpoint URL.";
1065
1409
  state.tokenUsageSummaryError =
1066
1410
  "Token usage requires a valid endpoint URL.";
1067
1411
  state.tokenUsageEventsError = null;
@@ -1072,16 +1416,18 @@
1072
1416
  const period = getSelectedPeriod();
1073
1417
  state.isLoading = true;
1074
1418
  state.error = null;
1419
+ state.tokenUsageDailyError = null;
1075
1420
  state.tokenUsageSummaryError = null;
1076
1421
  state.tokenUsageEventsError = null;
1077
1422
  render();
1078
1423
 
1079
1424
  try {
1080
1425
  const usageUrl = resolveUsageUrl(endpoint);
1081
- const [usageResult, summaryResult, eventsResult] =
1426
+ const [usageResult, summaryResult, dailyResult, eventsResult] =
1082
1427
  await Promise.allSettled([
1083
1428
  fetchJson(usageUrl),
1084
1429
  fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
1430
+ fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
1085
1431
  fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
1086
1432
  ]);
1087
1433
 
@@ -1106,6 +1452,20 @@
1106
1452
  );
1107
1453
  }
1108
1454
 
1455
+ if (dailyResult.status === "fulfilled") {
1456
+ state.tokenUsageDailySummary = dailyResult.value;
1457
+ } else if (
1458
+ isMissingTokenUsageEndpointError(dailyResult.reason)
1459
+ ) {
1460
+ state.tokenUsageDailySummary = null;
1461
+ state.tokenUsageDailyError = null;
1462
+ } else {
1463
+ state.tokenUsageDailySummary = null;
1464
+ state.tokenUsageDailyError = getErrorMessage(
1465
+ dailyResult.reason
1466
+ );
1467
+ }
1468
+
1109
1469
  if (eventsResult.status === "fulfilled") {
1110
1470
  state.tokenUsageEventsPage = eventsResult.value;
1111
1471
  } else if (
@@ -1122,9 +1482,11 @@
1122
1482
  } catch (error) {
1123
1483
  console.error("Fetch error:", error);
1124
1484
  state.data = null;
1485
+ state.tokenUsageDailySummary = null;
1125
1486
  state.tokenUsageSummary = null;
1126
1487
  state.tokenUsageEventsPage = null;
1127
1488
  state.error = getErrorMessage(error);
1489
+ state.tokenUsageDailyError = getErrorMessage(error);
1128
1490
  state.tokenUsageSummaryError = getErrorMessage(error);
1129
1491
  state.tokenUsageEventsError = getErrorMessage(error);
1130
1492
  } finally {
@@ -1136,8 +1498,11 @@
1136
1498
  async function fetchTokenUsageSummaryAndEvents(page = 1) {
1137
1499
  const endpoint = endpointUrlInput.value.trim();
1138
1500
  if (!endpoint) {
1501
+ state.tokenUsageDailySummary = null;
1139
1502
  state.tokenUsageSummary = null;
1140
1503
  state.tokenUsageEventsPage = null;
1504
+ state.tokenUsageDailyError =
1505
+ "Token usage requires a valid endpoint URL.";
1141
1506
  state.tokenUsageSummaryError =
1142
1507
  "Token usage requires a valid endpoint URL.";
1143
1508
  state.tokenUsageEventsError = null;
@@ -1147,18 +1512,22 @@
1147
1512
 
1148
1513
  const period = getSelectedPeriod();
1149
1514
  state.isTokenUsageLoading = true;
1515
+ state.tokenUsageDailySummary = null;
1150
1516
  state.tokenUsageSummary = null;
1151
1517
  state.tokenUsageEventsPage = null;
1518
+ state.tokenUsageDailyError = null;
1152
1519
  state.tokenUsageSummaryError = null;
1153
1520
  state.tokenUsageEventsError = null;
1154
1521
  render();
1155
1522
 
1156
1523
  try {
1157
1524
  const usageUrl = resolveUsageUrl(endpoint);
1158
- const [summaryResult, eventsResult] = await Promise.allSettled([
1159
- fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
1160
- fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
1161
- ]);
1525
+ const [summaryResult, dailyResult, eventsResult] =
1526
+ await Promise.allSettled([
1527
+ fetchJson(buildTokenUsageSummaryUrl(usageUrl, period)),
1528
+ fetchJson(buildTokenUsageDailyUrl(usageUrl, period)),
1529
+ fetchJson(buildTokenUsageEventsUrl(usageUrl, period, page)),
1530
+ ]);
1162
1531
 
1163
1532
  if (summaryResult.status === "fulfilled") {
1164
1533
  state.tokenUsageSummary = summaryResult.value;
@@ -1174,6 +1543,20 @@
1174
1543
  );
1175
1544
  }
1176
1545
 
1546
+ if (dailyResult.status === "fulfilled") {
1547
+ state.tokenUsageDailySummary = dailyResult.value;
1548
+ } else if (
1549
+ isMissingTokenUsageEndpointError(dailyResult.reason)
1550
+ ) {
1551
+ state.tokenUsageDailySummary = null;
1552
+ state.tokenUsageDailyError = null;
1553
+ } else {
1554
+ state.tokenUsageDailySummary = null;
1555
+ state.tokenUsageDailyError = getErrorMessage(
1556
+ dailyResult.reason
1557
+ );
1558
+ }
1559
+
1177
1560
  if (eventsResult.status === "fulfilled") {
1178
1561
  state.tokenUsageEventsPage = eventsResult.value;
1179
1562
  } else if (
@@ -1189,8 +1572,10 @@
1189
1572
  }
1190
1573
  } catch (error) {
1191
1574
  console.error("Token usage fetch error:", error);
1575
+ state.tokenUsageDailySummary = null;
1192
1576
  state.tokenUsageSummary = null;
1193
1577
  state.tokenUsageEventsPage = null;
1578
+ state.tokenUsageDailyError = getErrorMessage(error);
1194
1579
  state.tokenUsageSummaryError = getErrorMessage(error);
1195
1580
  state.tokenUsageEventsError = getErrorMessage(error);
1196
1581
  } finally {
@@ -1254,6 +1639,28 @@
1254
1639
  return;
1255
1640
  }
1256
1641
 
1642
+ const trendMetric = event.target.closest("[data-trend-metric]");
1643
+ if (trendMetric) {
1644
+ state.tokenUsageTrendMetric =
1645
+ trendMetric.getAttribute("data-trend-metric")
1646
+ || ALL_METRICS_VALUE;
1647
+ render();
1648
+ return;
1649
+ }
1650
+
1651
+ const trendPoint = event.target.closest("[data-trend-day-index]");
1652
+ if (trendPoint) {
1653
+ const index = Number.parseInt(
1654
+ trendPoint.getAttribute("data-trend-day-index") || "",
1655
+ 10
1656
+ );
1657
+ if (Number.isFinite(index) && index >= 0) {
1658
+ state.tokenUsageTrendDayIndex = index;
1659
+ render();
1660
+ }
1661
+ return;
1662
+ }
1663
+
1257
1664
  const actionButton = event.target.closest("[data-page-action]");
1258
1665
  if (!actionButton || !state.tokenUsageEventsPage || state.isEventsLoading) {
1259
1666
  return;
@@ -1276,6 +1683,20 @@
1276
1683
  }
1277
1684
  }
1278
1685
 
1686
+ function handleContentAreaChange(event) {
1687
+ if (!(event.target instanceof HTMLSelectElement)) {
1688
+ return;
1689
+ }
1690
+
1691
+ if (!event.target.matches("[data-token-usage-trend-model]")) {
1692
+ return;
1693
+ }
1694
+
1695
+ state.tokenUsageTrendModel =
1696
+ event.target.value || ALL_MODELS_VALUE;
1697
+ render();
1698
+ }
1699
+
1279
1700
  /**
1280
1701
  * Initializes the application.
1281
1702
  */
@@ -1286,6 +1707,7 @@
1286
1707
  storeApiKey(apiKeyInput.value);
1287
1708
  });
1288
1709
  contentArea.addEventListener("click", handleContentAreaClick);
1710
+ contentArea.addEventListener("change", handleContentAreaChange);
1289
1711
 
1290
1712
  apiKeyInput.value = loadStoredApiKey();
1291
1713
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"proxy-De0Po8kG.js","names":["get"],"sources":["../src/lib/config.ts","../src/lib/proxy.ts"],"sourcesContent":["import consola from \"consola\"\nimport fs from \"node:fs\"\n\nimport { PATHS } from \"./paths\"\n\nexport interface AppConfig {\n auth?: {\n apiKeys?: Array<string>\n }\n providers?: Record<string, ProviderConfig>\n extraPrompts?: Record<string, string>\n smallModel?: string\n responsesApiContextManagementModels?: Array<string>\n modelReasoningEfforts?: Record<\n string,\n \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"\n >\n useFunctionApplyPatch?: boolean\n useMessagesApi?: boolean\n useResponsesApiWebSocket?: boolean\n anthropicApiKey?: string\n useResponsesApiWebSearch?: boolean\n claudeTokenMultiplier?: number\n}\n\nexport interface ModelConfig {\n temperature?: number\n topP?: number\n topK?: number\n extraBody?: Record<string, unknown>\n contextCache?: boolean\n supportPdf?: boolean\n toolContentSupportType?: Array<ToolContentSupportType>\n}\n\nexport type ProviderAuthType = \"authorization\" | \"x-api-key\"\nexport type ProviderType = \"anthropic\" | \"openai-compatible\"\nexport type ToolContentSupportType = \"array\" | \"image\" | \"pdf\"\n\nexport interface ProviderConfig {\n type?: string\n enabled?: boolean\n baseUrl?: string\n apiKey?: string\n authType?: ProviderAuthType\n models?: Record<string, ModelConfig>\n adjustInputTokens?: boolean\n}\n\nexport interface ResolvedProviderConfig {\n name: string\n type: ProviderType\n baseUrl: string\n apiKey: string\n authType: ProviderAuthType\n models?: Record<string, ModelConfig>\n adjustInputTokens?: boolean\n}\n\nconst gpt5ExplorationPrompt = `## Exploration and reading files\n- **Think first.** Before any tool call, decide ALL files/resources you will need.\n- **Batch everything.** If you need multiple files (even from different places), read them together.\n- **multi_tool_use.parallel** Use multi_tool_use.parallel to parallelize tool calls and only this.\n- **Only make sequential calls if you truly cannot know the next file without seeing a result first.**\n- **Workflow:** (a) plan all needed reads → (b) issue one parallel batch → (c) analyze results → (d) repeat if new, unpredictable reads arise.`\n\nconst gpt5CommentaryPrompt = `# Working with the user\n\nYou interact with the user through a terminal. You have 2 ways of communicating with the users: \n- Share intermediary updates in \\`commentary\\` channel. \n- After you have completed all your work, send a message to the \\`final\\` channel. \n\n## Intermediary updates\n\n- Intermediary updates go to the \\`commentary\\` channel.\n- User updates are short updates while you are working, they are NOT final answers.\n- You use 1-2 sentence user updates to communicate progress and new information to the user as you are doing work.\n- Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements (“Done —”, “Got it”, “Great question, ”) or framing phrases.\n- You provide user updates frequently, every 20s.\n- Before exploring or doing substantial work, you start with a user update acknowledging the request and explaining your first step. You should include your understanding of the user request and explain what you will do. Avoid commenting on the request or using starters such as \"Got it -\" or \"Understood -\" etc.\n- When exploring, e.g. searching, reading files, you provide user updates as you go, every 20s, explaining what context you are gathering and what you've learned. Vary your sentence structure when providing these updates to avoid sounding repetitive - in particular, don't start each sentence the same way.\n- After you have sufficient context, and the work is substantial, you provide a longer plan (this is the only user update that may be longer than 2 sentences and can contain formatting).\n- Before performing file edits of any kind, you provide updates explaining what edits you are making.\n- As you are thinking, you very frequently provide updates even if not taking any actions, informing the user of your progress. You interrupt your thinking and send multiple updates in a row if thinking for more than 100 words.\n- Tone of your updates MUST match your personality.`\n\nconst defaultConfig: AppConfig = {\n auth: {\n apiKeys: [],\n },\n providers: {},\n extraPrompts: {\n \"gpt-5-mini\": gpt5ExplorationPrompt,\n \"gpt-5.3-codex\": gpt5CommentaryPrompt,\n \"gpt-5.4-mini\": gpt5CommentaryPrompt,\n \"gpt-5.4\": gpt5CommentaryPrompt,\n \"gpt-5.5\": gpt5CommentaryPrompt,\n },\n smallModel: \"gpt-5-mini\",\n responsesApiContextManagementModels: [],\n modelReasoningEfforts: {\n \"gpt-5-mini\": \"low\",\n \"gpt-5.3-codex\": \"xhigh\",\n \"gpt-5.4-mini\": \"xhigh\",\n \"gpt-5.4\": \"xhigh\",\n \"gpt-5.5\": \"xhigh\",\n },\n useFunctionApplyPatch: true,\n useMessagesApi: true,\n useResponsesApiWebSocket: true,\n useResponsesApiWebSearch: true,\n}\n\nlet cachedConfig: AppConfig | null = null\n\nfunction ensureConfigFile(): void {\n try {\n fs.accessSync(PATHS.CONFIG_PATH, fs.constants.R_OK | fs.constants.W_OK)\n } catch {\n fs.mkdirSync(PATHS.APP_DIR, { recursive: true })\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n try {\n fs.chmodSync(PATHS.CONFIG_PATH, 0o600)\n } catch {\n return\n }\n }\n}\n\nfunction readConfigFromDisk(): AppConfig {\n ensureConfigFile()\n try {\n const raw = fs.readFileSync(PATHS.CONFIG_PATH, \"utf8\")\n if (!raw.trim()) {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(defaultConfig, null, 2)}\\n`,\n \"utf8\",\n )\n return defaultConfig\n }\n return JSON.parse(raw) as AppConfig\n } catch (error) {\n consola.error(\"Failed to read config file, using default config\", error)\n return defaultConfig\n }\n}\n\nfunction mergeDefaultConfig(config: AppConfig): {\n mergedConfig: AppConfig\n changed: boolean\n} {\n const extraPrompts = config.extraPrompts ?? {}\n const defaultExtraPrompts = defaultConfig.extraPrompts ?? {}\n const modelReasoningEfforts = config.modelReasoningEfforts ?? {}\n const defaultModelReasoningEfforts = defaultConfig.modelReasoningEfforts ?? {}\n\n const missingExtraPromptModels = Object.keys(defaultExtraPrompts).filter(\n (model) => !Object.hasOwn(extraPrompts, model),\n )\n\n const missingReasoningEffortModels = Object.keys(\n defaultModelReasoningEfforts,\n ).filter((model) => !Object.hasOwn(modelReasoningEfforts, model))\n\n const hasExtraPromptChanges = missingExtraPromptModels.length > 0\n const hasReasoningEffortChanges = missingReasoningEffortModels.length > 0\n\n if (!hasExtraPromptChanges && !hasReasoningEffortChanges) {\n return { mergedConfig: config, changed: false }\n }\n\n return {\n mergedConfig: {\n ...config,\n extraPrompts: {\n ...defaultExtraPrompts,\n ...extraPrompts,\n },\n modelReasoningEfforts: {\n ...defaultModelReasoningEfforts,\n ...modelReasoningEfforts,\n },\n },\n changed: true,\n }\n}\n\nexport function mergeConfigWithDefaults(): AppConfig {\n const config = readConfigFromDisk()\n const { mergedConfig, changed } = mergeDefaultConfig(config)\n\n if (changed) {\n try {\n fs.writeFileSync(\n PATHS.CONFIG_PATH,\n `${JSON.stringify(mergedConfig, null, 2)}\\n`,\n \"utf8\",\n )\n } catch (writeError) {\n consola.warn(\n \"Failed to write merged extraPrompts to config file\",\n writeError,\n )\n }\n }\n\n cachedConfig = mergedConfig\n return mergedConfig\n}\n\nexport function getConfig(): AppConfig {\n cachedConfig ??= mergeDefaultConfig(readConfigFromDisk()).mergedConfig\n return cachedConfig\n}\n\nexport function getExtraPromptForModel(model: string): string {\n const config = getConfig()\n return config.extraPrompts?.[model] ?? \"\"\n}\n\nexport function getSmallModel(): string {\n const config = getConfig()\n return config.smallModel ?? \"gpt-5-mini\"\n}\n\nexport function getResponsesApiContextManagementModels(): Array<string> {\n const config = getConfig()\n return (\n config.responsesApiContextManagementModels\n ?? defaultConfig.responsesApiContextManagementModels\n ?? []\n )\n}\n\nexport function isResponsesApiContextManagementModel(model: string): boolean {\n return getResponsesApiContextManagementModels().includes(model)\n}\n\nexport function getReasoningEffortForModel(\n model: string,\n): \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" {\n const config = getConfig()\n return config.modelReasoningEfforts?.[model] ?? \"high\"\n}\n\nexport function normalizeProviderBaseUrl(url: string): string {\n return url.trim().replace(/\\/+$/u, \"\")\n}\n\nfunction getDefaultProviderAuthType(\n providerType: ProviderType,\n): ProviderAuthType {\n return providerType === \"openai-compatible\" ? \"authorization\" : \"x-api-key\"\n}\n\nexport function resolveProviderAuthType(\n providerName: string,\n authType: string | undefined,\n providerType: ProviderType,\n): ProviderAuthType {\n if (authType === undefined) {\n return getDefaultProviderAuthType(providerType)\n }\n\n if (authType === \"x-api-key\") {\n return \"x-api-key\"\n }\n\n if (authType === \"authorization\") {\n return authType\n }\n\n consola.warn(\n `Provider ${providerName} has invalid authType '${authType}', falling back to ${getDefaultProviderAuthType(providerType)}`,\n )\n return getDefaultProviderAuthType(providerType)\n}\n\nexport function getProviderConfig(name: string): ResolvedProviderConfig | null {\n const providerName = name.trim()\n if (!providerName) {\n return null\n }\n\n const config = getConfig()\n const provider = config.providers?.[providerName]\n if (!provider) {\n return null\n }\n\n if (provider.enabled === false) {\n return null\n }\n\n const type = provider.type ?? \"anthropic\"\n if (type !== \"anthropic\" && type !== \"openai-compatible\") {\n consola.warn(\n `Provider ${providerName} is ignored because type '${type}' is not supported`,\n )\n return null\n }\n\n const baseUrl = normalizeProviderBaseUrl(provider.baseUrl ?? \"\")\n const apiKey = (provider.apiKey ?? \"\").trim()\n const authType = resolveProviderAuthType(\n providerName,\n provider.authType,\n type,\n )\n if (!baseUrl || !apiKey) {\n consola.warn(\n `Provider ${providerName} is enabled but missing baseUrl or apiKey`,\n )\n return null\n }\n\n return {\n name: providerName,\n type,\n baseUrl,\n apiKey,\n authType,\n models: provider.models,\n adjustInputTokens: provider.adjustInputTokens,\n }\n}\n\nexport function listEnabledProviders(): Array<string> {\n const config = getConfig()\n const providerNames = Object.keys(config.providers ?? {})\n return providerNames.filter((name) => getProviderConfig(name) !== null)\n}\n\nexport function isMessagesApiEnabled(): boolean {\n const config = getConfig()\n return config.useMessagesApi ?? true\n}\n\nexport function isResponsesApiWebSocketEnabled(): boolean {\n const config = getConfig()\n return config.useResponsesApiWebSocket ?? true\n}\n\nexport function getAnthropicApiKey(): string | undefined {\n const config = getConfig()\n return config.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? undefined\n}\n\nexport function isResponsesApiWebSearchEnabled(): boolean {\n const config = getConfig()\n return config.useResponsesApiWebSearch ?? true\n}\n\nexport function getClaudeTokenMultiplier(): number {\n const config = getConfig()\n return config.claudeTokenMultiplier ?? 1.15\n}\n","import consola from \"consola\"\nimport { getProxyForUrl } from \"proxy-from-env\"\nimport { Agent, ProxyAgent, setGlobalDispatcher, type Dispatcher } from \"undici\"\n\nlet proxyEnvDispatcher: Dispatcher | undefined\n\nexport function getProxyEnvDispatcher(): Dispatcher | undefined {\n return proxyEnvDispatcher\n}\n\nexport function initProxyFromEnv(): void {\n try {\n const direct = new Agent()\n const proxies = new Map<string, ProxyAgent>()\n\n // We only need a minimal dispatcher that implements `dispatch` at runtime.\n // Typing the object as `Dispatcher` forces TypeScript to require many\n // additional methods. Instead, keep a plain object and cast when passing\n // to `setGlobalDispatcher`.\n const dispatcher = {\n dispatch(\n options: Dispatcher.DispatchOptions,\n handler: Dispatcher.DispatchHandler,\n ) {\n try {\n const origin =\n typeof options.origin === \"string\" ?\n new URL(options.origin)\n : (options.origin as URL)\n const get = getProxyForUrl as unknown as (\n u: string,\n ) => string | undefined\n const raw = get(origin.toString())\n const proxyUrl = raw && raw.length > 0 ? raw : undefined\n if (!proxyUrl) {\n consola.debug(`HTTP proxy bypass: ${origin.hostname}`)\n return (direct as unknown as Dispatcher).dispatch(options, handler)\n }\n let agent = proxies.get(proxyUrl)\n if (!agent) {\n agent = new ProxyAgent(proxyUrl)\n proxies.set(proxyUrl, agent)\n }\n let label = proxyUrl\n try {\n const u = new URL(proxyUrl)\n label = `${u.protocol}//${u.host}`\n } catch {\n /* noop */\n }\n consola.debug(`HTTP proxy route: ${origin.hostname} via ${label}`)\n return (agent as unknown as Dispatcher).dispatch(options, handler)\n } catch {\n return (direct as unknown as Dispatcher).dispatch(options, handler)\n }\n },\n close() {\n return direct.close()\n },\n destroy() {\n return direct.destroy()\n },\n }\n\n proxyEnvDispatcher = dispatcher as unknown as Dispatcher\n\n if (typeof Bun !== \"undefined\") {\n consola.debug(\"WebSocket proxy configured from environment (per-URL)\")\n return\n }\n\n setGlobalDispatcher(proxyEnvDispatcher)\n consola.debug(\"HTTP proxy configured from environment (per-URL)\")\n } catch (err) {\n consola.debug(\"Proxy setup skipped:\", err)\n }\n}\n"],"mappings":"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"}