letmecode 0.1.22 → 0.1.24
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/LICENSE +21 -0
- package/README.md +29 -10
- package/ink-app/dist/cli-options.js +13 -3
- package/ink-app/dist/index.js +44 -7
- package/ink-app/dist/providers/antigravity/provider.js +16 -5
- package/ink-app/dist/providers/claude.js +15 -8
- package/ink-app/dist/providers/codex.js +12 -5
- package/ink-app/dist/providers/limits.js +32 -1
- package/ink-app/dist/reporting.js +4 -2
- package/package.json +22 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devforth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,32 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# LetMeCode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Terminal AI real-money value usage dashboard for Codex, Claude, Copilot, and Antigravity.
|
|
4
|
+
|
|
5
|
+
See your real $ usage data, inspect limits, see daily activity, and model-level token totals in a terminal UI.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npx -y letmecode@latest
|
|
7
11
|
```
|
|
8
12
|
|
|
13
|
+
## Privacy and anonymous reporting
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<img width="2301" height="1397" alt="image" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
|
|
15
|
+
By default, `letmecode` sends an anonymous usage summary which powers [aggregated real-world plans comparison](https://devforth.io/agents-for-code/).
|
|
13
16
|
|
|
17
|
+
The report includes aggregated limit-window percentages, token counts, plan/window metadata, the `letmecode` version, and a hashed user identifier when a provider exposes one. It does not send prompt content, usernames, email addresses, company names, or other directly identifiable personal information.
|
|
14
18
|
|
|
19
|
+
To disable anonymous usage reporting:
|
|
15
20
|
|
|
21
|
+
```bash
|
|
22
|
+
npx -y letmecode@latest -- --no-usage
|
|
23
|
+
```
|
|
16
24
|
|
|
17
|
-
##
|
|
25
|
+
## Options
|
|
18
26
|
|
|
27
|
+
```bash
|
|
28
|
+
npx -y letmecode@latest -- --help
|
|
29
|
+
npx -y letmecode@latest -- --log-to ./letmecode.log
|
|
19
30
|
```
|
|
20
|
-
npx -y letmecode -- -h
|
|
21
|
-
npx -y letmecode -- --log-to log.txt
|
|
22
|
-
```
|
|
23
31
|
|
|
24
|
-
`--log-to`
|
|
32
|
+
`--log-to` records provider discovery details and raw usage parsing diagnostics so empty or unexpected windows are easier to debug.
|
|
33
|
+
|
|
34
|
+
## Providers
|
|
35
|
+
|
|
36
|
+
- Codex
|
|
37
|
+
- Claude
|
|
38
|
+
- Copilot
|
|
39
|
+
- Antigravity
|
|
40
|
+
|
|
41
|
+
## Preview
|
|
25
42
|
|
|
43
|
+
<img width="2301" height="1397" alt="letmecode preview" src="https://github.com/user-attachments/assets/c37d6847-9926-4977-8592-5cab8346a86f" />
|
|
26
44
|
|
|
27
45
|
## Local development
|
|
28
46
|
|
|
29
47
|
```bash
|
|
30
48
|
pnpm install
|
|
49
|
+
pnpm test
|
|
31
50
|
pnpm start
|
|
32
51
|
```
|
|
@@ -4,6 +4,7 @@ export function parseCliOptions(argv) {
|
|
|
4
4
|
let showHelp = false;
|
|
5
5
|
let verbose = false;
|
|
6
6
|
let logToPath;
|
|
7
|
+
let enableAnonymousUsageReporting = true;
|
|
7
8
|
for (let index = 0; index < argv.length; index += 1) {
|
|
8
9
|
const argument = argv[index] ?? "";
|
|
9
10
|
if (argument === "-h" || argument === "--help") {
|
|
@@ -29,9 +30,13 @@ export function parseCliOptions(argv) {
|
|
|
29
30
|
throw new Error("Expected a file path after --log-to=.");
|
|
30
31
|
}
|
|
31
32
|
logToPath = value;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (argument === "--no-usage") {
|
|
36
|
+
enableAnonymousUsageReporting = false;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
|
-
return { showHelp, verbose, logToPath };
|
|
39
|
+
return { showHelp, verbose, logToPath, enableAnonymousUsageReporting };
|
|
35
40
|
}
|
|
36
41
|
export function buildProviderStatsOptions(options) {
|
|
37
42
|
return {
|
|
@@ -41,7 +46,7 @@ export function buildProviderStatsOptions(options) {
|
|
|
41
46
|
}
|
|
42
47
|
export function buildHelpText() {
|
|
43
48
|
return [
|
|
44
|
-
"letmecode -
|
|
49
|
+
"letmecode - terminal AI usage dashboard",
|
|
45
50
|
"",
|
|
46
51
|
"Usage:",
|
|
47
52
|
" letmecode [options]",
|
|
@@ -50,6 +55,7 @@ export function buildHelpText() {
|
|
|
50
55
|
" -h, --help Show this help and exit",
|
|
51
56
|
" -v, --verbose Show extra provider warnings",
|
|
52
57
|
" --log-to PATH Write trace logs to PATH",
|
|
58
|
+
" --no-usage Disable anonymous usage reporting",
|
|
53
59
|
"",
|
|
54
60
|
"Controls:",
|
|
55
61
|
" [ ] / Tab Switch providers",
|
|
@@ -64,7 +70,11 @@ export function buildHelpText() {
|
|
|
64
70
|
" --log-to PATH writes Claude detection details,",
|
|
65
71
|
" session root selection, parsed session file summaries, aggregated usage selection,",
|
|
66
72
|
" every candidate binary path check, the final found/not-found result,",
|
|
67
|
-
" and the raw /usage command output plus live window matching details."
|
|
73
|
+
" and the raw /usage command output plus live window matching details.",
|
|
74
|
+
"",
|
|
75
|
+
"Anonymous reporting:",
|
|
76
|
+
" Enabled by default. Use --no-usage to disable the best-effort",
|
|
77
|
+
" anonymous usage summary upload."
|
|
68
78
|
].join("\n");
|
|
69
79
|
}
|
|
70
80
|
export function createFileTraceLogger(logPath) {
|
package/ink-app/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout, rende
|
|
|
4
4
|
import { buildHelpText, buildProviderStatsOptions, parseCliOptions } from "./cli-options.js";
|
|
5
5
|
import { configureCopilotVsCodeLogging, createProviders } from "./providers/index.js";
|
|
6
6
|
import { reportAnonymousUsage } from "./reporting.js";
|
|
7
|
+
import { estimateLimitFullValue } from "./providers/limits.js";
|
|
7
8
|
const ESC = String.fromCharCode(0x1b);
|
|
8
9
|
// Normal mouse tracking (button press/release only) + SGR extended coordinates.
|
|
9
10
|
// This makes the tabs clickable while leaving Shift+drag native text selection
|
|
@@ -19,7 +20,7 @@ const DETAIL_TABS = [
|
|
|
19
20
|
{ id: "usage-by-model", label: "Models" }
|
|
20
21
|
];
|
|
21
22
|
const CODEX_CREDIT_COST_USD = 0.01;
|
|
22
|
-
const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq."];
|
|
23
|
+
const LIMIT_TABLE_HEADERS = ["Scope", "Plan", "Models", "Window", "Used", "Start", "End", "API eq.", "Full"];
|
|
23
24
|
const DAILY_TABLE_HEADERS = ["Day", "Ev", "Input", "Output", "C read", "C write", "API eq."];
|
|
24
25
|
const MODEL_TABLE_HEADERS = ["Model", "Input", "Output", "C read", "C write", "API eq."];
|
|
25
26
|
const COPILOT_ACTIONS = [
|
|
@@ -96,7 +97,9 @@ function App(props) {
|
|
|
96
97
|
setSelectedProviderId(topProvider.provider.id);
|
|
97
98
|
}, [hasUserSelectedProvider, providerStates, sortedProviderStates]);
|
|
98
99
|
useEffect(() => {
|
|
99
|
-
if (
|
|
100
|
+
if (!props.usageReportingEnabled ||
|
|
101
|
+
hasReportedAnonymousUsageRef.current ||
|
|
102
|
+
providerStates.some((state) => state.status === "loading")) {
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
105
|
hasReportedAnonymousUsageRef.current = true;
|
|
@@ -106,7 +109,7 @@ function App(props) {
|
|
|
106
109
|
void reportAnonymousUsage(readyStats).catch(() => {
|
|
107
110
|
// Anonymous usage reporting is best-effort and must never disturb the TUI.
|
|
108
111
|
});
|
|
109
|
-
}, [providerStates]);
|
|
112
|
+
}, [props.usageReportingEnabled, providerStates]);
|
|
110
113
|
useMouseClick((click) => {
|
|
111
114
|
const regionId = resolveClick(click);
|
|
112
115
|
if (!regionId) {
|
|
@@ -415,7 +418,10 @@ function buildLimitWindowTableRow(window) {
|
|
|
415
418
|
formatCompactLocalDateTime(window.endTimeUtcIso),
|
|
416
419
|
// Status-aware: shows "-" when the API-equivalent cost is unknown rather
|
|
417
420
|
// than a misleading $0.00.
|
|
418
|
-
formatUsageUsd(window.totals)
|
|
421
|
+
formatUsageUsd(window.totals),
|
|
422
|
+
// Extrapolated full value of the limit, rounded to a single figure here;
|
|
423
|
+
// the details panel shows the unrounded ±1% range.
|
|
424
|
+
formatLimitFullValueCompact(window.totals, window.maxUsedPercent)
|
|
419
425
|
]
|
|
420
426
|
};
|
|
421
427
|
}
|
|
@@ -459,7 +465,7 @@ function SelectionDetailsPanel(props) {
|
|
|
459
465
|
}
|
|
460
466
|
if (props.tabId === "limit-windows" && props.selectedLimitRow) {
|
|
461
467
|
const row = props.selectedLimitRow;
|
|
462
|
-
return (
|
|
468
|
+
return (_jsxs(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) })] }), _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) })] })] }), _jsx(DetailRow, { label: "API eq.", value: formatUsageUsd(row.totals), note: "API equivalent cost" }), _jsx(DetailRow, { label: "Full value", value: formatLimitFullValueRange(row.totals, row.maxUsedPercent), note: "API equivalent cost if 100% will be used" })] }));
|
|
463
469
|
}
|
|
464
470
|
if (props.tabId === "day-to-day-analyses" && props.selectedDayRow) {
|
|
465
471
|
const row = props.selectedDayRow;
|
|
@@ -477,7 +483,7 @@ function DetailRow(props) {
|
|
|
477
483
|
const labelText = props.noSlice
|
|
478
484
|
? props.label.padEnd(props.padLength ?? 14)
|
|
479
485
|
: pad(props.label, props.padLength ?? 14);
|
|
480
|
-
return (_jsxs(Text, { children: [labelText, props.value] }));
|
|
486
|
+
return (_jsxs(Text, { children: [labelText, props.value, props.note ? _jsx(Text, { color: "gray", children: ` ${props.note}` }) : null] }));
|
|
481
487
|
}
|
|
482
488
|
function UsageTotalsDetails(props) {
|
|
483
489
|
const { totals } = props;
|
|
@@ -563,6 +569,37 @@ function formatUsd(value) {
|
|
|
563
569
|
maximumFractionDigits: 2
|
|
564
570
|
});
|
|
565
571
|
}
|
|
572
|
+
function formatUsdWhole(value) {
|
|
573
|
+
return Math.round(value).toLocaleString("en-US", {
|
|
574
|
+
currency: "USD",
|
|
575
|
+
style: "currency",
|
|
576
|
+
minimumFractionDigits: 0,
|
|
577
|
+
maximumFractionDigits: 0
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
// Extrapolate the limit's full USD value from the observed API-equivalent cost
|
|
581
|
+
// and how much of the limit it represents. Uses the highest reported percent
|
|
582
|
+
// (the latest cumulative usage) and returns "-" when the cost is unknown or the
|
|
583
|
+
// percent is missing.
|
|
584
|
+
function limitFullValueUsd(totals, usedPercent) {
|
|
585
|
+
if (totals.estimatedCreditsStatus === "unavailable") {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
const usedUsd = totals.estimatedCredits * CODEX_CREDIT_COST_USD;
|
|
589
|
+
return estimateLimitFullValue(usedUsd, usedPercent);
|
|
590
|
+
}
|
|
591
|
+
function formatLimitFullValueCompact(totals, usedPercent) {
|
|
592
|
+
const estimate = limitFullValueUsd(totals, usedPercent);
|
|
593
|
+
return estimate ? formatUsdWhole(estimate.point) : "-";
|
|
594
|
+
}
|
|
595
|
+
function formatLimitFullValueRange(totals, usedPercent) {
|
|
596
|
+
const estimate = limitFullValueUsd(totals, usedPercent);
|
|
597
|
+
if (!estimate) {
|
|
598
|
+
return "-";
|
|
599
|
+
}
|
|
600
|
+
const low = formatUsd(estimate.low);
|
|
601
|
+
return Number.isFinite(estimate.high) ? `${low} – ${formatUsd(estimate.high)}` : `≥ ${low}`;
|
|
602
|
+
}
|
|
566
603
|
function formatUnitUsd(value) {
|
|
567
604
|
if (!Number.isFinite(value)) {
|
|
568
605
|
return "-";
|
|
@@ -1002,7 +1039,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
1002
1039
|
restoreFullscreen();
|
|
1003
1040
|
};
|
|
1004
1041
|
process.once("exit", exitHandler);
|
|
1005
|
-
const instance = render(_jsx(App, { statsOptions: statsOptions }), {
|
|
1042
|
+
const instance = render(_jsx(App, { statsOptions: statsOptions, usageReportingEnabled: cliOptions.enableAnonymousUsageReporting }), {
|
|
1006
1043
|
stdout: process.stdout,
|
|
1007
1044
|
stdin: process.stdin,
|
|
1008
1045
|
stderr: process.stderr
|
|
@@ -236,10 +236,20 @@ function clampPercent(value) {
|
|
|
236
236
|
function deduplicateRecords(records) {
|
|
237
237
|
const byKey = new Map();
|
|
238
238
|
for (const record of records) {
|
|
239
|
-
|
|
239
|
+
const key = `${record.sessionId}:${record.responseId}`;
|
|
240
|
+
const existing = byKey.get(key);
|
|
241
|
+
// The RPC may surface the same response more than once (e.g. progressive
|
|
242
|
+
// snapshots in unspecified order). Keep the largest coherent total rather
|
|
243
|
+
// than trusting iteration order, so we never undercount a final snapshot.
|
|
244
|
+
if (!existing || recordTokenTotal(record) > recordTokenTotal(existing)) {
|
|
245
|
+
byKey.set(key, record);
|
|
246
|
+
}
|
|
240
247
|
}
|
|
241
248
|
return [...byKey.values()];
|
|
242
249
|
}
|
|
250
|
+
function recordTokenTotal(record) {
|
|
251
|
+
return record.input + record.cacheRead + record.cacheWrite + record.output;
|
|
252
|
+
}
|
|
243
253
|
function usageRecordToTotals(modelId, record) {
|
|
244
254
|
return {
|
|
245
255
|
inputTokens: record.input,
|
|
@@ -255,11 +265,12 @@ function usageRecordToTotals(modelId, record) {
|
|
|
255
265
|
record.output,
|
|
256
266
|
estimatedCredits: creditsFor(modelId, record),
|
|
257
267
|
eventCount: 1,
|
|
258
|
-
// The local RPC reports cache reads but never cache writes, so cache
|
|
259
|
-
//
|
|
260
|
-
//
|
|
268
|
+
// The local RPC reports cache reads but never cache writes, so a zero cache
|
|
269
|
+
// write is genuinely unknown (not a confirmed zero) and is surfaced as "-".
|
|
270
|
+
// A positive value only appears when a source explicitly reports it, in
|
|
271
|
+
// which case it is both billed (see creditsFor) and shown as known.
|
|
261
272
|
cacheReadStatus: "known",
|
|
262
|
-
cacheWriteStatus: "unavailable",
|
|
273
|
+
cacheWriteStatus: record.cacheWrite > 0 ? "known" : "unavailable",
|
|
263
274
|
estimatedCreditsStatus: rateForModel(modelId, record.input)
|
|
264
275
|
? "known"
|
|
265
276
|
: "unavailable"
|
|
@@ -240,7 +240,9 @@ function creditsFor(modelId, usage, timestampMs) {
|
|
|
240
240
|
return 0;
|
|
241
241
|
}
|
|
242
242
|
const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
|
|
243
|
-
|
|
243
|
+
// The US inference surcharge must match regardless of the casing the source
|
|
244
|
+
// reports (e.g. "us", "US"), so compare case-insensitively.
|
|
245
|
+
const inferenceMultiplier = usage.inferenceGeo.trim().toLowerCase() === "us" ? 1.1 : 1;
|
|
244
246
|
return (((usage.inputTokens / 1000000) * rate.input +
|
|
245
247
|
(usage.cacheReadInputTokens / 1000000) * rate.cacheRead +
|
|
246
248
|
(cacheWriteBreakdown.cacheWrite5mInputTokens / 1000000) * rate.cacheWrite5m +
|
|
@@ -566,15 +568,20 @@ function mergeParsedUsageEvents(previous, next) {
|
|
|
566
568
|
rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
|
|
567
569
|
};
|
|
568
570
|
}
|
|
569
|
-
// Pick the snapshot
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
571
|
+
// Pick the snapshot with the latest timestamp. Same-key events are repeated/streamed snapshots
|
|
572
|
+
// of one logical request, so the most recent one reflects the final state. The one exception is
|
|
573
|
+
// a zero-usage internal <synthetic> completion marker, which is a real followup row rather than
|
|
574
|
+
// an updated snapshot and must not clobber the real usage it follows.
|
|
573
575
|
function selectMergedSnapshotEvent(previous, next) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
+
const previousIsHollowSynthetic = isInternalClaudeModel(previous.modelId) && previous.totals.totalTokens === 0;
|
|
577
|
+
const nextIsHollowSynthetic = isInternalClaudeModel(next.modelId) && next.totals.totalTokens === 0;
|
|
578
|
+
if (nextIsHollowSynthetic && !previousIsHollowSynthetic) {
|
|
579
|
+
return previous;
|
|
576
580
|
}
|
|
577
|
-
|
|
581
|
+
if (previousIsHollowSynthetic && !nextIsHollowSynthetic) {
|
|
582
|
+
return next;
|
|
583
|
+
}
|
|
584
|
+
return normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
|
|
578
585
|
}
|
|
579
586
|
function selectMergedEventModelId(primary, other) {
|
|
580
587
|
if (primary.modelId === other.modelId) {
|
|
@@ -7,10 +7,16 @@ import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTota
|
|
|
7
7
|
import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
|
|
8
8
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
|
|
9
9
|
import { resolveUsageRate } from "./pricing.js";
|
|
10
|
+
// One credit equals $0.01 (see CODEX_CREDIT_COST_USD in index.tsx), so credits
|
|
11
|
+
// equal USD * 100. Rate cards are expressed in the model's actual API price in
|
|
12
|
+
// USD per 1M tokens and scaled to credits in creditsFor, matching the Claude
|
|
13
|
+
// provider. These are the real gpt-5.* API prices, not the (4x cheaper) Codex
|
|
14
|
+
// subscription credit prices.
|
|
15
|
+
const USD_TO_CREDITS = 100;
|
|
10
16
|
const RATE_CARD = {
|
|
11
|
-
"gpt-5.5": { input:
|
|
12
|
-
"gpt-5.4": { input:
|
|
13
|
-
"gpt-5.4-mini": { input:
|
|
17
|
+
"gpt-5.5": { input: 5, cacheRead: 0.5, cacheWrite: 5, cacheWrite5m: 5, cacheWrite1h: 5, output: 30 },
|
|
18
|
+
"gpt-5.4": { input: 2.5, cacheRead: 0.25, cacheWrite: 2.5, cacheWrite5m: 2.5, cacheWrite1h: 2.5, output: 15 },
|
|
19
|
+
"gpt-5.4-mini": { input: 0.75, cacheRead: 0.075, cacheWrite: 0.75, cacheWrite5m: 0.75, cacheWrite1h: 0.75, output: 4.5 }
|
|
14
20
|
};
|
|
15
21
|
export class CodexUsageProvider extends UsageProviderBase {
|
|
16
22
|
constructor(options = {}) {
|
|
@@ -235,9 +241,10 @@ function creditsFor(modelId, usage) {
|
|
|
235
241
|
}
|
|
236
242
|
const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
237
243
|
const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
|
|
238
|
-
return ((nonCachedInputTokens / 1000000) * rate.input +
|
|
244
|
+
return (((nonCachedInputTokens / 1000000) * rate.input +
|
|
239
245
|
(cachedInputTokens / 1000000) * rate.cacheRead +
|
|
240
|
-
(usage.outputTokens / 1000000) * rate.output)
|
|
246
|
+
(usage.outputTokens / 1000000) * rate.output) *
|
|
247
|
+
USD_TO_CREDITS);
|
|
241
248
|
}
|
|
242
249
|
function rawUsageToTotals(usage) {
|
|
243
250
|
const cacheReadInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
@@ -3,11 +3,42 @@ export function createLimitWindowAggregates() {
|
|
|
3
3
|
return new Map();
|
|
4
4
|
}
|
|
5
5
|
export function numberOrZero(value) {
|
|
6
|
-
|
|
6
|
+
if (typeof value === "number") {
|
|
7
|
+
return Number.isFinite(value) ? value : 0;
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (trimmed === "") {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number(trimmed);
|
|
15
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
7
18
|
}
|
|
8
19
|
export function asRecord(value) {
|
|
9
20
|
return value && typeof value === "object" ? value : null;
|
|
10
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Extrapolate the full value of a limit from a partial observation: if
|
|
24
|
+
* `usedValue` represents `usedPercent` of the limit, the full limit is
|
|
25
|
+
* `usedValue / (usedPercent / 100)`. Because `usedPercent` is only known
|
|
26
|
+
* approximately, a `±percentTolerance` band yields a low/high range around the
|
|
27
|
+
* point estimate. Returns null when there is nothing to extrapolate from
|
|
28
|
+
* (no observed value, or a non-positive percent).
|
|
29
|
+
*/
|
|
30
|
+
export function estimateLimitFullValue(usedValue, usedPercent, percentTolerance = 1) {
|
|
31
|
+
if (!(usedValue > 0) || !(usedPercent > 0)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const toFull = (percent) => usedValue / (percent / 100);
|
|
35
|
+
const upperPercent = usedPercent - percentTolerance;
|
|
36
|
+
return {
|
|
37
|
+
point: toFull(usedPercent),
|
|
38
|
+
low: toFull(usedPercent + percentTolerance),
|
|
39
|
+
high: upperPercent > 0 ? toFull(upperPercent) : Infinity
|
|
40
|
+
};
|
|
41
|
+
}
|
|
11
42
|
export function applyRateLimits(windows, rateLimits, eventTimeMs, modelId, deltaTotals, planTypes) {
|
|
12
43
|
if (!rateLimits) {
|
|
13
44
|
return;
|
|
@@ -4,7 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
|
|
6
6
|
const CREDIT_TO_DOLLARS = 0.01;
|
|
7
|
-
|
|
7
|
+
// Limit windows at or below this used-percent carry too little signal to be
|
|
8
|
+
// worth reporting, so they are dropped from the anonymous usage payload.
|
|
9
|
+
const SKIP_REPORT_USED_PERCENTS = 3;
|
|
8
10
|
let versionCache = null;
|
|
9
11
|
export async function reportAnonymousUsage(statsList) {
|
|
10
12
|
const payload = await buildAnonymousUsagePayload(statsList);
|
|
@@ -103,7 +105,7 @@ function resolveReportedUsedPercents(window) {
|
|
|
103
105
|
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
104
106
|
}
|
|
105
107
|
function shouldReportUsageWindow(window) {
|
|
106
|
-
return resolveReportedUsedPercents(window)
|
|
108
|
+
return resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS;
|
|
107
109
|
}
|
|
108
110
|
function clampPercent(value) {
|
|
109
111
|
if (!Number.isFinite(value)) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letmecode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "devforth.io",
|
|
3
|
+
"version": "0.1.24",
|
|
4
|
+
"description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
|
|
5
|
+
"author": "Devforth (https://devforth.io)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "commonjs",
|
|
8
|
+
"main": "./dist/index.js",
|
|
8
9
|
"bin": {
|
|
9
10
|
"letmecode": "./bin/letmecode.js"
|
|
10
11
|
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/devforth/letmecode.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/devforth/letmecode#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/devforth/letmecode/issues"
|
|
19
|
+
},
|
|
11
20
|
"files": [
|
|
12
21
|
"bin",
|
|
13
22
|
"dist",
|
|
@@ -21,9 +30,18 @@
|
|
|
21
30
|
"access": "public"
|
|
22
31
|
},
|
|
23
32
|
"keywords": [
|
|
33
|
+
"ai",
|
|
34
|
+
"agents",
|
|
24
35
|
"cli",
|
|
25
|
-
"
|
|
36
|
+
"dashboard",
|
|
37
|
+
"usage",
|
|
38
|
+
"terminal",
|
|
39
|
+
"codex",
|
|
40
|
+
"claude",
|
|
41
|
+
"copilot",
|
|
42
|
+
"antigravity",
|
|
26
43
|
"npx",
|
|
44
|
+
"ink",
|
|
27
45
|
"typescript"
|
|
28
46
|
],
|
|
29
47
|
"dependencies": {
|