@leo000001/opencode-quota-sidebar 3.0.10 → 4.0.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 (53) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +157 -42
  3. package/README.zh-CN.md +157 -42
  4. package/SECURITY.md +1 -1
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +354 -0
  7. package/dist/cli_render.d.ts +17 -0
  8. package/dist/cli_render.js +292 -0
  9. package/dist/events.d.ts +1 -1
  10. package/dist/events.js +2 -2
  11. package/dist/format.d.ts +4 -0
  12. package/dist/format.js +302 -41
  13. package/dist/history_messages.d.ts +8 -0
  14. package/dist/history_messages.js +157 -0
  15. package/dist/history_usage.d.ts +93 -0
  16. package/dist/history_usage.js +251 -0
  17. package/dist/index.js +29 -4
  18. package/dist/period.d.ts +29 -1
  19. package/dist/period.js +187 -9
  20. package/dist/provider_catalog.d.ts +8 -0
  21. package/dist/provider_catalog.js +68 -0
  22. package/dist/providers/core/anthropic.d.ts +1 -1
  23. package/dist/providers/core/anthropic.js +69 -45
  24. package/dist/providers/core/openai.js +38 -2
  25. package/dist/providers/index.d.ts +1 -2
  26. package/dist/providers/index.js +1 -3
  27. package/dist/quota.d.ts +4 -2
  28. package/dist/quota.js +18 -21
  29. package/dist/quota_render.d.ts +1 -1
  30. package/dist/quota_render.js +23 -24
  31. package/dist/quota_service.d.ts +1 -0
  32. package/dist/quota_service.js +151 -19
  33. package/dist/storage.d.ts +1 -1
  34. package/dist/storage.js +4 -4
  35. package/dist/storage_dates.d.ts +1 -1
  36. package/dist/storage_dates.js +8 -5
  37. package/dist/storage_parse.js +23 -1
  38. package/dist/supported_quota.d.ts +4 -0
  39. package/dist/supported_quota.js +36 -0
  40. package/dist/title.js +18 -8
  41. package/dist/tools.d.ts +14 -3
  42. package/dist/tools.js +54 -2
  43. package/dist/tui.tsx +17 -6
  44. package/dist/tui_helpers.js +11 -6
  45. package/dist/types.d.ts +8 -0
  46. package/dist/usage.d.ts +18 -0
  47. package/dist/usage.js +93 -9
  48. package/dist/usage_service.d.ts +4 -1
  49. package/dist/usage_service.js +193 -189
  50. package/package.json +4 -1
  51. package/quota-sidebar.config.example.json +36 -45
  52. package/dist/providers/third_party/xyai.d.ts +0 -2
  53. package/dist/providers/third_party/xyai.js +0 -348
package/dist/format.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
2
+ import type { HistoryUsageResult } from './usage_service.js';
2
3
  import { type UsageSummary } from './usage.js';
3
4
  export type TitleView = 'multiline' | 'compact';
