tokentrace 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/app/api/prices/refresh/route.ts +14 -4
- package/app/api/prices/route.ts +3 -0
- package/app/api/scan/route.ts +13 -6
- package/app/api/settings/route.ts +6 -3
- package/app/evidence/page.tsx +3 -3
- package/app/page.tsx +7 -7
- package/dist/runtime/db-seed.mjs +10 -2
- package/dist/runtime/digest.mjs +4 -3
- package/dist/runtime/doctor.mjs +4 -3
- package/dist/runtime/insights.mjs +4 -3
- package/dist/runtime/pricing-refresh.mjs +10 -2
- package/dist/runtime/reset.mjs +10 -2
- package/dist/runtime/scan.mjs +16 -7
- package/dist/runtime/status.mjs +2 -1
- package/package.json +1 -1
- package/src/db/settings.ts +5 -2
- package/src/ingestion/adapters/codex-cli.ts +4 -3
- package/src/ingestion/adapters/generic-log.ts +13 -1
- package/src/lib/api-json.ts +28 -3
- package/src/lib/format.ts +12 -1
- package/src/lib/pricing-manifest.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to TokenTrace are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-05-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Codex CLI `token_count` imports now preserve non-cached input tokens and add cached input tokens to processed totals, matching Codex's `input (+ cached)` session summary.
|
|
10
|
+
- Codex parser provenance is bumped to version 3 so previously imported Codex session files are reprocessed on the next scan instead of keeping stale undercounted rows.
|
|
11
|
+
- Generic text-log parsing now recognizes Codex `Token usage: total=... input=... (+ ... cached) output=...` summary lines as structured usage instead of weak text estimates.
|
|
12
|
+
- Overview and evidence token totals now switch to billions at large scales instead of displaying multi-thousand million values.
|
|
13
|
+
- Scan, settings, pricing, and pricing-refresh API writes now reject malformed JSON and avoid JavaScript truthiness coercion for boolean flags.
|
|
14
|
+
- Settings and scan custom folders are trimmed and blank entries are discarded before persistence or scan execution.
|
|
15
|
+
- Pricing manifest imports now ignore invalid, boolean, array, blank, and negative price values instead of coercing them into trusted numeric prices.
|
|
16
|
+
|
|
5
17
|
## [0.8.1] - 2026-05-13
|
|
6
18
|
|
|
7
19
|
### Fixed
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { jsonBooleanFlag, readOptionalJsonObject } from "@/src/lib/api-json";
|
|
3
3
|
import { refreshPricing } from "@/src/lib/pricing-refresh";
|
|
4
4
|
|
|
5
5
|
export const dynamic = "force-dynamic";
|
|
6
6
|
|
|
7
|
+
function refreshSource(value: unknown) {
|
|
8
|
+
if (value == null) return "remote";
|
|
9
|
+
if (value === "remote" || value === "bundled") return value;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export async function POST(request: Request) {
|
|
8
|
-
const parsed = await
|
|
14
|
+
const parsed = await readOptionalJsonObject(request);
|
|
9
15
|
if (!parsed.ok) {
|
|
10
16
|
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
11
17
|
}
|
|
12
18
|
const body = parsed.body;
|
|
19
|
+
const source = refreshSource(body.source);
|
|
20
|
+
if (!source) {
|
|
21
|
+
return NextResponse.json({ error: "source must be remote or bundled" }, { status: 400 });
|
|
22
|
+
}
|
|
13
23
|
const result = await refreshPricing({
|
|
14
|
-
source
|
|
15
|
-
force:
|
|
24
|
+
source,
|
|
25
|
+
force: jsonBooleanFlag(body.force)
|
|
16
26
|
});
|
|
17
27
|
return NextResponse.json(result);
|
|
18
28
|
}
|
package/app/api/prices/route.ts
CHANGED
|
@@ -11,6 +11,9 @@ function requiredText(value: unknown) {
|
|
|
11
11
|
function nullablePrice(value: unknown, field: string) {
|
|
12
12
|
if (value == null) return { ok: true as const, value: null };
|
|
13
13
|
if (typeof value === "string" && value.trim() === "") return { ok: true as const, value: null };
|
|
14
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
15
|
+
return { ok: false as const, error: `${field} must be a non-negative number or empty` };
|
|
16
|
+
}
|
|
14
17
|
const number = Number(value);
|
|
15
18
|
if (!Number.isFinite(number) || number < 0) {
|
|
16
19
|
return { ok: false as const, error: `${field} must be a non-negative number or empty` };
|
package/app/api/scan/route.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { runScan } from "@/src/ingestion/scan";
|
|
3
|
-
import {
|
|
3
|
+
import { jsonBooleanFlag, readOptionalJsonObject } from "@/src/lib/api-json";
|
|
4
4
|
|
|
5
5
|
export const dynamic = "force-dynamic";
|
|
6
6
|
|
|
7
|
+
function stringList(value: unknown) {
|
|
8
|
+
return Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
.filter((folder): folder is string => typeof folder === "string")
|
|
11
|
+
.map((folder) => folder.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export async function POST(request: Request) {
|
|
8
|
-
const parsed = await
|
|
17
|
+
const parsed = await readOptionalJsonObject(request);
|
|
9
18
|
if (!parsed.ok) {
|
|
10
19
|
return NextResponse.json({ error: parsed.error }, { status: 400 });
|
|
11
20
|
}
|
|
12
21
|
const body = parsed.body;
|
|
13
22
|
const result = await runScan({
|
|
14
|
-
folders:
|
|
15
|
-
|
|
16
|
-
: undefined,
|
|
17
|
-
force: Boolean(body.force)
|
|
23
|
+
folders: stringList(body.folders),
|
|
24
|
+
force: jsonBooleanFlag(body.force)
|
|
18
25
|
});
|
|
19
26
|
return NextResponse.json(result);
|
|
20
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import { getDatabasePath } from "@/src/db/client";
|
|
3
3
|
import { getAppSettings, normalizeUsageGuardrails, saveAppSettings } from "@/src/db/settings";
|
|
4
|
-
import { readJsonObject } from "@/src/lib/api-json";
|
|
4
|
+
import { jsonBooleanFlag, readJsonObject } from "@/src/lib/api-json";
|
|
5
5
|
|
|
6
6
|
export const dynamic = "force-dynamic";
|
|
7
7
|
|
|
@@ -19,11 +19,14 @@ export async function PUT(request: Request) {
|
|
|
19
19
|
}
|
|
20
20
|
const body = parsed.body;
|
|
21
21
|
const customFolders = Array.isArray(body.customFolders)
|
|
22
|
-
? body.customFolders
|
|
22
|
+
? body.customFolders
|
|
23
|
+
.filter((folder: unknown): folder is string => typeof folder === "string")
|
|
24
|
+
.map((folder) => folder.trim())
|
|
25
|
+
.filter(Boolean)
|
|
23
26
|
: [];
|
|
24
27
|
const saved = saveAppSettings({
|
|
25
28
|
customFolders,
|
|
26
|
-
storeRawMessageContent:
|
|
29
|
+
storeRawMessageContent: jsonBooleanFlag(body.storeRawMessageContent),
|
|
27
30
|
usageGuardrails: normalizeUsageGuardrails(body.usageGuardrails)
|
|
28
31
|
});
|
|
29
32
|
|
package/app/evidence/page.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|
|
6
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
7
7
|
import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
|
|
8
8
|
import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
|
|
9
|
-
import { formatCurrency,
|
|
9
|
+
import { formatCurrency, formatExactTokens, percent } from "@/src/lib/format";
|
|
10
10
|
|
|
11
11
|
export const dynamic = "force-dynamic";
|
|
12
12
|
|
|
@@ -57,7 +57,7 @@ export default async function EvidencePage({ searchParams }: EvidencePageProps)
|
|
|
57
57
|
<div className="grid divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
|
|
58
58
|
<div className="p-3">
|
|
59
59
|
<FieldLabel>Tokens</FieldLabel>
|
|
60
|
-
<DataValue className="mt-1" size="md">{
|
|
60
|
+
<DataValue className="mt-1" size="md">{formatExactTokens(trail.totals.tokens)}</DataValue>
|
|
61
61
|
</div>
|
|
62
62
|
<div className="p-3">
|
|
63
63
|
<FieldLabel>Cost</FieldLabel>
|
|
@@ -111,7 +111,7 @@ export default async function EvidencePage({ searchParams }: EvidencePageProps)
|
|
|
111
111
|
{session.tool} / {session.provider} / {session.project}
|
|
112
112
|
</div>
|
|
113
113
|
</TableCell>
|
|
114
|
-
<TableCell>{
|
|
114
|
+
<TableCell>{formatExactTokens(session.totalTokens)}</TableCell>
|
|
115
115
|
<TableCell>{formatCurrency(session.cost)}</TableCell>
|
|
116
116
|
<TableCell className="max-w-80">
|
|
117
117
|
<Link href={session.sourceHref} title={session.sourceFile}>
|
package/app/page.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import { getDefaultSearchRoots } from "@/src/ingestion/discovery";
|
|
|
15
15
|
import { buildFirstRunStatus, type FirstRunStatus } from "@/src/lib/first-run-status";
|
|
16
16
|
import { buildUnknownCostRepairWorkbench } from "@/src/lib/unknown-cost-repair";
|
|
17
17
|
import { resolveDateRange } from "@/src/lib/date-range";
|
|
18
|
-
import { formatCurrency, formatTokens, percent } from "@/src/lib/format";
|
|
18
|
+
import { formatCurrency, formatSignedTokens, formatTokens, percent } from "@/src/lib/format";
|
|
19
19
|
import { cn } from "@/src/lib/utils";
|
|
20
20
|
import type { UsageGuardrailMetric } from "@/src/lib/usage-guardrails";
|
|
21
21
|
|
|
@@ -74,16 +74,16 @@ function MetricCard({
|
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function formatSignedNumber(value: number) {
|
|
78
|
-
if (value === 0) return "0";
|
|
79
|
-
return `${value > 0 ? "+" : "-"}${Math.abs(value).toLocaleString()}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
77
|
function formatSignedCurrency(value: number) {
|
|
83
78
|
if (value === 0) return "$0.00";
|
|
84
79
|
return `${value > 0 ? "+" : "-"}${formatCurrency(Math.abs(value))}`;
|
|
85
80
|
}
|
|
86
81
|
|
|
82
|
+
function formatSignedNumber(value: number) {
|
|
83
|
+
if (value === 0) return "0";
|
|
84
|
+
return `${value > 0 ? "+" : "-"}${Math.abs(value).toLocaleString()}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
87
|
function formatPercentValue(value: number | null) {
|
|
88
88
|
if (value == null) return "new";
|
|
89
89
|
if (value === 0) return "flat";
|
|
@@ -319,7 +319,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
|
|
|
319
319
|
<DeltaMetric
|
|
320
320
|
label="Tokens"
|
|
321
321
|
value={formatTokens(data.comparison.current.totalTokens)}
|
|
322
|
-
delta={
|
|
322
|
+
delta={formatSignedTokens(data.comparison.delta.totalTokens)}
|
|
323
323
|
percentValue={data.comparison.delta.totalTokensPercent}
|
|
324
324
|
previous={formatTokens(data.comparison.previous.totalTokens)}
|
|
325
325
|
/>
|
package/dist/runtime/db-seed.mjs
CHANGED
|
@@ -1193,8 +1193,16 @@ var default_model_prices_default = {
|
|
|
1193
1193
|
// src/lib/pricing-manifest.ts
|
|
1194
1194
|
function nullableNumber(value) {
|
|
1195
1195
|
if (value == null) return null;
|
|
1196
|
-
|
|
1197
|
-
|
|
1196
|
+
if (typeof value === "number") {
|
|
1197
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof value === "string") {
|
|
1200
|
+
const trimmed = value.trim();
|
|
1201
|
+
if (!trimmed) return null;
|
|
1202
|
+
const number = Number(trimmed);
|
|
1203
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1198
1206
|
}
|
|
1199
1207
|
function validModel(value) {
|
|
1200
1208
|
if (!value || typeof value !== "object") return null;
|
package/dist/runtime/digest.mjs
CHANGED
|
@@ -775,7 +775,8 @@ var init_model_aliases = __esm({
|
|
|
775
775
|
// src/lib/format.ts
|
|
776
776
|
function formatTokens(value) {
|
|
777
777
|
const number3 = value ?? 0;
|
|
778
|
-
if (number3 >=
|
|
778
|
+
if (number3 >= 999995e3) return `${(number3 / 1e9).toFixed(2)}B`;
|
|
779
|
+
if (number3 >= 999950) return `${(number3 / 1e6).toFixed(2)}M`;
|
|
779
780
|
if (number3 >= 1e3) return `${(number3 / 1e3).toFixed(1)}K`;
|
|
780
781
|
return number3.toLocaleString();
|
|
781
782
|
}
|
|
@@ -964,8 +965,8 @@ function normalizeSettings(value) {
|
|
|
964
965
|
if (!value || typeof value !== "object") return defaultSettings;
|
|
965
966
|
const candidate = value;
|
|
966
967
|
return {
|
|
967
|
-
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string") : [],
|
|
968
|
-
storeRawMessageContent:
|
|
968
|
+
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
|
|
969
|
+
storeRawMessageContent: candidate.storeRawMessageContent === true,
|
|
969
970
|
usageGuardrails: normalizeUsageGuardrails(candidate.usageGuardrails)
|
|
970
971
|
};
|
|
971
972
|
}
|
package/dist/runtime/doctor.mjs
CHANGED
|
@@ -775,7 +775,8 @@ var init_model_aliases = __esm({
|
|
|
775
775
|
// src/lib/format.ts
|
|
776
776
|
function formatTokens(value) {
|
|
777
777
|
const number4 = value ?? 0;
|
|
778
|
-
if (number4 >=
|
|
778
|
+
if (number4 >= 999995e3) return `${(number4 / 1e9).toFixed(2)}B`;
|
|
779
|
+
if (number4 >= 999950) return `${(number4 / 1e6).toFixed(2)}M`;
|
|
779
780
|
if (number4 >= 1e3) return `${(number4 / 1e3).toFixed(1)}K`;
|
|
780
781
|
return number4.toLocaleString();
|
|
781
782
|
}
|
|
@@ -949,8 +950,8 @@ function normalizeSettings(value) {
|
|
|
949
950
|
if (!value || typeof value !== "object") return defaultSettings;
|
|
950
951
|
const candidate = value;
|
|
951
952
|
return {
|
|
952
|
-
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string") : [],
|
|
953
|
-
storeRawMessageContent:
|
|
953
|
+
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
|
|
954
|
+
storeRawMessageContent: candidate.storeRawMessageContent === true,
|
|
954
955
|
usageGuardrails: normalizeUsageGuardrails(candidate.usageGuardrails)
|
|
955
956
|
};
|
|
956
957
|
}
|
|
@@ -775,7 +775,8 @@ var init_model_aliases = __esm({
|
|
|
775
775
|
// src/lib/format.ts
|
|
776
776
|
function formatTokens(value) {
|
|
777
777
|
const number3 = value ?? 0;
|
|
778
|
-
if (number3 >=
|
|
778
|
+
if (number3 >= 999995e3) return `${(number3 / 1e9).toFixed(2)}B`;
|
|
779
|
+
if (number3 >= 999950) return `${(number3 / 1e6).toFixed(2)}M`;
|
|
779
780
|
if (number3 >= 1e3) return `${(number3 / 1e3).toFixed(1)}K`;
|
|
780
781
|
return number3.toLocaleString();
|
|
781
782
|
}
|
|
@@ -949,8 +950,8 @@ function normalizeSettings(value) {
|
|
|
949
950
|
if (!value || typeof value !== "object") return defaultSettings;
|
|
950
951
|
const candidate = value;
|
|
951
952
|
return {
|
|
952
|
-
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string") : [],
|
|
953
|
-
storeRawMessageContent:
|
|
953
|
+
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
|
|
954
|
+
storeRawMessageContent: candidate.storeRawMessageContent === true,
|
|
954
955
|
usageGuardrails: normalizeUsageGuardrails(candidate.usageGuardrails)
|
|
955
956
|
};
|
|
956
957
|
}
|
|
@@ -1470,8 +1470,16 @@ var init_default_model_prices = __esm({
|
|
|
1470
1470
|
// src/lib/pricing-manifest.ts
|
|
1471
1471
|
function nullableNumber(value) {
|
|
1472
1472
|
if (value == null) return null;
|
|
1473
|
-
|
|
1474
|
-
|
|
1473
|
+
if (typeof value === "number") {
|
|
1474
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
1475
|
+
}
|
|
1476
|
+
if (typeof value === "string") {
|
|
1477
|
+
const trimmed = value.trim();
|
|
1478
|
+
if (!trimmed) return null;
|
|
1479
|
+
const number = Number(trimmed);
|
|
1480
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1475
1483
|
}
|
|
1476
1484
|
function validModel(value) {
|
|
1477
1485
|
if (!value || typeof value !== "object") return null;
|
package/dist/runtime/reset.mjs
CHANGED
|
@@ -1193,8 +1193,16 @@ var default_model_prices_default = {
|
|
|
1193
1193
|
// src/lib/pricing-manifest.ts
|
|
1194
1194
|
function nullableNumber(value) {
|
|
1195
1195
|
if (value == null) return null;
|
|
1196
|
-
|
|
1197
|
-
|
|
1196
|
+
if (typeof value === "number") {
|
|
1197
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof value === "string") {
|
|
1200
|
+
const trimmed = value.trim();
|
|
1201
|
+
if (!trimmed) return null;
|
|
1202
|
+
const number = Number(trimmed);
|
|
1203
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
1204
|
+
}
|
|
1205
|
+
return null;
|
|
1198
1206
|
}
|
|
1199
1207
|
function validModel(value) {
|
|
1200
1208
|
if (!value || typeof value !== "object") return null;
|
package/dist/runtime/scan.mjs
CHANGED
|
@@ -532,8 +532,8 @@ function normalizeSettings(value) {
|
|
|
532
532
|
if (!value || typeof value !== "object") return defaultSettings;
|
|
533
533
|
const candidate = value;
|
|
534
534
|
return {
|
|
535
|
-
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string") : [],
|
|
536
|
-
storeRawMessageContent:
|
|
535
|
+
customFolders: Array.isArray(candidate.customFolders) ? candidate.customFolders.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [],
|
|
536
|
+
storeRawMessageContent: candidate.storeRawMessageContent === true,
|
|
537
537
|
usageGuardrails: normalizeUsageGuardrails(candidate.usageGuardrails)
|
|
538
538
|
};
|
|
539
539
|
}
|
|
@@ -1566,15 +1566,16 @@ function deltaFromUsage(current, previous) {
|
|
|
1566
1566
|
return delta.inputTokens + delta.cachedInputTokens + delta.outputTokens + delta.reasoningOutputTokens + delta.totalTokens > 0 ? delta : null;
|
|
1567
1567
|
}
|
|
1568
1568
|
function normalizedUsageFromDelta(delta) {
|
|
1569
|
-
const inputTokens =
|
|
1569
|
+
const inputTokens = delta.inputTokens;
|
|
1570
1570
|
const outputTokens = Math.max(0, delta.outputTokens - delta.reasoningOutputTokens);
|
|
1571
|
+
const displayedTotalWithCache = delta.totalTokens + delta.cachedInputTokens;
|
|
1571
1572
|
const computedTotal = inputTokens + delta.cachedInputTokens + outputTokens + delta.reasoningOutputTokens;
|
|
1572
1573
|
return {
|
|
1573
1574
|
input_tokens: inputTokens,
|
|
1574
1575
|
output_tokens: outputTokens,
|
|
1575
1576
|
cache_read_input_tokens: delta.cachedInputTokens,
|
|
1576
1577
|
reasoning_tokens: delta.reasoningOutputTokens,
|
|
1577
|
-
total_tokens: Math.max(
|
|
1578
|
+
total_tokens: Math.max(displayedTotalWithCache, computedTotal)
|
|
1578
1579
|
};
|
|
1579
1580
|
}
|
|
1580
1581
|
function exactTokenCountRecords(records, fallbackSessionId) {
|
|
@@ -1617,7 +1618,7 @@ var init_codex_cli = __esm({
|
|
|
1617
1618
|
codexCliAdapter = {
|
|
1618
1619
|
id: "codex-cli",
|
|
1619
1620
|
displayName: "Codex CLI",
|
|
1620
|
-
version:
|
|
1621
|
+
version: 3,
|
|
1621
1622
|
async detect(file) {
|
|
1622
1623
|
const extension = path5.extname(file.path).toLowerCase();
|
|
1623
1624
|
if (isCodexCliUsagePath(file.path)) {
|
|
@@ -1879,23 +1880,31 @@ var init_generic_log = __esm({
|
|
|
1879
1880
|
const session = textAfter(line, [/session(?:_id)?\s*[:=]\s*([^\s,]+)/i]);
|
|
1880
1881
|
if (session) currentSession = session;
|
|
1881
1882
|
projectPath = projectPath ?? textAfter(line, [/(?:cwd|project|path)\s*[:=]\s*(.+)$/i]);
|
|
1883
|
+
const codexSummaryLine = /\bToken usage:/i.test(line) || /\(\s*\+\s*[0-9,]+\s+cached\s*\)/i.test(line);
|
|
1882
1884
|
const model = textAfter(line, [/model\s*[:=]\s*([A-Za-z0-9_.:/-]+)/i]);
|
|
1883
1885
|
const inputTokens = numberAfter(line, [
|
|
1886
|
+
/\binput\s*[:=]\s*([0-9,]+)/i,
|
|
1884
1887
|
/(?:input_tokens|prompt_tokens|input tokens|prompt tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1885
1888
|
]);
|
|
1886
1889
|
const outputTokens = numberAfter(line, [
|
|
1890
|
+
/\boutput\s*[:=]\s*([0-9,]+)/i,
|
|
1887
1891
|
/(?:output_tokens|completion_tokens|output tokens|completion tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1888
1892
|
]);
|
|
1889
1893
|
const cacheReadTokens = numberAfter(line, [
|
|
1894
|
+
/\(\s*\+\s*([0-9,]+)\s+cached\s*\)/i,
|
|
1895
|
+
/\bcached\s*[:=]\s*([0-9,]+)/i,
|
|
1890
1896
|
/(?:cache_read_input_tokens|cached_input_tokens|cache read tokens|cached tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1891
1897
|
]);
|
|
1892
1898
|
const cacheWriteTokens = numberAfter(line, [
|
|
1893
1899
|
/(?:cache_creation_input_tokens|cache_write_input_tokens|cache write tokens|cache creation tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1894
1900
|
]);
|
|
1895
1901
|
const reasoningTokens = numberAfter(line, [
|
|
1902
|
+
/\(\s*reasoning\s+([0-9,]+)\s*\)/i,
|
|
1903
|
+
/\breasoning\s*[:=]\s*([0-9,]+)/i,
|
|
1896
1904
|
/(?:reasoning_output_tokens|reasoning_tokens|reasoning output tokens|reasoning tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1897
1905
|
]);
|
|
1898
1906
|
const explicitTotalTokens = numberAfter(line, [
|
|
1907
|
+
/\btotal\s*[:=]\s*([0-9,]+)/i,
|
|
1899
1908
|
/(?:total_tokens|total tokens)\s*[:=]\s*([0-9,]+)/i
|
|
1900
1909
|
]);
|
|
1901
1910
|
const fallbackTotalTokens = inputTokens == null && outputTokens == null && cacheReadTokens == null && cacheWriteTokens == null && reasoningTokens == null ? numberAfter(line, [/\btokens\s*[:=]\s*([0-9,]+)/i]) : null;
|
|
@@ -1911,7 +1920,7 @@ var init_generic_log = __esm({
|
|
|
1911
1920
|
const normalizedCacheWriteTokens = cacheWriteTokens ?? 0;
|
|
1912
1921
|
const normalizedReasoningTokens = reasoningTokens ?? 0;
|
|
1913
1922
|
const partSum = () => normalizedInputTokens + normalizedOutputTokens + normalizedCacheReadTokens + normalizedCacheWriteTokens + normalizedReasoningTokens;
|
|
1914
|
-
if (inputTokens != null && totalTokens != null && normalizedCacheReadTokens + normalizedCacheWriteTokens > 0 && partSum() > totalTokens) {
|
|
1923
|
+
if (!codexSummaryLine && inputTokens != null && totalTokens != null && normalizedCacheReadTokens + normalizedCacheWriteTokens > 0 && partSum() > totalTokens) {
|
|
1915
1924
|
normalizedInputTokens = Math.max(
|
|
1916
1925
|
0,
|
|
1917
1926
|
normalizedInputTokens - normalizedCacheReadTokens - normalizedCacheWriteTokens
|
|
@@ -1920,7 +1929,7 @@ var init_generic_log = __esm({
|
|
|
1920
1929
|
if (outputTokens != null && totalTokens != null && normalizedReasoningTokens > 0 && partSum() > totalTokens) {
|
|
1921
1930
|
normalizedOutputTokens = Math.max(0, normalizedOutputTokens - normalizedReasoningTokens);
|
|
1922
1931
|
}
|
|
1923
|
-
const structuredTotal = totalTokens ?? partSum();
|
|
1932
|
+
const structuredTotal = codexSummaryLine && totalTokens != null ? Math.max(totalTokens + normalizedCacheReadTokens + normalizedCacheWriteTokens, partSum()) : totalTokens ?? partSum();
|
|
1924
1933
|
interactions2.push({
|
|
1925
1934
|
externalId: `${currentSession}-${index2}`,
|
|
1926
1935
|
timestamp,
|
package/dist/runtime/status.mjs
CHANGED
|
@@ -170,7 +170,8 @@ var init_helpers = __esm({
|
|
|
170
170
|
// src/lib/format.ts
|
|
171
171
|
function formatTokens(value) {
|
|
172
172
|
const number2 = value ?? 0;
|
|
173
|
-
if (number2 >=
|
|
173
|
+
if (number2 >= 999995e3) return `${(number2 / 1e9).toFixed(2)}B`;
|
|
174
|
+
if (number2 >= 999950) return `${(number2 / 1e6).toFixed(2)}M`;
|
|
174
175
|
if (number2 >= 1e3) return `${(number2 / 1e3).toFixed(1)}K`;
|
|
175
176
|
return number2.toLocaleString();
|
|
176
177
|
}
|
package/package.json
CHANGED
package/src/db/settings.ts
CHANGED
|
@@ -45,9 +45,12 @@ function normalizeSettings(value: unknown): AppSettings {
|
|
|
45
45
|
const candidate = value as Partial<AppSettings>;
|
|
46
46
|
return {
|
|
47
47
|
customFolders: Array.isArray(candidate.customFolders)
|
|
48
|
-
? candidate.customFolders
|
|
48
|
+
? candidate.customFolders
|
|
49
|
+
.filter((item): item is string => typeof item === "string")
|
|
50
|
+
.map((item) => item.trim())
|
|
51
|
+
.filter(Boolean)
|
|
49
52
|
: [],
|
|
50
|
-
storeRawMessageContent:
|
|
53
|
+
storeRawMessageContent: candidate.storeRawMessageContent === true,
|
|
51
54
|
usageGuardrails: normalizeUsageGuardrails(candidate.usageGuardrails)
|
|
52
55
|
};
|
|
53
56
|
}
|
|
@@ -155,8 +155,9 @@ function deltaFromUsage(current: CodexUsageTotal, previous: CodexUsageTotal | nu
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
function normalizedUsageFromDelta(delta: CodexUsageTotal) {
|
|
158
|
-
const inputTokens =
|
|
158
|
+
const inputTokens = delta.inputTokens;
|
|
159
159
|
const outputTokens = Math.max(0, delta.outputTokens - delta.reasoningOutputTokens);
|
|
160
|
+
const displayedTotalWithCache = delta.totalTokens + delta.cachedInputTokens;
|
|
160
161
|
const computedTotal =
|
|
161
162
|
inputTokens + delta.cachedInputTokens + outputTokens + delta.reasoningOutputTokens;
|
|
162
163
|
|
|
@@ -165,7 +166,7 @@ function normalizedUsageFromDelta(delta: CodexUsageTotal) {
|
|
|
165
166
|
output_tokens: outputTokens,
|
|
166
167
|
cache_read_input_tokens: delta.cachedInputTokens,
|
|
167
168
|
reasoning_tokens: delta.reasoningOutputTokens,
|
|
168
|
-
total_tokens: Math.max(
|
|
169
|
+
total_tokens: Math.max(displayedTotalWithCache, computedTotal)
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -207,7 +208,7 @@ function exactTokenCountRecords(records: Record<string, unknown>[], fallbackSess
|
|
|
207
208
|
export const codexCliAdapter: IngestionAdapter = {
|
|
208
209
|
id: "codex-cli",
|
|
209
210
|
displayName: "Codex CLI",
|
|
210
|
-
version:
|
|
211
|
+
version: 3,
|
|
211
212
|
|
|
212
213
|
async detect(file) {
|
|
213
214
|
const extension = path.extname(file.path).toLowerCase();
|
|
@@ -57,23 +57,32 @@ export const genericLogAdapter: IngestionAdapter = {
|
|
|
57
57
|
projectPath =
|
|
58
58
|
projectPath ?? textAfter(line, [/(?:cwd|project|path)\s*[:=]\s*(.+)$/i]);
|
|
59
59
|
|
|
60
|
+
const codexSummaryLine =
|
|
61
|
+
/\bToken usage:/i.test(line) || /\(\s*\+\s*[0-9,]+\s+cached\s*\)/i.test(line);
|
|
60
62
|
const model = textAfter(line, [/model\s*[:=]\s*([A-Za-z0-9_.:/-]+)/i]);
|
|
61
63
|
const inputTokens = numberAfter(line, [
|
|
64
|
+
/\binput\s*[:=]\s*([0-9,]+)/i,
|
|
62
65
|
/(?:input_tokens|prompt_tokens|input tokens|prompt tokens)\s*[:=]\s*([0-9,]+)/i
|
|
63
66
|
]);
|
|
64
67
|
const outputTokens = numberAfter(line, [
|
|
68
|
+
/\boutput\s*[:=]\s*([0-9,]+)/i,
|
|
65
69
|
/(?:output_tokens|completion_tokens|output tokens|completion tokens)\s*[:=]\s*([0-9,]+)/i
|
|
66
70
|
]);
|
|
67
71
|
const cacheReadTokens = numberAfter(line, [
|
|
72
|
+
/\(\s*\+\s*([0-9,]+)\s+cached\s*\)/i,
|
|
73
|
+
/\bcached\s*[:=]\s*([0-9,]+)/i,
|
|
68
74
|
/(?:cache_read_input_tokens|cached_input_tokens|cache read tokens|cached tokens)\s*[:=]\s*([0-9,]+)/i
|
|
69
75
|
]);
|
|
70
76
|
const cacheWriteTokens = numberAfter(line, [
|
|
71
77
|
/(?:cache_creation_input_tokens|cache_write_input_tokens|cache write tokens|cache creation tokens)\s*[:=]\s*([0-9,]+)/i
|
|
72
78
|
]);
|
|
73
79
|
const reasoningTokens = numberAfter(line, [
|
|
80
|
+
/\(\s*reasoning\s+([0-9,]+)\s*\)/i,
|
|
81
|
+
/\breasoning\s*[:=]\s*([0-9,]+)/i,
|
|
74
82
|
/(?:reasoning_output_tokens|reasoning_tokens|reasoning output tokens|reasoning tokens)\s*[:=]\s*([0-9,]+)/i
|
|
75
83
|
]);
|
|
76
84
|
const explicitTotalTokens = numberAfter(line, [
|
|
85
|
+
/\btotal\s*[:=]\s*([0-9,]+)/i,
|
|
77
86
|
/(?:total_tokens|total tokens)\s*[:=]\s*([0-9,]+)/i
|
|
78
87
|
]);
|
|
79
88
|
const fallbackTotalTokens =
|
|
@@ -109,6 +118,7 @@ export const genericLogAdapter: IngestionAdapter = {
|
|
|
109
118
|
normalizedCacheWriteTokens +
|
|
110
119
|
normalizedReasoningTokens;
|
|
111
120
|
if (
|
|
121
|
+
!codexSummaryLine &&
|
|
112
122
|
inputTokens != null &&
|
|
113
123
|
totalTokens != null &&
|
|
114
124
|
normalizedCacheReadTokens + normalizedCacheWriteTokens > 0 &&
|
|
@@ -128,7 +138,9 @@ export const genericLogAdapter: IngestionAdapter = {
|
|
|
128
138
|
normalizedOutputTokens = Math.max(0, normalizedOutputTokens - normalizedReasoningTokens);
|
|
129
139
|
}
|
|
130
140
|
const structuredTotal =
|
|
131
|
-
totalTokens
|
|
141
|
+
codexSummaryLine && totalTokens != null
|
|
142
|
+
? Math.max(totalTokens + normalizedCacheReadTokens + normalizedCacheWriteTokens, partSum())
|
|
143
|
+
: totalTokens ?? partSum();
|
|
132
144
|
interactions.push({
|
|
133
145
|
externalId: `${currentSession}-${index}`,
|
|
134
146
|
timestamp,
|
package/src/lib/api-json.ts
CHANGED
|
@@ -4,11 +4,24 @@ export type JsonObjectResult =
|
|
|
4
4
|
| { ok: true; body: JsonObject }
|
|
5
5
|
| { ok: false; error: string };
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
let
|
|
7
|
+
async function readJsonObjectText(request: Request, allowEmpty: boolean): Promise<JsonObjectResult> {
|
|
8
|
+
let text: string;
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
text = await request.text();
|
|
12
|
+
} catch {
|
|
13
|
+
return { ok: false, error: "request body must be valid JSON" };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!text.trim()) {
|
|
17
|
+
return allowEmpty
|
|
18
|
+
? { ok: true, body: {} }
|
|
19
|
+
: { ok: false, error: "request body must be valid JSON" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let body: unknown;
|
|
23
|
+
try {
|
|
24
|
+
body = JSON.parse(text);
|
|
12
25
|
} catch {
|
|
13
26
|
return { ok: false, error: "request body must be valid JSON" };
|
|
14
27
|
}
|
|
@@ -19,3 +32,15 @@ export async function readJsonObject(request: Request): Promise<JsonObjectResult
|
|
|
19
32
|
|
|
20
33
|
return { ok: true, body: body as JsonObject };
|
|
21
34
|
}
|
|
35
|
+
|
|
36
|
+
export function readJsonObject(request: Request): Promise<JsonObjectResult> {
|
|
37
|
+
return readJsonObjectText(request, false);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readOptionalJsonObject(request: Request): Promise<JsonObjectResult> {
|
|
41
|
+
return readJsonObjectText(request, true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function jsonBooleanFlag(value: unknown) {
|
|
45
|
+
return value === true;
|
|
46
|
+
}
|
package/src/lib/format.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export function formatTokens(value: number | null | undefined) {
|
|
2
2
|
const number = value ?? 0;
|
|
3
|
-
if (number >=
|
|
3
|
+
if (number >= 999_995_000) return `${(number / 1_000_000_000).toFixed(2)}B`;
|
|
4
|
+
if (number >= 999_950) return `${(number / 1_000_000).toFixed(2)}M`;
|
|
4
5
|
if (number >= 1_000) return `${(number / 1_000).toFixed(1)}K`;
|
|
5
6
|
return number.toLocaleString();
|
|
6
7
|
}
|
|
7
8
|
|
|
9
|
+
export function formatSignedTokens(value: number | null | undefined) {
|
|
10
|
+
const number = value ?? 0;
|
|
11
|
+
if (number === 0) return "0";
|
|
12
|
+
return `${number > 0 ? "+" : "-"}${formatTokens(Math.abs(number))}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatExactTokens(value: number | null | undefined) {
|
|
16
|
+
return (value ?? 0).toLocaleString();
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export function formatCurrency(value: number | null | undefined, currency = "USD") {
|
|
9
20
|
if (value == null) return "Unknown";
|
|
10
21
|
return new Intl.NumberFormat("en-US", {
|
|
@@ -31,8 +31,16 @@ export type PricingManifest = {
|
|
|
31
31
|
|
|
32
32
|
function nullableNumber(value: unknown) {
|
|
33
33
|
if (value == null) return null;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
if (!trimmed) return null;
|
|
40
|
+
const number = Number(trimmed);
|
|
41
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
function validModel(value: unknown): PricingManifestModel | null {
|