letmecode 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -27
- package/ink-app/dist/index.js +68 -35
- package/ink-app/dist/providers/antigravity/models.js +46 -0
- package/ink-app/dist/providers/antigravity/provider.js +288 -0
- package/ink-app/dist/providers/antigravity/quota-parser.js +49 -0
- package/ink-app/dist/providers/antigravity/rpc/client.js +54 -0
- package/ink-app/dist/providers/antigravity/rpc/discovery.js +84 -0
- package/ink-app/dist/providers/antigravity/rpc/quota.js +25 -0
- package/ink-app/dist/providers/antigravity/rpc/usage.js +80 -0
- package/ink-app/dist/providers/antigravity/types.js +1 -0
- package/ink-app/dist/providers/antigravity/usage-parse.js +23 -0
- package/ink-app/dist/providers/antigravity.js +2 -537
- package/ink-app/dist/providers/claude.js +71 -152
- package/ink-app/dist/providers/contract.js +5 -2
- package/ink-app/dist/providers/copilot/models.js +55 -0
- package/ink-app/dist/providers/copilot/otel/configure.js +134 -0
- package/ink-app/dist/providers/copilot/otel/discover.js +94 -0
- package/ink-app/dist/providers/copilot/otel/parse.js +228 -0
- package/ink-app/dist/providers/copilot/provider.js +259 -0
- package/ink-app/dist/providers/copilot/quota.js +257 -0
- package/ink-app/dist/providers/copilot/usage/aggregate.js +84 -0
- package/ink-app/dist/providers/copilot.js +4 -373
- package/ink-app/dist/providers/index.js +1 -1
- package/ink-app/dist/reporting.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,43 +3,26 @@
|
|
|
3
3
|
Ussage:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx -y letmecode
|
|
7
|
-
npx -y letmecode -- -h
|
|
8
|
-
npx -y letmecode -- --log-to log.txt
|
|
6
|
+
npx -y letmecode@latest
|
|
9
7
|
```
|
|
10
8
|
|
|
11
|
-
`--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
Preview:
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
<img width="2301" height="1397" alt="image" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
|
|
16
13
|
|
|
17
|
-
| Key | Action |
|
|
18
|
-
| --- | --- |
|
|
19
|
-
| `[` / `]` | Switch providers |
|
|
20
|
-
| `Tab` / `Shift+Tab` | Switch providers when supported by the terminal |
|
|
21
|
-
| `Up` / `Down` or `k` / `j` | Switch dashboard sections |
|
|
22
|
-
| `Left` / `Right` | Select the previous or next table row |
|
|
23
|
-
| `Enter` | Run the selected provider action |
|
|
24
|
-
| `1` | Select the Copilot VS Code setup action |
|
|
25
|
-
| `h` / `l` | Select a Copilot setup action |
|
|
26
|
-
| `q` or `Esc` | Quit |
|
|
27
|
-
| Mouse click | Click a provider tab or a section tab to switch to it. Hold `Shift` while dragging to select/copy text anywhere else. |
|
|
28
14
|
|
|
29
|
-
## Copilot
|
|
30
15
|
|
|
31
|
-
Copilot CLI usage is read from `~/.copilot/session-state`.
|
|
32
16
|
|
|
33
|
-
|
|
17
|
+
## Advanced ussage
|
|
34
18
|
|
|
35
|
-
```json
|
|
36
|
-
{
|
|
37
|
-
"github.copilot.chat.otel.enabled": true,
|
|
38
|
-
"github.copilot.chat.otel.exporterType": "file",
|
|
39
|
-
"github.copilot.chat.otel.outfile": "~/.copilot/otel/vscode.jsonl",
|
|
40
|
-
"github.copilot.chat.otel.captureContent": false
|
|
41
|
-
}
|
|
42
19
|
```
|
|
20
|
+
npx -y letmecode -- -h
|
|
21
|
+
npx -y letmecode -- --log-to log.txt
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
|
|
25
|
+
|
|
43
26
|
|
|
44
27
|
## Local development
|
|
45
28
|
|
package/ink-app/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const DETAIL_TABS = [
|
|
|
19
19
|
{ id: "usage-by-model", label: "Models" }
|
|
20
20
|
];
|
|
21
21
|
const CODEX_CREDIT_COST_USD = 0.01;
|
|
22
|
-
const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Window", "Used", "Start", "End", "API eq."];
|
|
22
|
+
const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq."];
|
|
23
23
|
const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
|
|
24
24
|
const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
|
|
25
25
|
const COPILOT_ACTIONS = [
|
|
@@ -195,7 +195,7 @@ function App(props) {
|
|
|
195
195
|
moveSelectedTableRow(-1);
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, {
|
|
198
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: viewportHeight, overflow: "hidden", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "LetMeCode Usage Dashboard" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, color: "white", backgroundColor: "green", children: " beta " })] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "Provider " }), sortedProviderStates.map((state) => (_jsx(ProviderTab, { label: state.provider.label, active: state.provider.id === selectedProvider.provider.id, status: state.status, regionRef: getRegionRef(`provider:${state.provider.id}`) }, state.provider.id)))] }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "View " }), DETAIL_TABS.map((tab, index) => (_jsx(DetailTab, { label: tab.label, active: index === selectedDetailTabIndex, regionRef: getRegionRef(`vtab:${index}`) }, tab.id)))] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, overflow: "hidden", children: _jsx(Box, { ref: contentPanelRef, flexDirection: "column", flexGrow: 1, overflow: "hidden", children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRowKey: selectedLimitRow ? getLimitRowKey(selectedLimitRow) : undefined, selectedDayKey: selectedDayRow?.dayKey, selectedModelId: selectedModelRow?.modelId, availableHeight: contentPanelHeight }) }) }), _jsx(SelectionDetailsPanel, { providerState: selectedProvider, tabId: selectedDetailTab.id, selectedLimitRow: selectedLimitRow, selectedDayRow: selectedDayRow, selectedModelRow: selectedModelRow }), _jsx(CopilotActionsPanel, { providerState: selectedProvider, actionMessage: copilotActionMessage, selectedActionIndex: selectedCopilotActionIndex }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", overflow: "hidden", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }), _jsx(Text, { color: "gray", children: "Tab provider \u00B7 \u2190/\u2192 view \u00B7 \u2191/\u2193 row \u00B7 q quit" })] }));
|
|
199
199
|
}
|
|
200
200
|
function CopilotActionsPanel(props) {
|
|
201
201
|
if (props.providerState.provider.id !== "copilot") {
|
|
@@ -203,7 +203,7 @@ function CopilotActionsPanel(props) {
|
|
|
203
203
|
}
|
|
204
204
|
const hasNoUsage = props.providerState.status === "ready" && props.providerState.stats.summary.tokenEvents === 0;
|
|
205
205
|
const accentColor = hasNoUsage ? "red" : "cyan";
|
|
206
|
-
return (_jsxs(Box, {
|
|
206
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: accentColor, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: accentColor, children: "Copilot setup" }), _jsx(Box, { children: COPILOT_ACTIONS.map((action, index) => (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: index === (props.selectedActionIndex ?? 0), bold: hasNoUsage && action.id === "vscode", color: action.enabled ? (hasNoUsage && action.id === "vscode" ? accentColor : undefined) : "gray", children: `${index + 1} ${action.label}` }) }, action.id))) }), _jsx(Text, { color: hasNoUsage ? accentColor : "gray", children: "Press 1 or h/l to select an action, enter to run selected." }), props.actionMessage ? _jsx(Text, { children: props.actionMessage }) : null] }));
|
|
207
207
|
}
|
|
208
208
|
function runCopilotAction(actionId, setCopilotActionMessage) {
|
|
209
209
|
setCopilotActionMessage("Updating VS Code settings...");
|
|
@@ -247,7 +247,10 @@ function SummaryPanel(props) {
|
|
|
247
247
|
const costPerEvent = totals.eventCount > 0
|
|
248
248
|
? (totals.estimatedCredits * CODEX_CREDIT_COST_USD) / totals.eventCount
|
|
249
249
|
: 0;
|
|
250
|
-
return (_jsxs(Box, {
|
|
250
|
+
return (_jsxs(Box, { borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Summary" }), _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 45, children: [_jsx(DetailRow, { label: "Plan", value: summary.distinctPlanTypes.join(", ") || "none" }), _jsx(DetailRow, { label: "Models", value: summary.distinctModels.join(", ") || "none" }), _jsx(DetailRow, { label: "Period", value: period }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Usage" }), _jsx(DetailRow, { label: "Events", value: formatInteger(totals.eventCount) }), _jsx(DetailRow, { label: "Input", value: formatOverviewTokenCount(totals.inputTokens) }), _jsx(DetailRow, { label: "Output", value: formatOverviewTokenCount(totals.outputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheOverviewTokenCount(totals.cacheReadStatus, totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Reasoning", value: formatOverviewTokenCount(totals.reasoningOutputTokens) }), _jsx(DetailRow, { label: "Total", value: formatOverviewTokenCount(totals.totalTokens) }), _jsx(DetailRow, { label: "API equiv.", value: formatUsageUsd(totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Efficiency" }), _jsx(DetailRow, { label: "Cache ratio", value: formatPercent(cacheRatio) }), (() => {
|
|
251
|
+
const { label, value } = formatInputOutputRatio(totals, props.stats.providerId);
|
|
252
|
+
return _jsx(DetailRow, { label: label, value: value, padLength: Math.max(14, label.length + 1), noSlice: true });
|
|
253
|
+
})(), _jsx(DetailRow, { label: "Avg/event", value: `${formatOverviewTokenCount(averageTokensPerEvent)} tokens` }), _jsx(DetailRow, { label: "Cost/event", value: formatUnitUsd(costPerEvent) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: "Data source" }), _jsx(DetailRow, { label: "Files", value: formatInteger(summary.filesScanned) }), _jsx(DetailRow, { label: "Lines", value: formatInteger(summary.linesRead) }), _jsx(DetailRow, { label: "Path", value: summary.rootPath })] })] })] }));
|
|
251
254
|
}
|
|
252
255
|
function ContentPanel(props) {
|
|
253
256
|
if (props.providerState.status === "loading") {
|
|
@@ -320,7 +323,6 @@ function buildTextTableLines(options) {
|
|
|
320
323
|
if (options.rows.length === 0 && !options.totalRow) {
|
|
321
324
|
return {
|
|
322
325
|
bodyLines: [
|
|
323
|
-
{ key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
|
|
324
326
|
{ key: `${options.lineKeyPrefix}-empty`, text: "No rows found.", color: "gray" }
|
|
325
327
|
]
|
|
326
328
|
};
|
|
@@ -330,7 +332,6 @@ function buildTextTableLines(options) {
|
|
|
330
332
|
: options.rows;
|
|
331
333
|
const table = createTextTable(options.headers, tableRows.map((row) => row.cells));
|
|
332
334
|
const headerLines = [
|
|
333
|
-
{ key: `${options.lineKeyPrefix}-title`, text: options.title, bold: true },
|
|
334
335
|
{
|
|
335
336
|
key: `${options.lineKeyPrefix}-top-border`,
|
|
336
337
|
text: buildTableBorder(table, "┌", "┬", "┐"),
|
|
@@ -394,7 +395,6 @@ function buildLimitWindowTableLines(stats, selectedRowKey) {
|
|
|
394
395
|
? new Set([primaryRows[primaryRows.length - 1].key])
|
|
395
396
|
: undefined;
|
|
396
397
|
return buildTextTableLines({
|
|
397
|
-
title: "Limits",
|
|
398
398
|
lineKeyPrefix: "limits",
|
|
399
399
|
headers: LIMIT_TABLE_HEADERS,
|
|
400
400
|
rows: [...primaryRows, ...secondaryRows],
|
|
@@ -408,17 +408,19 @@ function buildLimitWindowTableRow(window) {
|
|
|
408
408
|
cells: [
|
|
409
409
|
window.scope,
|
|
410
410
|
window.planType,
|
|
411
|
+
formatLimitWindowModels(window),
|
|
411
412
|
formatCompactWindowMinutes(window.windowMinutes),
|
|
412
413
|
formatUsedPercentRange(window.minUsedPercent, window.maxUsedPercent),
|
|
413
414
|
formatCompactLocalDateTime(window.startTimeUtcIso),
|
|
414
415
|
formatCompactLocalDateTime(window.endTimeUtcIso),
|
|
415
|
-
|
|
416
|
+
// Status-aware: shows "-" when the API-equivalent cost is unknown rather
|
|
417
|
+
// than a misleading $0.00.
|
|
418
|
+
formatUsageUsd(window.totals)
|
|
416
419
|
]
|
|
417
420
|
};
|
|
418
421
|
}
|
|
419
422
|
function buildDailyUsageTableLines(rows, selectedDayKey) {
|
|
420
423
|
return buildTextTableLines({
|
|
421
|
-
title: "Daily usage",
|
|
422
424
|
lineKeyPrefix: "daily",
|
|
423
425
|
headers: DAILY_TABLE_HEADERS,
|
|
424
426
|
rows: rows.map((row) => ({
|
|
@@ -428,8 +430,8 @@ function buildDailyUsageTableLines(rows, selectedDayKey) {
|
|
|
428
430
|
formatCompactTokenCount(row.totals.eventCount),
|
|
429
431
|
formatCompactTokenCount(row.totals.inputTokens),
|
|
430
432
|
formatCompactTokenCount(row.totals.outputTokens),
|
|
431
|
-
formatCompactCacheTokens(row.totals, row.totals.cacheReadInputTokens),
|
|
432
|
-
formatCompactCacheTokens(row.totals, row.totals.cacheWriteInputTokens),
|
|
433
|
+
formatCompactCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens),
|
|
434
|
+
formatCompactCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens),
|
|
433
435
|
formatUsageUsd(row.totals)
|
|
434
436
|
]
|
|
435
437
|
})),
|
|
@@ -438,7 +440,6 @@ function buildDailyUsageTableLines(rows, selectedDayKey) {
|
|
|
438
440
|
}
|
|
439
441
|
function buildModelUsageTableLines(rows, totals, selectedModelId) {
|
|
440
442
|
return buildTextTableLines({
|
|
441
|
-
title: "Model usage",
|
|
442
443
|
lineKeyPrefix: "model",
|
|
443
444
|
headers: MODEL_TABLE_HEADERS,
|
|
444
445
|
rows: rows.map((row) => ({
|
|
@@ -458,7 +459,7 @@ function SelectionDetailsPanel(props) {
|
|
|
458
459
|
}
|
|
459
460
|
if (props.tabId === "limit-windows" && props.selectedLimitRow) {
|
|
460
461
|
const row = props.selectedLimitRow;
|
|
461
|
-
return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
|
|
462
|
+
return (_jsx(DetailsPanelFrame, { children: _jsxs(Box, { children: [_jsxs(Box, { flexDirection: "column", width: 25, children: [_jsx(DetailRow, { label: "Plan", value: row.planType }), _jsx(DetailRow, { label: "Models", value: formatLimitWindowModels(row) }), _jsx(DetailRow, { label: "Window", value: formatCompactWindowMinutes(row.windowMinutes) }), _jsx(DetailRow, { label: "Usage", value: formatUsedPercentRange(row.minUsedPercent, row.maxUsedPercent) }), _jsx(DetailRow, { label: "Events", value: formatInteger(row.eventCount) }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals) })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(DetailRow, { label: "Period", value: `${formatCompactLocalDateTime(row.startTimeUtcIso)} → ${formatCompactLocalDateTime(row.endTimeUtcIso)}` }), _jsx(DetailRow, { label: "Input", value: formatInteger(row.totals.inputTokens) }), _jsx(DetailRow, { label: "Cache read", value: formatCacheTokens(row.totals.cacheReadStatus, row.totals.cacheReadInputTokens) }), _jsx(DetailRow, { label: "Cache write", value: formatCacheTokens(row.totals.cacheWriteStatus, row.totals.cacheWriteInputTokens) }), _jsx(DetailRow, { label: "Output", value: formatInteger(row.totals.outputTokens) }), _jsx(DetailRow, { label: "Total", value: formatInteger(row.totals.totalTokens) })] })] }) }));
|
|
462
463
|
}
|
|
463
464
|
if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
464
465
|
const row = props.selectedDayRow;
|
|
@@ -470,10 +471,13 @@ function SelectionDetailsPanel(props) {
|
|
|
470
471
|
return null;
|
|
471
472
|
}
|
|
472
473
|
function DetailsPanelFrame(props) {
|
|
473
|
-
return (_jsxs(Box, {
|
|
474
|
+
return (_jsxs(Box, { borderStyle: "round", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Details" }), props.children] }));
|
|
474
475
|
}
|
|
475
476
|
function DetailRow(props) {
|
|
476
|
-
|
|
477
|
+
const labelText = props.noSlice
|
|
478
|
+
? props.label.padEnd(props.padLength ?? 14)
|
|
479
|
+
: pad(props.label, props.padLength ?? 14);
|
|
480
|
+
return (_jsxs(Text, { children: [labelText, props.value] }));
|
|
477
481
|
}
|
|
478
482
|
function UsageTotalsDetails(props) {
|
|
479
483
|
const { totals } = props;
|
|
@@ -495,8 +499,8 @@ function formatOverviewTokenCount(value) {
|
|
|
495
499
|
}
|
|
496
500
|
return formatInteger(roundedValue);
|
|
497
501
|
}
|
|
498
|
-
function formatCacheOverviewTokenCount(
|
|
499
|
-
return
|
|
502
|
+
function formatCacheOverviewTokenCount(status, value) {
|
|
503
|
+
return status === "unavailable" ? "-" : formatOverviewTokenCount(value);
|
|
500
504
|
}
|
|
501
505
|
function formatFixedCompactNumber(value) {
|
|
502
506
|
return value.toLocaleString("en-US", {
|
|
@@ -580,7 +584,7 @@ function formatPercent(value) {
|
|
|
580
584
|
})}%`;
|
|
581
585
|
}
|
|
582
586
|
function resolveCacheRatio(totals) {
|
|
583
|
-
if (totals.
|
|
587
|
+
if (totals.cacheReadStatus === "unavailable") {
|
|
584
588
|
return NaN;
|
|
585
589
|
}
|
|
586
590
|
const inputPool = totals.inputTokens +
|
|
@@ -590,14 +594,32 @@ function resolveCacheRatio(totals) {
|
|
|
590
594
|
? (totals.cacheReadInputTokens / inputPool) * 100
|
|
591
595
|
: 0;
|
|
592
596
|
}
|
|
593
|
-
function formatInputOutputRatio(totals) {
|
|
597
|
+
function formatInputOutputRatio(totals, providerId) {
|
|
594
598
|
if (totals.outputTokens <= 0) {
|
|
595
|
-
return
|
|
599
|
+
return {
|
|
600
|
+
label: providerId === "claude" ? "Input/Write/Read/W5m/W1h/Output" : "Input/ReadCache/Output",
|
|
601
|
+
value: "-"
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const fmt = (val) => {
|
|
605
|
+
const ratio = val / totals.outputTokens;
|
|
606
|
+
return ratio.toLocaleString("en-US", {
|
|
607
|
+
maximumFractionDigits: 1,
|
|
608
|
+
minimumFractionDigits: 0
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
if (providerId === "claude") {
|
|
612
|
+
return {
|
|
613
|
+
label: "Input/Write/Read/W5m/W1h/Output",
|
|
614
|
+
value: `${fmt(totals.inputTokens)} : ${fmt(totals.cacheWriteInputTokens)} : ${fmt(totals.cacheReadInputTokens)} : ${fmt(totals.cacheWrite5mInputTokens)} : ${fmt(totals.cacheWrite1hInputTokens)} : 1`
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
return {
|
|
619
|
+
label: "Input/ReadCache/Output",
|
|
620
|
+
value: `${fmt(totals.inputTokens)} : ${fmt(totals.cacheReadInputTokens)} : 1`
|
|
621
|
+
};
|
|
596
622
|
}
|
|
597
|
-
return `${(totals.inputTokens / totals.outputTokens).toLocaleString("en-US", {
|
|
598
|
-
maximumFractionDigits: 1,
|
|
599
|
-
minimumFractionDigits: 0
|
|
600
|
-
})} : 1`;
|
|
601
623
|
}
|
|
602
624
|
function formatUsedPercentRange(minUsedPercent, maxUsedPercent) {
|
|
603
625
|
const fmt = (v) => `${Math.round(v)}%`;
|
|
@@ -619,6 +641,13 @@ function formatCompactWindowMinutes(value) {
|
|
|
619
641
|
}
|
|
620
642
|
return `${formatCompactNumber(hours)}h`;
|
|
621
643
|
}
|
|
644
|
+
function formatLimitWindowModels(window) {
|
|
645
|
+
const label = window.modelType?.trim();
|
|
646
|
+
if (!label) {
|
|
647
|
+
return "All";
|
|
648
|
+
}
|
|
649
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
650
|
+
}
|
|
622
651
|
function formatLocalDateTime(value) {
|
|
623
652
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
624
653
|
month: "short",
|
|
@@ -683,23 +712,23 @@ function formatModelUsageTableCells(modelId, totals) {
|
|
|
683
712
|
displayModelId,
|
|
684
713
|
formatCompactTokenCount(totals.inputTokens),
|
|
685
714
|
formatCompactTokenCount(totals.outputTokens),
|
|
686
|
-
formatCompactCacheTokens(totals, totals.cacheReadInputTokens),
|
|
687
|
-
formatCompactCacheTokens(totals, totals.cacheWriteInputTokens),
|
|
715
|
+
formatCompactCacheTokens(totals.cacheReadStatus, totals.cacheReadInputTokens),
|
|
716
|
+
formatCompactCacheTokens(totals.cacheWriteStatus, totals.cacheWriteInputTokens),
|
|
688
717
|
formatUsageUsd(totals, modelId)
|
|
689
718
|
];
|
|
690
719
|
}
|
|
691
720
|
function UsageBreakdownLines(props) {
|
|
692
721
|
const { totals } = props;
|
|
693
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input: ", formatInteger(totals.inputTokens), " output: ", formatInteger(totals.outputTokens), " cacheRead: ", formatCacheTokens(totals, totals.cacheReadInputTokens)] }), _jsxs(Text, { children: ["cacheWrite: ", formatCacheTokens(totals, totals.cacheWriteInputTokens), " cacheW5m: ", formatOptionalTokens(totals.cacheWrite5mInputTokens), " cacheW1h: ", formatOptionalTokens(totals.cacheWrite1hInputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
|
|
722
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["input: ", formatInteger(totals.inputTokens), " output: ", formatInteger(totals.outputTokens), " cacheRead: ", formatCacheTokens(totals.cacheReadStatus, totals.cacheReadInputTokens)] }), _jsxs(Text, { children: ["cacheWrite: ", formatCacheTokens(totals.cacheWriteStatus, totals.cacheWriteInputTokens), " cacheW5m: ", formatOptionalTokens(totals.cacheWrite5mInputTokens), " cacheW1h: ", formatOptionalTokens(totals.cacheWrite1hInputTokens), " reasoning: ", formatInteger(totals.reasoningOutputTokens), " total: ", formatInteger(totals.totalTokens)] })] }));
|
|
694
723
|
}
|
|
695
|
-
function formatCacheTokens(
|
|
696
|
-
if (
|
|
724
|
+
function formatCacheTokens(status, value) {
|
|
725
|
+
if (status === "unavailable") {
|
|
697
726
|
return "-";
|
|
698
727
|
}
|
|
699
728
|
return formatOptionalTokens(value);
|
|
700
729
|
}
|
|
701
|
-
function formatCompactCacheTokens(
|
|
702
|
-
if (
|
|
730
|
+
function formatCompactCacheTokens(status, value) {
|
|
731
|
+
if (status === "unavailable") {
|
|
703
732
|
return "-";
|
|
704
733
|
}
|
|
705
734
|
return formatOptionalCompactTokens(value);
|
|
@@ -714,10 +743,14 @@ function formatInputPerOutput(totals) {
|
|
|
714
743
|
if (totals.outputTokens <= 0) {
|
|
715
744
|
return "input:cacheRead:cacheWrite:output = 0:0:0:0";
|
|
716
745
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
746
|
+
const inputRatio = formatInteger(Math.round(totals.inputTokens / totals.outputTokens));
|
|
747
|
+
const cacheReadRatio = totals.cacheReadStatus === "unavailable"
|
|
748
|
+
? "-"
|
|
749
|
+
: formatInteger(Math.round(totals.cacheReadInputTokens / totals.outputTokens));
|
|
750
|
+
const cacheWriteRatio = totals.cacheWriteStatus === "unavailable"
|
|
751
|
+
? "-"
|
|
752
|
+
: formatInteger(Math.round(totals.cacheWriteInputTokens / totals.outputTokens));
|
|
753
|
+
return `input:cacheRead:cacheWrite:output = ${inputRatio}:${cacheReadRatio}:${cacheWriteRatio}:1`;
|
|
721
754
|
}
|
|
722
755
|
function useMeasuredElementSize() {
|
|
723
756
|
const ref = useRef(null);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for mapping Antigravity model identifiers to canonical
|
|
3
|
+
* model ids. Covers both the opaque RPC placeholders (MODEL_PLACEHOLDER_*) and
|
|
4
|
+
* the dated/suffixed aliases that show up in usage records.
|
|
5
|
+
*/
|
|
6
|
+
const MODEL_IDS = {
|
|
7
|
+
MODEL_PLACEHOLDER_M20: "gemini-3.5-flash",
|
|
8
|
+
MODEL_PLACEHOLDER_M132: "gemini-3.5-flash",
|
|
9
|
+
MODEL_PLACEHOLDER_M187: "gemini-3.5-flash",
|
|
10
|
+
MODEL_PLACEHOLDER_M36: "gemini-3.1-pro",
|
|
11
|
+
MODEL_PLACEHOLDER_M16: "gemini-3.1-pro",
|
|
12
|
+
MODEL_PLACEHOLDER_M35: "claude-sonnet-4-6",
|
|
13
|
+
MODEL_PLACEHOLDER_M26: "claude-opus-4-6",
|
|
14
|
+
MODEL_OPENAI_GPT_OSS_120B_MEDIUM: "gpt-oss-120b",
|
|
15
|
+
"gemini-3-flash-a": "gemini-3-flash",
|
|
16
|
+
"gemini-3-flash-preview": "gemini-3-flash",
|
|
17
|
+
"gemini-3.1-pro-preview": "gemini-3.1-pro",
|
|
18
|
+
"gemini-3.5-flash-preview": "gemini-3.5-flash",
|
|
19
|
+
"claude-sonnet-4-6-20251201": "claude-sonnet-4-6",
|
|
20
|
+
"claude-opus-4-6-20251201": "claude-opus-4-6"
|
|
21
|
+
};
|
|
22
|
+
export function normalizeAntigravityModelId(modelId) {
|
|
23
|
+
return MODEL_IDS[modelId] ?? (modelId || "unknown");
|
|
24
|
+
}
|
|
25
|
+
export function antigravityModelScope(rawModelId) {
|
|
26
|
+
const modelId = normalizeAntigravityModelId(rawModelId);
|
|
27
|
+
if (modelId.startsWith("gemini")) {
|
|
28
|
+
return "gemini";
|
|
29
|
+
}
|
|
30
|
+
if (modelId.startsWith("claude") || modelId.startsWith("gpt")) {
|
|
31
|
+
return "third-party";
|
|
32
|
+
}
|
|
33
|
+
return "unknown";
|
|
34
|
+
}
|
|
35
|
+
export function modelScopeMatches(scope, modelId) {
|
|
36
|
+
return antigravityModelScope(modelId) === scope;
|
|
37
|
+
}
|
|
38
|
+
export function modelScopeLabel(scope) {
|
|
39
|
+
if (scope === "gemini") {
|
|
40
|
+
return "Gemini";
|
|
41
|
+
}
|
|
42
|
+
if (scope === "third-party") {
|
|
43
|
+
return "Claude/GPT";
|
|
44
|
+
}
|
|
45
|
+
return "Unknown";
|
|
46
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "../contract.js";
|
|
3
|
+
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../daily.js";
|
|
4
|
+
import { resolveUsageRate } from "../pricing.js";
|
|
5
|
+
import { modelScopeLabel, modelScopeMatches, normalizeAntigravityModelId } from "./models.js";
|
|
6
|
+
import { parseAntigravityQuotaEntries } from "./quota-parser.js";
|
|
7
|
+
import { findAntigravityLocalServer } from "./rpc/discovery.js";
|
|
8
|
+
import { extractQuotaGroups, fetchAntigravityUserStatus } from "./rpc/quota.js";
|
|
9
|
+
import { collectUsageFromLocalRpc } from "./usage-parse.js";
|
|
10
|
+
const RATE_CARD = {
|
|
11
|
+
"gemini-3.5-flash": {
|
|
12
|
+
input: 150,
|
|
13
|
+
cacheRead: 15,
|
|
14
|
+
cacheWrite: 150,
|
|
15
|
+
cacheWrite5m: 150,
|
|
16
|
+
cacheWrite1h: 150,
|
|
17
|
+
output: 900
|
|
18
|
+
},
|
|
19
|
+
"gemini-3.1-pro": {
|
|
20
|
+
input: 200,
|
|
21
|
+
cacheRead: 20,
|
|
22
|
+
cacheWrite: 200,
|
|
23
|
+
cacheWrite5m: 200,
|
|
24
|
+
cacheWrite1h: 200,
|
|
25
|
+
output: 1200,
|
|
26
|
+
longContext: {
|
|
27
|
+
thresholdTokens: 200000,
|
|
28
|
+
rate: {
|
|
29
|
+
input: 400,
|
|
30
|
+
cacheRead: 40,
|
|
31
|
+
cacheWrite: 400,
|
|
32
|
+
cacheWrite5m: 400,
|
|
33
|
+
cacheWrite1h: 400,
|
|
34
|
+
output: 1800
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"gemini-3-flash": {
|
|
39
|
+
input: 50,
|
|
40
|
+
cacheRead: 5,
|
|
41
|
+
cacheWrite: 50,
|
|
42
|
+
cacheWrite5m: 50,
|
|
43
|
+
cacheWrite1h: 50,
|
|
44
|
+
output: 300
|
|
45
|
+
},
|
|
46
|
+
"claude-sonnet-4-6": {
|
|
47
|
+
input: 300,
|
|
48
|
+
cacheRead: 30,
|
|
49
|
+
cacheWrite: 375,
|
|
50
|
+
cacheWrite5m: 375,
|
|
51
|
+
cacheWrite1h: 600,
|
|
52
|
+
output: 1500
|
|
53
|
+
},
|
|
54
|
+
"claude-opus-4-6": {
|
|
55
|
+
input: 500,
|
|
56
|
+
cacheRead: 50,
|
|
57
|
+
cacheWrite: 625,
|
|
58
|
+
cacheWrite5m: 625,
|
|
59
|
+
cacheWrite1h: 1000,
|
|
60
|
+
output: 2500
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const UNPRICED_MODELS = new Set([
|
|
64
|
+
"gpt-oss-120b"
|
|
65
|
+
]);
|
|
66
|
+
export class AntigravityUsageProvider extends UsageProviderBase {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
super("antigravity", "Antigravity");
|
|
69
|
+
this.collectUsageOverride = options.collectUsage;
|
|
70
|
+
this.collectQuotaOverride = options.collectQuota;
|
|
71
|
+
this.findConnection = options.findConnection ?? findAntigravityLocalServer;
|
|
72
|
+
}
|
|
73
|
+
async getStats(options = {}) {
|
|
74
|
+
const warnings = [];
|
|
75
|
+
// Discover the local language server at most once per refresh and share the
|
|
76
|
+
// resulting connection (and its probe payload) between both collectors.
|
|
77
|
+
let connectionPromise;
|
|
78
|
+
const connect = () => (connectionPromise ?? (connectionPromise = this.findConnection()));
|
|
79
|
+
const [usageResult, quotaResult] = await Promise.allSettled([
|
|
80
|
+
this.collectUsageOverride
|
|
81
|
+
? this.collectUsageOverride(options)
|
|
82
|
+
: collectUsageFromConnection(connect, options),
|
|
83
|
+
this.collectQuotaOverride
|
|
84
|
+
? this.collectQuotaOverride()
|
|
85
|
+
: collectQuotaFromConnection(connect)
|
|
86
|
+
]);
|
|
87
|
+
const records = usageResult.status === "fulfilled"
|
|
88
|
+
? usageResult.value
|
|
89
|
+
: [];
|
|
90
|
+
const quotaSnapshot = quotaResult.status === "fulfilled"
|
|
91
|
+
? quotaResult.value
|
|
92
|
+
: null;
|
|
93
|
+
if (usageResult.status === "rejected") {
|
|
94
|
+
warnings.push("Could not read Antigravity usage from the local RPC.");
|
|
95
|
+
}
|
|
96
|
+
if (quotaResult.status === "rejected") {
|
|
97
|
+
warnings.push("Live Antigravity quota is unavailable. Ensure the Antigravity IDE is running.");
|
|
98
|
+
}
|
|
99
|
+
else if (quotaResult.value.entries.length === 0) {
|
|
100
|
+
warnings.push("Antigravity local quota RPC responded, but no recognized model quota windows were found.");
|
|
101
|
+
}
|
|
102
|
+
const selectedRecords = deduplicateRecords(records);
|
|
103
|
+
const duplicateEvents = records.length - selectedRecords.length;
|
|
104
|
+
if (duplicateEvents > 0) {
|
|
105
|
+
warnings.push(`Collapsed ${duplicateEvents} duplicate Antigravity usage response(s).`);
|
|
106
|
+
}
|
|
107
|
+
const byModel = new Map();
|
|
108
|
+
const byDay = createDailyUsageAggregates();
|
|
109
|
+
for (const record of selectedRecords) {
|
|
110
|
+
const modelId = normalizeAntigravityModelId(record.modelId);
|
|
111
|
+
const totals = usageRecordToTotals(modelId, record);
|
|
112
|
+
addModelUsage(byModel, modelId, totals);
|
|
113
|
+
addDailyUsage(byDay, record.timestamp, modelId, undefined, totals);
|
|
114
|
+
}
|
|
115
|
+
const modelUsage = [...byModel.entries()]
|
|
116
|
+
.map(([modelId, totals]) => ({
|
|
117
|
+
modelId,
|
|
118
|
+
totals
|
|
119
|
+
}))
|
|
120
|
+
.sort((left, right) => right.totals.estimatedCredits -
|
|
121
|
+
left.totals.estimatedCredits);
|
|
122
|
+
const unknownPricedModels = modelUsage
|
|
123
|
+
.filter((row) => !rateForModel(row.modelId, rowInputTokens(row)) && !UNPRICED_MODELS.has(row.modelId))
|
|
124
|
+
.map((row) => row.modelId);
|
|
125
|
+
if (unknownPricedModels.length > 0) {
|
|
126
|
+
warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
127
|
+
}
|
|
128
|
+
const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
|
|
129
|
+
return {
|
|
130
|
+
providerId: this.id,
|
|
131
|
+
providerLabel: this.label,
|
|
132
|
+
summary: {
|
|
133
|
+
// The provider reads no files or lines; usage comes from the local RPC.
|
|
134
|
+
filesScanned: 0,
|
|
135
|
+
linesRead: 0,
|
|
136
|
+
tokenEvents: selectedRecords.length,
|
|
137
|
+
totals: sumUsageTotals(modelUsage.map((row) => row.totals)),
|
|
138
|
+
distinctModels: modelUsage.map((row) => row.modelId),
|
|
139
|
+
distinctPlanTypes: [
|
|
140
|
+
...new Set(limitWindows.map((window) => window.planType))
|
|
141
|
+
],
|
|
142
|
+
rootLabel: "Antigravity local RPC",
|
|
143
|
+
rootPath: "127.0.0.1"
|
|
144
|
+
},
|
|
145
|
+
modelUsage,
|
|
146
|
+
dayUsage: buildDailyUsageRows(byDay),
|
|
147
|
+
primaryLimitWindows: limitWindows.filter((window) => window.scope === "primary"),
|
|
148
|
+
secondaryLimitWindows: limitWindows.filter((window) => window.scope === "secondary"),
|
|
149
|
+
warnings,
|
|
150
|
+
analytics: quotaSnapshot?.userIdHash
|
|
151
|
+
? {
|
|
152
|
+
agentName: this.label.replace(/\s/g, ""),
|
|
153
|
+
userIdHash: quotaSnapshot.userIdHash
|
|
154
|
+
}
|
|
155
|
+
: undefined
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function collectUsageFromConnection(connect, options) {
|
|
160
|
+
const connection = await connect();
|
|
161
|
+
return connection
|
|
162
|
+
? collectUsageFromLocalRpc(connection.server, options)
|
|
163
|
+
: [];
|
|
164
|
+
}
|
|
165
|
+
async function collectQuotaFromConnection(connect) {
|
|
166
|
+
const connection = await connect();
|
|
167
|
+
if (!connection) {
|
|
168
|
+
throw new Error("Antigravity local language server was not found.");
|
|
169
|
+
}
|
|
170
|
+
const status = await fetchAntigravityUserStatus(connection.server);
|
|
171
|
+
return {
|
|
172
|
+
entries: parseAntigravityQuotaEntries(extractQuotaGroups(connection.quotaSummary)),
|
|
173
|
+
fetchedAt: Date.now(),
|
|
174
|
+
planType: status.planName ?? "unknown",
|
|
175
|
+
userIdHash: status.email
|
|
176
|
+
? createHash("md5").update(status.email).digest("hex")
|
|
177
|
+
: null
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
|
|
181
|
+
const startAt = quota.resetAt - quota.windowMinutes * 60000;
|
|
182
|
+
const byModel = new Map();
|
|
183
|
+
const matchingTimestamps = [];
|
|
184
|
+
for (const record of records) {
|
|
185
|
+
const modelId = normalizeAntigravityModelId(record.modelId);
|
|
186
|
+
if (record.timestamp < startAt ||
|
|
187
|
+
record.timestamp >= quota.resetAt ||
|
|
188
|
+
!modelScopeMatches(quota.modelScope, modelId)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
matchingTimestamps.push(record.timestamp);
|
|
192
|
+
addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
|
|
193
|
+
}
|
|
194
|
+
const modelUsage = [...byModel.entries()]
|
|
195
|
+
.map(([modelId, totals]) => ({
|
|
196
|
+
modelId,
|
|
197
|
+
totals
|
|
198
|
+
}))
|
|
199
|
+
.sort((left, right) => right.totals.estimatedCredits -
|
|
200
|
+
left.totals.estimatedCredits);
|
|
201
|
+
const totals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
202
|
+
const usedPercent = clampPercent((1 - quota.remainingFraction) * 100);
|
|
203
|
+
// The first/last-seen range reflects the matched local usage events inside
|
|
204
|
+
// the window. With no matches, fall back to the window start and the fetch
|
|
205
|
+
// time. Quota percentage is authoritative from Antigravity RPC; token totals
|
|
206
|
+
// are reconstructed locally and may not match its internal accounting exactly.
|
|
207
|
+
const firstSeenMs = matchingTimestamps.length
|
|
208
|
+
? Math.min(...matchingTimestamps)
|
|
209
|
+
: startAt;
|
|
210
|
+
const lastSeenMs = matchingTimestamps.length
|
|
211
|
+
? Math.max(...matchingTimestamps)
|
|
212
|
+
: fetchedAt;
|
|
213
|
+
return {
|
|
214
|
+
scope: quota.scope,
|
|
215
|
+
planType,
|
|
216
|
+
limitId: quota.limitId,
|
|
217
|
+
modelType: modelScopeLabel(quota.modelScope),
|
|
218
|
+
windowMinutes: quota.windowMinutes,
|
|
219
|
+
startTimeUtcIso: new Date(startAt).toISOString(),
|
|
220
|
+
endTimeUtcIso: new Date(quota.resetAt).toISOString(),
|
|
221
|
+
firstSeenUtcIso: new Date(firstSeenMs).toISOString(),
|
|
222
|
+
lastSeenUtcIso: new Date(lastSeenMs).toISOString(),
|
|
223
|
+
minUsedPercent: usedPercent,
|
|
224
|
+
maxUsedPercent: usedPercent,
|
|
225
|
+
totals,
|
|
226
|
+
modelUsage,
|
|
227
|
+
eventCount: totals.eventCount
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function clampPercent(value) {
|
|
231
|
+
if (!Number.isFinite(value)) {
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
return Math.min(100, Math.max(0, value));
|
|
235
|
+
}
|
|
236
|
+
function deduplicateRecords(records) {
|
|
237
|
+
const byKey = new Map();
|
|
238
|
+
for (const record of records) {
|
|
239
|
+
byKey.set(`${record.sessionId}:${record.responseId}`, record);
|
|
240
|
+
}
|
|
241
|
+
return [...byKey.values()];
|
|
242
|
+
}
|
|
243
|
+
function usageRecordToTotals(modelId, record) {
|
|
244
|
+
return {
|
|
245
|
+
inputTokens: record.input,
|
|
246
|
+
outputTokens: record.output,
|
|
247
|
+
cacheReadInputTokens: record.cacheRead,
|
|
248
|
+
cacheWriteInputTokens: record.cacheWrite,
|
|
249
|
+
cacheWrite5mInputTokens: 0,
|
|
250
|
+
cacheWrite1hInputTokens: 0,
|
|
251
|
+
reasoningOutputTokens: Math.min(record.reasoning, record.output),
|
|
252
|
+
totalTokens: record.input +
|
|
253
|
+
record.cacheRead +
|
|
254
|
+
record.cacheWrite +
|
|
255
|
+
record.output,
|
|
256
|
+
estimatedCredits: creditsFor(modelId, record),
|
|
257
|
+
eventCount: 1,
|
|
258
|
+
// The local RPC reports cache reads but never cache writes, so cache reads
|
|
259
|
+
// are accurate while cache writes are genuinely unknown (not a confirmed
|
|
260
|
+
// zero) — surfaced as "-" everywhere, including the input/output ratio.
|
|
261
|
+
cacheReadStatus: "known",
|
|
262
|
+
cacheWriteStatus: "unavailable",
|
|
263
|
+
estimatedCreditsStatus: rateForModel(modelId, record.input)
|
|
264
|
+
? "known"
|
|
265
|
+
: "unavailable"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function creditsFor(modelId, record) {
|
|
269
|
+
const rate = rateForModel(modelId, record.input);
|
|
270
|
+
if (!rate) {
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
return ((record.input / 1000000) * rate.input +
|
|
274
|
+
(record.cacheRead / 1000000) * rate.cacheRead +
|
|
275
|
+
(record.cacheWrite / 1000000) * rate.cacheWrite +
|
|
276
|
+
(record.output / 1000000) * rate.output);
|
|
277
|
+
}
|
|
278
|
+
function rateForModel(modelId, inputTokens) {
|
|
279
|
+
return resolveUsageRate(RATE_CARD, modelId, inputTokens);
|
|
280
|
+
}
|
|
281
|
+
function rowInputTokens(row) {
|
|
282
|
+
return row.totals.inputTokens + row.totals.cacheReadInputTokens + row.totals.cacheWriteInputTokens;
|
|
283
|
+
}
|
|
284
|
+
function addModelUsage(byModel, modelId, deltaTotals) {
|
|
285
|
+
const totals = byModel.get(modelId) ?? createEmptyUsageTotals();
|
|
286
|
+
addUsageTotals(totals, deltaTotals);
|
|
287
|
+
byModel.set(modelId, totals);
|
|
288
|
+
}
|