@leo000001/opencode-quota-sidebar 3.0.9 → 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.
- package/CHANGELOG.md +0 -1
- package/README.md +157 -42
- package/README.zh-CN.md +157 -42
- package/SECURITY.md +1 -1
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +354 -0
- package/dist/cli_render.d.ts +17 -0
- package/dist/cli_render.js +292 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +2 -2
- package/dist/format.d.ts +4 -0
- package/dist/format.js +302 -41
- package/dist/history_messages.d.ts +8 -0
- package/dist/history_messages.js +157 -0
- package/dist/history_usage.d.ts +93 -0
- package/dist/history_usage.js +251 -0
- package/dist/index.js +29 -4
- package/dist/period.d.ts +29 -1
- package/dist/period.js +187 -9
- package/dist/provider_catalog.d.ts +8 -0
- package/dist/provider_catalog.js +68 -0
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +69 -45
- package/dist/providers/core/openai.js +101 -8
- package/dist/providers/index.d.ts +1 -2
- package/dist/providers/index.js +1 -3
- package/dist/quota.d.ts +4 -2
- package/dist/quota.js +18 -21
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -24
- package/dist/quota_service.d.ts +1 -0
- package/dist/quota_service.js +151 -19
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/storage_dates.d.ts +1 -1
- package/dist/storage_dates.js +8 -5
- package/dist/storage_parse.js +23 -1
- package/dist/supported_quota.d.ts +4 -0
- package/dist/supported_quota.js +36 -0
- package/dist/title.js +18 -8
- package/dist/tools.d.ts +14 -3
- package/dist/tools.js +54 -2
- package/dist/tui.tsx +17 -6
- package/dist/tui_helpers.js +11 -6
- package/dist/types.d.ts +8 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +93 -9
- package/dist/usage_service.d.ts +4 -1
- package/dist/usage_service.js +193 -189
- package/package.json +4 -1
- package/quota-sidebar.config.example.json +36 -45
- package/dist/providers/third_party/xyai.d.ts +0 -2
- 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 =
|
|
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
|
-
|
|
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
|
-
? [
|
|
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
|
-
|
|
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
|
+
}
|