4
5
  /**
@@ -31,6 +32,9 @@ export declare function renderSidebarQuotaLineGroups(quotas: QuotaSnapshot[], co
31
32
  quota: QuotaSnapshot;
32
33
  lines: string[];
33
34
  }[];
35
+ export declare function renderHistoryMarkdownReport(result: HistoryUsageResult, quotas: QuotaSnapshot[], options?: {
36
+ showCost?: boolean;
37
+ }): string;
34
38
  export declare function renderMarkdownReport(period: string, usage: UsageSummary, quotas: QuotaSnapshot[], options?: {
35
39
  showCost?: boolean;
36
40
  }): string;
package/dist/format.js CHANGED
@@ -223,8 +223,6 @@ function compactProviderLabel(quota) {
223
223
  return 'MiniMax';
224
224
  if (canonical === 'rightcode')
225
225
  return 'RC';
226
- if (canonical === 'xyai')
227
- return 'XYAI';
228
226
  return sanitizeLine(quotaDisplayLabel(quota));
229
227
  }
230
228
  function compactWindowToken(label) {
@@ -267,6 +265,10 @@ function compactQuotaPercentToken(label, percent) {
267
265
  }
268
266
  if (/^cowork\s+7d$/i.test(safe))
269
267
  return rounded ? `Co7d${rounded}` : 'Co7d';
268
+ if (/^spark\s+5h$/i.test(safe))
269
+ return rounded ? `Sk5h${rounded}` : 'Sk5h';
270
+ if (/^spark\s+weekly$/i.test(safe))
271
+ return rounded ? `SkW${rounded}` : 'SkW';
270
272
  const token = compactWindowToken(safe).replace(/\s+/g, '');
271
273
  if (!rounded)
272
274
  return token;
@@ -328,6 +330,12 @@ function compactDesktopCurrencyValue(value, currency) {
328
330
  return rendered.replace(/^\$/, '');
329
331
  return rendered;
330
332
  }
333
+ function compactQuotaStaleToken(quota) {
334
+ return quota.stale ? 'St' : undefined;
335
+ }
336
+ function verboseQuotaStaleText(quota) {
337
+ return quota.stale ? 'stale' : undefined;
338
+ }
331
339
  function compactDesktopQuotaSegment(quota) {
332
340
  const label = compactProviderLabel(quota);
333
341
  if (quota.status !== 'ok') {
@@ -354,10 +362,15 @@ function compactDesktopQuotaSegment(quota) {
354
362
  const balanceToken = `B${compactDesktopCurrencyValue(quota.balance.amount, quota.balance.currency)}`;
355
363
  parts.push(balanceToken);
356
364
  }
365
+ const staleToken = compactQuotaStaleToken(quota);
366
+ if (staleToken)
367
+ parts.push(staleToken);
357
368
  return [label, ...parts].filter(Boolean).join(' ');
358
369
  }
359
370
  function renderDesktopCompactTitle(baseTitle, usage, quotas, config, _width) {
360
- const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
371
+ const visibleQuotas = config.sidebar.showQuota
372
+ ? collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status))
373
+ : [];
361
374
  const selectedProviderIDs = new Set(selectDesktopCompactProviderIDs(usage, config));
362
375
  const quotaSegments = visibleQuotas
363
376
  .filter((quota) => selectedProviderIDs.has(quota.providerID))
@@ -467,6 +480,7 @@ function alignPairs(pairs, indent = ' ') {
467
480
  }
468
481
  function compactQuotaInline(quota) {
469
482
  const label = sanitizeLine(quotaDisplayLabel(quota));
483
+ const staleToken = compactQuotaStaleToken(quota);
470
484
  if (quota.status !== 'ok') {
471
485
  if (quota.status === 'error')
472
486
  return `${label} Remaining ?`;
@@ -485,19 +499,19 @@ function compactQuotaInline(quota) {
485
499
  : firstLabel.replace(/^Daily\s+/i, '') || firstLabel;
486
500
  const hasMore = quota.windows.length > 1 ||
487
501
  (quota.balance !== undefined && !summary.includes('Balance '));
488
- return `${label}${summary ? ` ${summary}` : ''}${hasMore ? '+' : ''}`;
502
+ return `${label}${summary ? ` ${summary}` : ''}${hasMore ? '+' : ''}${staleToken ? ` ${staleToken}` : ''}`;
489
503
  }
490
504
  if (quota.balance) {
491
- return `${label} Balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`;
505
+ return `${label} Balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}${staleToken ? ` ${staleToken}` : ''}`;
492
506
  }
493
507
  const singlePercent = formatQuotaPercent(quota.remainingPercent, {
494
508
  rounded: true,
495
509
  missing: '',
496
510
  });
497
511
  if (singlePercent) {
498
- return `${label} ${singlePercent}`;
512
+ return `${label} ${singlePercent}${staleToken ? ` ${staleToken}` : ''}`;
499
513
  }
500
- return label;
514
+ return `${label}${staleToken ? ` ${staleToken}` : ''}`;
501
515
  }
502
516
  function renderSingleLineTitle(baseTitle, usage, quotas, config, width) {
503
517
  const baseBudget = Math.min(16, Math.max(8, Math.floor(width * 0.35)));
@@ -711,6 +725,9 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
711
725
  const tokens = [...compactTokens];
712
726
  if (balanceText)
713
727
  tokens.push(balanceText);
728
+ const staleToken = compactQuotaStaleToken(quota);
729
+ if (staleToken)
730
+ tokens.push(staleToken);
714
731
  return packInlineTokens(label, tokens, width, ' '.repeat(stringCellWidth(label) + 1));
715
732
  }
716
733
  // Keep a unified wrapped layout for providers that have multiple detail
@@ -724,16 +741,22 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
724
741
  return maybeBreak(single, [single]);
725
742
  }
726
743
  if (balanceText) {
727
- return maybeBreak(balanceText, [balanceText]);
744
+ const staleText = verboseQuotaStaleText(quota);
745
+ const detail = staleText ? `${balanceText} ${staleText}` : balanceText;
746
+ return maybeBreak(detail, [detail]);
728
747
  }
729
748
  // Fallback: single value from top-level remainingPercent
730
749
  const percent = formatQuotaPercent(quota.remainingPercent, { rounded: true });
731
750
  const reset = compactReset(quota.resetAt, 'Rst');
732
751
  const fallbackText = compactDetails
733
- ? [`R${percent.replace(/%$/, '')}`, reset ? `R${reset}` : undefined]
752
+ ? [
753
+ `R${percent.replace(/%$/, '')}`,
754
+ reset ? `R${reset}` : undefined,
755
+ compactQuotaStaleToken(quota),
756
+ ]
734
757
  .filter(Boolean)
735
758
  .join(' ')
736
- : `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
759
+ : `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}${verboseQuotaStaleText(quota) ? ` ${verboseQuotaStaleText(quota)}` : ''}`;
737
760
  return maybeBreak(fallbackText, [fallbackText]);
738
761
  }
739
762
  function compactCountdown(remainingMs) {
@@ -817,6 +840,238 @@ function periodLabel(period) {
817
840
  return 'This Month';
818
841
  return 'Current Session';
819
842
  }
843
+ function historyPeriodLabel(period) {
844
+ if (period === 'day')
845
+ return 'Daily';
846
+ if (period === 'week')
847
+ return 'Weekly';
848
+ if (period === 'month')
849
+ return 'Monthly';
850
+ return 'Session';
851
+ }
852
+ function historyProviderLabel(providerID) {
853
+ return quotaDisplayLabel({
854
+ providerID,
855
+ label: providerID,
856
+ status: 'ok',
857
+ checkedAt: 0,
858
+ });
859
+ }
860
+ function historyMdCell(value) {
861
+ return sanitizeLine(value).replace(/\|/g, '\\|');
862
+ }
863
+ function formatDelta(current, previous) {
864
+ if (previous === undefined)
865
+ return 'n/a';
866
+ if (!Number.isFinite(previous) || previous < 0)
867
+ return 'n/a';
868
+ if (previous === 0)
869
+ return current === 0 ? 'flat' : 'new';
870
+ const delta = ((current - previous) / previous) * 100;
871
+ if (!Number.isFinite(delta))
872
+ return 'n/a';
873
+ const abs = Math.abs(delta);
874
+ const rounded = (abs >= 10 ? delta.toFixed(0) : delta.toFixed(1)).replace(/\.0$/, '');
875
+ return `${delta > 0 ? '+' : ''}${rounded}%`;
876
+ }
877
+ function currentHistoryRow(result) {
878
+ return ([...result.rows].reverse().find((row) => row.range.isCurrent) ||
879
+ result.rows.at(-1));
880
+ }
881
+ function previousHistoryRow(result) {
882
+ const current = currentHistoryRow(result);
883
+ if (!current)
884
+ return undefined;
885
+ const index = result.rows.indexOf(current);
886
+ if (index <= 0)
887
+ return undefined;
888
+ return result.rows[index - 1];
889
+ }
890
+ function historyPeakRow(result, pick) {
891
+ let peak;
892
+ let peakValue = Number.NEGATIVE_INFINITY;
893
+ for (const row of result.rows) {
894
+ const value = pick(row);
895
+ if (value > peakValue) {
896
+ peak = row;
897
+ peakValue = value;
898
+ }
899
+ }
900
+ return peak;
901
+ }
902
+ function renderHistoryTotalsTable(result, options) {
903
+ const rows = result.rows;
904
+ const cacheTotal = getCacheCoverageMetrics(result.total).cachedRatio;
905
+ const cacheValues = rows
906
+ .map((row) => getCacheCoverageMetrics(row.usage).cachedRatio)
907
+ .filter((value) => value !== undefined);
908
+ const cacheAverage = cacheValues.length > 0
909
+ ? cacheValues.reduce((sum, value) => sum + value, 0) / cacheValues.length
910
+ : undefined;
911
+ const metricRows = [
912
+ {
913
+ label: 'Requests',
914
+ total: shortNumber(result.total.assistantMessages),
915
+ average: rows.length
916
+ ? shortNumber(result.total.assistantMessages / rows.length)
917
+ : '-',
918
+ },
919
+ {
920
+ label: 'Total Tokens',
921
+ total: shortNumber(result.total.total),
922
+ average: rows.length
923
+ ? shortNumber(result.total.total / rows.length)
924
+ : '-',
925
+ },
926
+ {
927
+ label: 'Cache Hit',
928
+ total: cacheTotal !== undefined ? formatPercent(cacheTotal, 1) : '-',
929
+ average: cacheAverage !== undefined ? formatPercent(cacheAverage, 1) : '-',
930
+ },
931
+ ...(options?.showCost !== false
932
+ ? [
933
+ {
934
+ label: 'API Cost',
935
+ total: formatApiCostValue(result.total.apiCost),
936
+ average: rows.length
937
+ ? formatApiCostValue(result.total.apiCost / rows.length)
938
+ : '-',
939
+ },
940
+ ]
941
+ : []),
942
+ ];
943
+ return [
944
+ '| Metric | Total | Avg/Period |',
945
+ '| --- | ---: | ---: |',
946
+ ...metricRows.map((metric) => `| ${metric.label} | ${metric.total} | ${metric.average} |`),
947
+ ];
948
+ }
949
+ function renderHistoryProviderBreakdown(result, options) {
950
+ const providers = Object.values(result.total.providers);
951
+ if (providers.length === 0)
952
+ return ['- no provider activity in selected range'];
953
+ const sorted = [...providers].sort((a, b) => {
954
+ if (b.total !== a.total)
955
+ return b.total - a.total;
956
+ return b.assistantMessages - a.assistantMessages;
957
+ });
958
+ return [
959
+ options?.showCost !== false
960
+ ? '| Provider | Req | Input | Output | Total | Share | Cache Hit | API Cost |'
961
+ : '| Provider | Req | Input | Output | Total | Share | Cache Hit |',
962
+ options?.showCost !== false
963
+ ? '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'
964
+ : '| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
965
+ ...sorted.map((provider) => {
966
+ const cache = getProviderCacheCoverageMetrics(provider).cachedRatio;
967
+ const share = result.total.total > 0
968
+ ? formatPercent(provider.total / result.total.total, 1)
969
+ : '-';
970
+ const cells = [
971
+ historyMdCell(historyProviderLabel(provider.providerID)),
972
+ shortNumber(provider.assistantMessages),
973
+ shortNumber(provider.input),
974
+ shortNumber(provider.output),
975
+ shortNumber(provider.total),
976
+ share,
977
+ cache !== undefined ? formatPercent(cache, 1) : '-',
978
+ ];
979
+ if (options?.showCost !== false) {
980
+ cells.push(provider.apiCost > 0 ? formatApiCostValue(provider.apiCost) : '-');
981
+ }
982
+ return `| ${cells.join(' | ')} |`;
983
+ }),
984
+ ];
985
+ }
986
+ function renderHistoryQuotaSnapshot(quotas) {
987
+ const visible = toolVisibleQuotaSnapshots(quotas).slice(0, 5);
988
+ if (visible.length === 0)
989
+ return ['- no provider quota data available'];
990
+ return visible.map((quota) => {
991
+ const label = quotaDisplayLabel(quota);
992
+ if (quota.status === 'error') {
993
+ return `- ${label}: error${quota.note ? ` | ${quota.note}` : ''}`;
994
+ }
995
+ if (quota.windows && quota.windows.length > 0) {
996
+ const summary = quota.windows
997
+ .slice(0, 2)
998
+ .map((window) => {
999
+ const remaining = window.showPercent === false
1000
+ ? undefined
1001
+ : formatQuotaPercent(window.remainingPercent);
1002
+ const reset = reportResetLine(window.resetAt, window.resetLabel, window.label);
1003
+ return [window.label || 'Quota', remaining, `reset ${reset}`]
1004
+ .filter(Boolean)
1005
+ .join(' | ');
1006
+ })
1007
+ .join('; ');
1008
+ return `- ${label}: ${summary}`;
1009
+ }
1010
+ if (quota.balance) {
1011
+ return `- ${label}: balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`;
1012
+ }
1013
+ return `- ${label}: ${formatQuotaPercent(quota.remainingPercent)} | reset ${reportResetLine(quota.resetAt)}`;
1014
+ });
1015
+ }
1016
+ function renderHistoryPeriodDetailRows(result, options) {
1017
+ const showCost = options?.showCost !== false;
1018
+ return result.rows.length
1019
+ ? result.rows.map((row) => {
1020
+ const cache = getCacheCoverageMetrics(row.usage).cachedRatio;
1021
+ const cells = [
1022
+ `${row.range.label}${row.range.isCurrent ? '*' : ''}`,
1023
+ shortNumber(row.usage.assistantMessages),
1024
+ shortNumber(row.usage.input),
1025
+ shortNumber(row.usage.output),
1026
+ shortNumber(row.usage.cacheRead + row.usage.cacheWrite),
1027
+ cache !== undefined ? formatPercent(cache, 1) : '-',
1028
+ shortNumber(row.usage.total),
1029
+ ];
1030
+ if (showCost)
1031
+ cells.push(formatApiCostValue(row.usage.apiCost));
1032
+ return `| ${cells.join(' | ')} |`;
1033
+ })
1034
+ : [
1035
+ showCost
1036
+ ? '| - | - | - | - | - | - | - | - |'
1037
+ : '| - | - | - | - | - | - | - |',
1038
+ ];
1039
+ }
1040
+ export function renderHistoryMarkdownReport(result, quotas, options) {
1041
+ const showCost = options?.showCost !== false;
1042
+ const detailRows = renderHistoryPeriodDetailRows(result, { showCost });
1043
+ return [
1044
+ `## Quota History - ${historyPeriodLabel(result.period)} since ${result.since.raw}`,
1045
+ ...(result.warning
1046
+ ? ['', `> Warning: ${sanitizeLine(result.warning)}`]
1047
+ : []),
1048
+ '',
1049
+ '### Quota Status',
1050
+ '',
1051
+ ...renderHistoryQuotaSnapshot(quotas),
1052
+ '',
1053
+ '### Totals',
1054
+ '',
1055
+ ...renderHistoryTotalsTable(result, { showCost }),
1056
+ '',
1057
+ '### Provider Breakdown',
1058
+ '',
1059
+ ...renderHistoryProviderBreakdown(result, { showCost }),
1060
+ '',
1061
+ '### Period Detail',
1062
+ '',
1063
+ showCost
1064
+ ? '| Period | Requests | Input | Output | Cache | Cache Hit | Total | API Cost |'
1065
+ : '| Period | Requests | Input | Output | Cache | Cache Hit | Total |',
1066
+ showCost
1067
+ ? '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'
1068
+ : '| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
1069
+ ...detailRows,
1070
+ ...(result.rows.some((row) => row.range.isCurrent)
1071
+ ? ['', '* `*` marks the current partial period.']
1072
+ : []),
1073
+ ].join('\n');
1074
+ }
820
1075
  export function renderMarkdownReport(period, usage, quotas, options) {
821
1076
  const showCost = options?.showCost !== false;
822
1077
  const cacheMetrics = getCacheCoverageMetrics(usage);
@@ -926,19 +1181,21 @@ export function renderMarkdownReport(period, usage, quotas, options) {
926
1181
  : '| --- | ---: | ---: | ---: | ---: | ---: |';
927
1182
  const quotaLines = toolVisibleQuotaSnapshots(quotas).flatMap((quota) => {
928
1183
  const displayLabel = quotaDisplayLabel(quota);
1184
+ const staleSuffix = quota.stale ? ' | stale' : '';
929
1185
  // Multi-window detail
930
1186
  if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
931
1187
  const windowLines = quota.windows.map((win) => {
932
1188
  const extraNote = win.note || (win === quota.windows?.[0] && quota.note)
933
1189
  ? ` | ${win.note || quota.note}`
934
1190
  : '';
1191
+ const staleNote = quota.stale && win === quota.windows?.[0] ? staleSuffix : '';
935
1192
  if (win.showPercent === false) {
936
1193
  const winLabel = win.label ? ` (${win.label})` : '';
937
- return mdCell(`- ${displayLabel}${winLabel}: ${quota.status} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}${extraNote}`);
1194
+ return mdCell(`- ${displayLabel}${winLabel}: ${quota.status} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}${extraNote}${staleNote}`);
938
1195
  }
939
1196
  const remaining = formatQuotaPercent(win.remainingPercent);
940
1197
  const winLabel = win.label ? ` (${win.label})` : '';
941
- return mdCell(`- ${displayLabel}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}${extraNote}`);
1198
+ return mdCell(`- ${displayLabel}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(win.resetAt, win.resetLabel, win.label)}${extraNote}${staleNote}`);
942
1199
  });
943
1200
  if (quota.balance) {
944
1201
  windowLines.push(mdCell(`- ${displayLabel}: ${quota.status} | balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`));
@@ -947,7 +1204,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
947
1204
  }
948
1205
  if (quota.status === 'ok' && quota.balance) {
949
1206
  return [
950
- mdCell(`- ${displayLabel}: ${quota.status} | balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`),
1207
+ mdCell(`- ${displayLabel}: ${quota.status} | balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}${staleSuffix}`),
951
1208
  ];
952
1209
  }
953
1210
  if (quota.status === 'error') {
@@ -957,12 +1214,20 @@ export function renderMarkdownReport(period, usage, quotas, options) {
957
1214
  }
958
1215
  const remaining = formatQuotaPercent(quota.remainingPercent);
959
1216
  return [
960
- mdCell(`- ${displayLabel}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`),
1217
+ mdCell(`- ${displayLabel}: ${quota.status} | remaining ${remaining} | reset ${reportResetLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}${staleSuffix}`),
961
1218
  ];
962
1219
  });
963
1220
  return [
964
1221
  `## Quota Report - ${periodLabel(period)}`,
965
1222
  '',
1223
+ '### Quota Status',
1224
+ '',
1225
+ ...(quotaLines.length
1226
+ ? quotaLines
1227
+ : ['- no provider quota data available']),
1228
+ '',
1229
+ '### Usage Summary',
1230
+ '',
966
1231
  `- Sessions: ${usage.sessionCount}`,
967
1232
  `- Requests: ${usage.assistantMessages}`,
968
1233
  `- Tokens: input ${usage.input}, output ${usage.output}, cache_read ${usage.cacheRead}, cache_write ${usage.cacheWrite}, total ${usage.total}`,
@@ -975,9 +1240,6 @@ export function renderMarkdownReport(period, usage, quotas, options) {
975
1240
  `- API cost: ${apiCostSummaryValue()}`,
976
1241
  ]
977
1242
  : []),
978
- ...(highlightLines().length > 0
979
- ? ['', '### Highlights', ...highlightLines()]
980
- : []),
981
1243
  '',
982
1244
  '### Usage by Provider',
983
1245
  '',
@@ -990,12 +1252,9 @@ export function renderMarkdownReport(period, usage, quotas, options) {
990
1252
  ? '| - | - | - | - | - | - | - | - | - |'
991
1253
  : '| - | - | - | - | - | - |',
992
1254
  ]),
993
- '',
994
- '### Subscription Quota',
995
- '',
996
- ...(quotaLines.length
997
- ? quotaLines
998
- : ['- no provider quota data available']),
1255
+ ...(highlightLines().length > 0
1256
+ ? ['', '### Highlights', ...highlightLines()]
1257
+ : []),
999
1258
  ].join('\n');
1000
1259
  }
1001
1260
  export function renderToastMessage(period, usage, quotas, options) {
@@ -1030,6 +1289,22 @@ export function renderToastMessage(period, usage, quotas, options) {
1030
1289
  });
1031
1290
  }
1032
1291
  lines.push(...alignPairs(tokenPairs).map((line) => fitLine(line, width)));
1292
+ const providerCachePairs = Object.values(usage.providers)
1293
+ .map((provider) => {
1294
+ const metrics = getProviderCacheCoverageMetrics(provider);
1295
+ if (metrics.cachedRatio === undefined)
1296
+ return undefined;
1297
+ return {
1298
+ label: displayShortLabel(provider.providerID),
1299
+ value: `Cached ${formatPercent(metrics.cachedRatio, 1)}`,
1300
+ };
1301
+ })
1302
+ .filter((item) => Boolean(item));
1303
+ if (providerCachePairs.length > 0) {
1304
+ lines.push('');
1305
+ lines.push(fitLine('Provider Cache', width));
1306
+ lines.push(...alignPairs(providerCachePairs).map((line) => fitLine(line, width)));
1307
+ }
1033
1308
  if (showCost) {
1034
1309
  const costPairs = Object.values(usage.providers)
1035
1310
  .filter((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot')
@@ -1055,22 +1330,6 @@ export function renderToastMessage(period, usage, quotas, options) {
1055
1330
  : ' -', width));
1056
1331
  }
1057
1332
  }
1058
- const providerCachePairs = Object.values(usage.providers)
1059
- .map((provider) => {
1060
- const metrics = getProviderCacheCoverageMetrics(provider);
1061
- if (metrics.cachedRatio === undefined)
1062
- return undefined;
1063
- return {
1064
- label: displayShortLabel(provider.providerID),
1065
- value: `Cached ${formatPercent(metrics.cachedRatio, 1)}`,
1066
- };
1067
- })
1068
- .filter((item) => Boolean(item));
1069
- if (providerCachePairs.length > 0) {
1070
- lines.push('');
1071
- lines.push(fitLine('Provider Cache', width));
1072
- lines.push(...alignPairs(providerCachePairs).map((line) => fitLine(line, width)));
1073
- }
1074
1333
  const quotaPairs = toolVisibleQuotaSnapshots(quotas).flatMap((item) => {
1075
1334
  if (item.status === 'ok') {
1076
1335
  if (item.windows && item.windows.length > 0) {
@@ -1085,6 +1344,8 @@ export function renderToastMessage(period, usage, quotas, options) {
1085
1344
  parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
1086
1345
  if (win.note)
1087
1346
  parts.push(win.note);
1347
+ if (item.stale && idx === 0)
1348
+ parts.push('stale');
1088
1349
  return {
1089
1350
  label: idx === 0 ? quotaDisplayLabel(item) : '',
1090
1351
  value: parts.filter(Boolean).join(' '),
@@ -1102,7 +1363,7 @@ export function renderToastMessage(period, usage, quotas, options) {
1102
1363
  return [
1103
1364
  {
1104
1365
  label: quotaDisplayLabel(item),
1105
- value: `Balance ${formatCurrency(item.balance.amount, item.balance.currency)}`,
1366
+ value: `Balance ${formatCurrency(item.balance.amount, item.balance.currency)}${item.stale ? ' stale' : ''}`,
1106
1367
  },
1107
1368
  ];
1108
1369
  }
@@ -1111,7 +1372,7 @@ export function renderToastMessage(period, usage, quotas, options) {
1111
1372
  return [
1112
1373
  {
1113
1374
  label: quotaDisplayLabel(item),
1114
- value: `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`,
1375
+ value: `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}${item.stale ? ' stale' : ''}`,
1115
1376
  },
1116
1377
  ];
1117
1378
  }
@@ -0,0 +1,8 @@
1
+ import type { Message } from '@opencode-ai/sdk';
2
+ export type MessageEntry = {
3
+ info: Message;
4
+ };
5
+ export declare function decodeMessageInfo(value: unknown): Message | undefined;
6
+ export declare function decodeMessageEntries(value: unknown): MessageEntry[] | undefined;
7
+ export declare function nextCursorFromResponse(value: unknown): string | undefined;
8
+ export declare function isMissingSessionError(error: unknown): boolean;
@@ -0,0 +1,157 @@
1
+ import { debug } from './helpers.js';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function isFiniteNumber(value) {
6
+ return typeof value === 'number' && Number.isFinite(value);
7
+ }
8
+ function decodeTokens(value) {
9
+ if (!isRecord(value))
10
+ return undefined;
11
+ if (!isFiniteNumber(value.input))
12
+ return undefined;
13
+ if (!isFiniteNumber(value.output))
14
+ return undefined;
15
+ const reasoning = isFiniteNumber(value.reasoning) ? value.reasoning : 0;
16
+ const cacheRaw = isRecord(value.cache) ? value.cache : {};
17
+ const read = isFiniteNumber(cacheRaw.read) ? cacheRaw.read : 0;
18
+ const write = isFiniteNumber(cacheRaw.write) ? cacheRaw.write : 0;
19
+ return {
20
+ input: value.input,
21
+ output: value.output,
22
+ reasoning,
23
+ cache: { read, write },
24
+ };
25
+ }
26
+ export function decodeMessageInfo(value) {
27
+ if (!isRecord(value))
28
+ return undefined;
29
+ if (typeof value.id !== 'string')
30
+ return undefined;
31
+ if (typeof value.sessionID !== 'string')
32
+ return undefined;
33
+ if (typeof value.role !== 'string')
34
+ return undefined;
35
+ if (!isRecord(value.time))
36
+ return undefined;
37
+ if (!isFiniteNumber(value.time.created))
38
+ return undefined;
39
+ if (value.time.completed !== undefined &&
40
+ !isFiniteNumber(value.time.completed)) {
41
+ return undefined;
42
+ }
43
+ if (value.role !== 'assistant') {
44
+ return {
45
+ ...value,
46
+ time: {
47
+ created: value.time.created,
48
+ completed: value.time.completed,
49
+ },
50
+ };
51
+ }
52
+ if (typeof value.providerID !== 'string')
53
+ return undefined;
54
+ if (typeof value.modelID !== 'string')
55
+ return undefined;
56
+ const tokens = decodeTokens(value.tokens);
57
+ if (!tokens)
58
+ return undefined;
59
+ return {
60
+ ...value,
61
+ time: {
62
+ created: value.time.created,
63
+ completed: value.time.completed,
64
+ },
65
+ tokens,
66
+ };
67
+ }
68
+ export function decodeMessageEntries(value) {
69
+ if (!Array.isArray(value))
70
+ return undefined;
71
+ const decoded = value
72
+ .map((item) => {
73
+ if (!isRecord(item))
74
+ return undefined;
75
+ const info = decodeMessageInfo(item.info);
76
+ if (!info)
77
+ return undefined;
78
+ return { info };
79
+ })
80
+ .filter((item) => Boolean(item));
81
+ if (decoded.length > 0 && decoded.length < value.length) {
82
+ debug(`message entries partially decoded: kept ${decoded.length}/${value.length}`);
83
+ return undefined;
84
+ }
85
+ if (decoded.length === 0 && value.length > 0)
86
+ return undefined;
87
+ return decoded;
88
+ }
89
+ export function nextCursorFromResponse(value) {
90
+ if (!isRecord(value))
91
+ return undefined;
92
+ const response = value.response;
93
+ if (!isRecord(response))
94
+ return undefined;
95
+ const headers = response.headers;
96
+ if (!headers || typeof headers.get !== 'function') {
97
+ return undefined;
98
+ }
99
+ const next = headers.get('X-Next-Cursor');
100
+ return typeof next === 'string' && next ? next : undefined;
101
+ }
102
+ function errorStatusCode(value, seen = new Set()) {
103
+ if (!isRecord(value) || seen.has(value))
104
+ return undefined;
105
+ seen.add(value);
106
+ const status = value.status;
107
+ if (typeof status === 'number' && Number.isFinite(status))
108
+ return status;
109
+ const statusCode = value.statusCode;
110
+ if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
111
+ return statusCode;
112
+ }
113
+ return (errorStatusCode(value.response, seen) ||
114
+ errorStatusCode(value.cause, seen) ||
115
+ errorStatusCode(value.error, seen));
116
+ }
117
+ function errorText(value, seen = new Set()) {
118
+ if (!value || seen.has(value))
119
+ return '';
120
+ if (typeof value === 'string')
121
+ return value;
122
+ if (typeof value === 'number' || typeof value === 'boolean')
123
+ return `${value}`;
124
+ if (value instanceof Error) {
125
+ seen.add(value);
126
+ return [
127
+ value.message,
128
+ errorText(value.cause, seen),
129
+ ]
130
+ .filter(Boolean)
131
+ .join('\n');
132
+ }
133
+ if (!isRecord(value))
134
+ return '';
135
+ seen.add(value);
136
+ return [
137
+ typeof value.message === 'string' ? value.message : '',
138
+ typeof value.error === 'string' ? value.error : '',
139
+ typeof value.detail === 'string' ? value.detail : '',
140
+ typeof value.title === 'string' ? value.title : '',
141
+ errorText(value.response, seen),
142
+ errorText(value.data, seen),
143
+ errorText(value.cause, seen),
144
+ ]
145
+ .filter(Boolean)
146
+ .join('\n');
147
+ }
148
+ export function isMissingSessionError(error) {
149
+ const status = errorStatusCode(error);
150
+ if (status === 404 || status === 410)
151
+ return true;
152
+ const text = errorText(error).toLowerCase();
153
+ if (!text)
154
+ return false;
155
+ return (/\b(session|conversation)\b.*\b(not found|missing|deleted|does not exist)\b/.test(text) ||
156
+ /\b(not found|missing|deleted|does not exist)\b.*\b(session|conversation)\b/.test(text));
157
+ }