@cruxy/cli 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.js +21 -0
- package/dist/agent/prompts.d.ts +7 -0
- package/dist/agent/prompts.js +6 -0
- package/dist/agent/session.d.ts +26 -1
- package/dist/agent/session.js +39 -6
- package/dist/cli/commands/memory.d.ts +8 -0
- package/dist/cli/commands/memory.js +98 -0
- package/dist/cli/commands/run.js +19 -0
- package/dist/cli/commands/usage.d.ts +9 -0
- package/dist/cli/commands/usage.js +81 -0
- package/dist/cli/program.js +4 -0
- package/dist/cli/session-factory.js +39 -1
- package/dist/config/schema.d.ts +336 -12
- package/dist/config/schema.js +56 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.js +18 -0
- package/dist/errors/constructors.d.ts +21 -0
- package/dist/errors/constructors.js +64 -0
- package/dist/errors/types.d.ts +12 -0
- package/dist/errors/types.js +24 -0
- package/dist/hooks/types.d.ts +1 -1
- package/dist/memory/index.d.ts +7 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/recall.d.ts +32 -0
- package/dist/memory/recall.js +73 -0
- package/dist/memory/remember-tool.d.ts +25 -0
- package/dist/memory/remember-tool.js +56 -0
- package/dist/memory/secrets.d.ts +29 -0
- package/dist/memory/secrets.js +61 -0
- package/dist/memory/service.d.ts +92 -0
- package/dist/memory/service.js +164 -0
- package/dist/memory/store.d.ts +32 -0
- package/dist/memory/store.js +100 -0
- package/dist/memory/trust.d.ts +52 -0
- package/dist/memory/trust.js +106 -0
- package/dist/memory/types.d.ts +101 -0
- package/dist/memory/types.js +58 -0
- package/dist/plan/service.d.ts +13 -1
- package/dist/plan/service.js +4 -0
- package/dist/usage/collect.d.ts +40 -0
- package/dist/usage/collect.js +34 -0
- package/dist/usage/cost.d.ts +19 -0
- package/dist/usage/cost.js +29 -0
- package/dist/usage/index.d.ts +15 -0
- package/dist/usage/index.js +15 -0
- package/dist/usage/store.d.ts +37 -0
- package/dist/usage/store.js +83 -0
- package/dist/usage/summary.d.ts +32 -0
- package/dist/usage/summary.js +119 -0
- package/dist/usage/types.d.ts +220 -0
- package/dist/usage/types.js +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CruxyError } from "../errors/index.js";
|
|
2
|
+
import { type UsageFile, type UsageRecord } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The usage store (C.22): a single JSON file under `~/.cruxy/usage`, holding a
|
|
5
|
+
* bounded, newest-last list of run records. Pure DATA — parsed with the strict
|
|
6
|
+
* schema, NEVER eval'd — and written `0600` (personal accounting). This module
|
|
7
|
+
* touches only the local filesystem; it makes ZERO network calls.
|
|
8
|
+
*
|
|
9
|
+
* Reads are non-fatal by contract: a missing file is empty (not an error); a
|
|
10
|
+
* corrupt/unreadable one yields an empty result plus a coded
|
|
11
|
+
* {@link usageRead} error the caller can surface and SKIP — a broken usage file
|
|
12
|
+
* never crashes a run.
|
|
13
|
+
*/
|
|
14
|
+
/** `~/.cruxy/usage/runs.json` — the single usage store file. */
|
|
15
|
+
export declare function usageStorePath(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Load and validate the store. Never throws. A missing file → empty, no error.
|
|
18
|
+
* Invalid JSON or a schema mismatch → empty + a `CRUXY_E_USAGE_READ` error
|
|
19
|
+
* (skip, don't crash). The raw text is parsed with `JSON.parse` only — a store
|
|
20
|
+
* file is never executed, so a hand-edited file cannot run code.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadUsage(file?: string): {
|
|
23
|
+
data: UsageFile;
|
|
24
|
+
error?: CruxyError;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Append one run's usage, pruning oldest-first to the last `retention` runs.
|
|
28
|
+
* Never throws — any failure (a corrupt existing file that cannot be safely
|
|
29
|
+
* appended to, or a write error) is returned as a coded error for the caller to
|
|
30
|
+
* downgrade to a warning, so persistence never takes a run down with it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function appendRun(record: UsageRecord, opts?: {
|
|
33
|
+
retention: number;
|
|
34
|
+
file?: string;
|
|
35
|
+
}): {
|
|
36
|
+
error?: CruxyError;
|
|
37
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { globalDir } from "../config/paths.js";
|
|
4
|
+
import { USAGE_DIR_NAME, USAGE_FILE_NAME } from "../constants.js";
|
|
5
|
+
import { usageRead } from "../errors/index.js";
|
|
6
|
+
import { UsageFileSchema, USAGE_FILE_VERSION, } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* The usage store (C.22): a single JSON file under `~/.cruxy/usage`, holding a
|
|
9
|
+
* bounded, newest-last list of run records. Pure DATA — parsed with the strict
|
|
10
|
+
* schema, NEVER eval'd — and written `0600` (personal accounting). This module
|
|
11
|
+
* touches only the local filesystem; it makes ZERO network calls.
|
|
12
|
+
*
|
|
13
|
+
* Reads are non-fatal by contract: a missing file is empty (not an error); a
|
|
14
|
+
* corrupt/unreadable one yields an empty result plus a coded
|
|
15
|
+
* {@link usageRead} error the caller can surface and SKIP — a broken usage file
|
|
16
|
+
* never crashes a run.
|
|
17
|
+
*/
|
|
18
|
+
/** `~/.cruxy/usage/runs.json` — the single usage store file. */
|
|
19
|
+
export function usageStorePath() {
|
|
20
|
+
return path.join(globalDir(), USAGE_DIR_NAME, USAGE_FILE_NAME);
|
|
21
|
+
}
|
|
22
|
+
/** An empty (fresh) store. */
|
|
23
|
+
function emptyFile() {
|
|
24
|
+
return { version: USAGE_FILE_VERSION, runs: [] };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load and validate the store. Never throws. A missing file → empty, no error.
|
|
28
|
+
* Invalid JSON or a schema mismatch → empty + a `CRUXY_E_USAGE_READ` error
|
|
29
|
+
* (skip, don't crash). The raw text is parsed with `JSON.parse` only — a store
|
|
30
|
+
* file is never executed, so a hand-edited file cannot run code.
|
|
31
|
+
*/
|
|
32
|
+
export function loadUsage(file = usageStorePath()) {
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = readFileSync(file, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Missing / unreadable → nothing recorded yet (not an error).
|
|
39
|
+
return { data: emptyFile() };
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { data: emptyFile(), error: usageRead(file, "not valid JSON", err) };
|
|
47
|
+
}
|
|
48
|
+
const result = UsageFileSchema.safeParse(parsed);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
return {
|
|
51
|
+
data: emptyFile(),
|
|
52
|
+
error: usageRead(file, result.error.issues[0]?.message ?? "invalid shape"),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { data: result.data };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Append one run's usage, pruning oldest-first to the last `retention` runs.
|
|
59
|
+
* Never throws — any failure (a corrupt existing file that cannot be safely
|
|
60
|
+
* appended to, or a write error) is returned as a coded error for the caller to
|
|
61
|
+
* downgrade to a warning, so persistence never takes a run down with it.
|
|
62
|
+
*/
|
|
63
|
+
export function appendRun(record, opts = { retention: 50 }) {
|
|
64
|
+
const file = opts.file ?? usageStorePath();
|
|
65
|
+
// A corrupt existing file is NOT overwritten — surface it and skip, rather
|
|
66
|
+
// than silently destroying whatever the user has (or blindly appending to a
|
|
67
|
+
// shape we couldn't validate).
|
|
68
|
+
const loaded = loadUsage(file);
|
|
69
|
+
if (loaded.error)
|
|
70
|
+
return { error: loaded.error };
|
|
71
|
+
const runs = [...loaded.data.runs, record];
|
|
72
|
+
const retention = Math.max(1, Math.floor(opts.retention));
|
|
73
|
+
const pruned = runs.slice(-retention); // keep the newest `retention` runs
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
76
|
+
const body = JSON.stringify({ version: USAGE_FILE_VERSION, runs: pruned }, null, 2);
|
|
77
|
+
writeFileSync(file, body, { mode: 0o600 });
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return { error: usageRead(file, "could not write the usage store", err) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Theme } from "../theme/index.js";
|
|
2
|
+
import type { PriceTable, UsageRecord, UsageSummary } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Usage aggregation + rendering (C.22). Aggregation sums only KNOWN token
|
|
5
|
+
* counts and counts the requests that reported none separately, so a total is
|
|
6
|
+
* never inflated by a fabricated zero — and the renderer ALWAYS surfaces that
|
|
7
|
+
* count, so a total can never be misread as complete while requests are silently
|
|
8
|
+
* excluded. Costs appear only for priced tiers. Only tier names ever reach the
|
|
9
|
+
* output (U.8 gag). Pure — no I/O, no network.
|
|
10
|
+
*/
|
|
11
|
+
export interface SummarizeOptions {
|
|
12
|
+
prices: PriceTable;
|
|
13
|
+
/** Currency label to prefix costs; "" when the user configured none. */
|
|
14
|
+
currency: string;
|
|
15
|
+
}
|
|
16
|
+
/** Aggregate one or more run records into a {@link UsageSummary}. */
|
|
17
|
+
export declare function summarizeRuns(runs: readonly UsageRecord[], opts: SummarizeOptions): UsageSummary;
|
|
18
|
+
/**
|
|
19
|
+
* Format a cost figure honestly: enough precision for small per-run costs,
|
|
20
|
+
* trailing zeros trimmed. Prefixed by the currency label only when one is set
|
|
21
|
+
* (never an assumed symbol). Callers pass a cost only when it was actually
|
|
22
|
+
* computed (priced + known tokens).
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatCost(n: number, currency: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Render a {@link UsageSummary} to a single themed line. Honors the theme
|
|
27
|
+
* end-to-end: NO_COLOR yields zero ANSI (identity stylers), and screen-reader
|
|
28
|
+
* mode swaps the arrow glyphs for words. The count of requests that reported no
|
|
29
|
+
* usage is ALWAYS shown when non-zero, so the total is never mistaken for a
|
|
30
|
+
* complete accounting.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderSummary(summary: UsageSummary, t: Theme): string;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { formatTokens } from "../render/state.js";
|
|
2
|
+
import { costFor } from "./cost.js";
|
|
3
|
+
/** Aggregate one or more run records into a {@link UsageSummary}. */
|
|
4
|
+
export function summarizeRuns(runs, opts) {
|
|
5
|
+
const byTier = new Map();
|
|
6
|
+
let totalInputTokens;
|
|
7
|
+
let totalOutputTokens;
|
|
8
|
+
let requests = 0;
|
|
9
|
+
let requestsWithoutUsage = 0;
|
|
10
|
+
const addKnown = (acc, v) => (v === undefined ? acc : (acc ?? 0) + v);
|
|
11
|
+
for (const run of runs) {
|
|
12
|
+
for (const e of run.entries) {
|
|
13
|
+
requests++;
|
|
14
|
+
const known = e.inputTokens !== undefined || e.outputTokens !== undefined;
|
|
15
|
+
if (!known)
|
|
16
|
+
requestsWithoutUsage++;
|
|
17
|
+
totalInputTokens = addKnown(totalInputTokens, e.inputTokens);
|
|
18
|
+
totalOutputTokens = addKnown(totalOutputTokens, e.outputTokens);
|
|
19
|
+
// Per-tier attribution only for entries that carry a tier. Untiered
|
|
20
|
+
// requests (routing inert) still count toward totals — the total stays
|
|
21
|
+
// honest — but there is no tier label to bucket them under.
|
|
22
|
+
if (e.tier !== undefined) {
|
|
23
|
+
const b = byTier.get(e.tier) ?? {
|
|
24
|
+
requests: 0,
|
|
25
|
+
requestsWithoutUsage: 0,
|
|
26
|
+
};
|
|
27
|
+
b.requests++;
|
|
28
|
+
if (!known)
|
|
29
|
+
b.requestsWithoutUsage++;
|
|
30
|
+
b.inputTokens = addKnown(b.inputTokens, e.inputTokens);
|
|
31
|
+
b.outputTokens = addKnown(b.outputTokens, e.outputTokens);
|
|
32
|
+
byTier.set(e.tier, b);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const perTier = [...byTier.entries()].map(([tier, b]) => ({
|
|
37
|
+
tier,
|
|
38
|
+
inputTokens: b.inputTokens,
|
|
39
|
+
outputTokens: b.outputTokens,
|
|
40
|
+
requests: b.requests,
|
|
41
|
+
requestsWithoutUsage: b.requestsWithoutUsage,
|
|
42
|
+
cost: costFor(tier, b.inputTokens, b.outputTokens, opts.prices),
|
|
43
|
+
}));
|
|
44
|
+
const costs = perTier
|
|
45
|
+
.map((p) => p.cost)
|
|
46
|
+
.filter((c) => c !== undefined);
|
|
47
|
+
const priced = costs.length > 0;
|
|
48
|
+
const totalCost = priced ? costs.reduce((a, c) => a + c, 0) : undefined;
|
|
49
|
+
return {
|
|
50
|
+
perTier,
|
|
51
|
+
totalInputTokens,
|
|
52
|
+
totalOutputTokens,
|
|
53
|
+
totalCost,
|
|
54
|
+
priced,
|
|
55
|
+
currency: opts.currency,
|
|
56
|
+
requestsWithoutUsage,
|
|
57
|
+
requests,
|
|
58
|
+
runCount: runs.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Format a cost figure honestly: enough precision for small per-run costs,
|
|
63
|
+
* trailing zeros trimmed. Prefixed by the currency label only when one is set
|
|
64
|
+
* (never an assumed symbol). Callers pass a cost only when it was actually
|
|
65
|
+
* computed (priced + known tokens).
|
|
66
|
+
*/
|
|
67
|
+
export function formatCost(n, currency) {
|
|
68
|
+
const abs = Math.abs(n);
|
|
69
|
+
const decimals = abs >= 1 ? 2 : abs >= 0.01 ? 4 : 6;
|
|
70
|
+
const trimmed = n.toFixed(decimals).replace(/\.?0+$/, "");
|
|
71
|
+
return `${currency}${trimmed === "" || trimmed === "-0" ? "0" : trimmed}`;
|
|
72
|
+
}
|
|
73
|
+
/** `↑1.2k ↓340` (worded `up 1.2k down 340` under a screen reader). */
|
|
74
|
+
function tokenText(input, output, t) {
|
|
75
|
+
const g = t.glyph;
|
|
76
|
+
return `${g.caretUp} ${formatTokens(input ?? 0)} ${g.caretDown} ${formatTokens(output ?? 0)}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Render a {@link UsageSummary} to a single themed line. Honors the theme
|
|
80
|
+
* end-to-end: NO_COLOR yields zero ANSI (identity stylers), and screen-reader
|
|
81
|
+
* mode swaps the arrow glyphs for words. The count of requests that reported no
|
|
82
|
+
* usage is ALWAYS shown when non-zero, so the total is never mistaken for a
|
|
83
|
+
* complete accounting.
|
|
84
|
+
*/
|
|
85
|
+
export function renderSummary(summary, t) {
|
|
86
|
+
const sep = t.sep;
|
|
87
|
+
const parts = [t.strong("usage")];
|
|
88
|
+
for (const p of summary.perTier) {
|
|
89
|
+
const known = p.inputTokens !== undefined || p.outputTokens !== undefined;
|
|
90
|
+
if (known) {
|
|
91
|
+
const cost = p.cost !== undefined ? ` ${formatCost(p.cost, summary.currency)}` : "";
|
|
92
|
+
parts.push(`${p.tier} ${tokenText(p.inputTokens, p.outputTokens, t)}${t.muted(cost)}`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Every request on this tier reported nothing — say so, don't show 0.
|
|
96
|
+
parts.push(`${p.tier} ${t.muted(unreported(p.requestsWithoutUsage))}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Total: known figures only. A run with no reported usage at all shows `—`,
|
|
100
|
+
// never a fabricated 0.
|
|
101
|
+
const totalKnown = summary.totalInputTokens !== undefined ||
|
|
102
|
+
summary.totalOutputTokens !== undefined;
|
|
103
|
+
const totalCost = summary.totalCost !== undefined
|
|
104
|
+
? ` ${formatCost(summary.totalCost, summary.currency)}`
|
|
105
|
+
: "";
|
|
106
|
+
parts.push(totalKnown
|
|
107
|
+
? `${t.strong("total")} ${tokenText(summary.totalInputTokens, summary.totalOutputTokens, t)}${t.muted(totalCost)}`
|
|
108
|
+
: `${t.strong("total")} —`);
|
|
109
|
+
// The honesty guard on display: a visible note whenever any request went
|
|
110
|
+
// unreported, so the total above is never read as the whole story.
|
|
111
|
+
if (summary.requestsWithoutUsage > 0) {
|
|
112
|
+
parts.push(t.warning(unreported(summary.requestsWithoutUsage)));
|
|
113
|
+
}
|
|
114
|
+
return parts.join(sep);
|
|
115
|
+
}
|
|
116
|
+
/** `2 requests: usage not reported`. */
|
|
117
|
+
function unreported(n) {
|
|
118
|
+
return `${n} request${n === 1 ? "" : "s"}: usage not reported`;
|
|
119
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MODEL_TIERS } from "../brand/voice.js";
|
|
3
|
+
/**
|
|
4
|
+
* Usage telemetry + cost tracking (C.22): types for LOCAL, honest usage
|
|
5
|
+
* accounting. Every number here is a real figure the provider reported — a token
|
|
6
|
+
* count is present only when the provider returned usage for that request, and
|
|
7
|
+
* `undefined` (never `0`) when it did not. Nothing in this module is ever
|
|
8
|
+
* transmitted anywhere (see the module barrel's no-phone-home note).
|
|
9
|
+
*/
|
|
10
|
+
/** Bump when the on-disk usage file shape changes (enables future migration). */
|
|
11
|
+
export declare const USAGE_FILE_VERSION: 1;
|
|
12
|
+
/**
|
|
13
|
+
* One model request's usage. A request that reported no usage keeps
|
|
14
|
+
* `inputTokens`/`outputTokens` as `undefined` — the honest "unknown", never
|
|
15
|
+
* zero-filled or re-tokenized. A provider-reported `0` is stored as `0` (a real
|
|
16
|
+
* count), so the two cases stay distinguishable downstream.
|
|
17
|
+
*/
|
|
18
|
+
export declare const UsageEntrySchema: z.ZodObject<{
|
|
19
|
+
/** The routing tier (C.30) this request ran on; absent when routing is inert. */
|
|
20
|
+
tier: z.ZodOptional<z.ZodString>;
|
|
21
|
+
/** Provider-reported prompt tokens; `undefined` ⇔ no usage was reported. */
|
|
22
|
+
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
24
|
+
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
25
|
+
/** ISO-8601 timestamp the request completed. */
|
|
26
|
+
at: z.ZodString;
|
|
27
|
+
}, "strict", z.ZodTypeAny, {
|
|
28
|
+
at: string;
|
|
29
|
+
tier?: string | undefined;
|
|
30
|
+
inputTokens?: number | undefined;
|
|
31
|
+
outputTokens?: number | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
at: string;
|
|
34
|
+
tier?: string | undefined;
|
|
35
|
+
inputTokens?: number | undefined;
|
|
36
|
+
outputTokens?: number | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
export type UsageEntry = z.infer<typeof UsageEntrySchema>;
|
|
39
|
+
/** One run's usage: an ordered list of per-request entries. */
|
|
40
|
+
export declare const UsageRecordSchema: z.ZodObject<{
|
|
41
|
+
/** Unique id for this run (one `session.send`). */
|
|
42
|
+
runId: z.ZodString;
|
|
43
|
+
/** Groups runs of one interactive session, so `--session` aggregates them. */
|
|
44
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
45
|
+
/** ISO-8601 timestamp the run started. */
|
|
46
|
+
startedAt: z.ZodString;
|
|
47
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
48
|
+
/** The routing tier (C.30) this request ran on; absent when routing is inert. */
|
|
49
|
+
tier: z.ZodOptional<z.ZodString>;
|
|
50
|
+
/** Provider-reported prompt tokens; `undefined` ⇔ no usage was reported. */
|
|
51
|
+
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
52
|
+
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
53
|
+
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
/** ISO-8601 timestamp the request completed. */
|
|
55
|
+
at: z.ZodString;
|
|
56
|
+
}, "strict", z.ZodTypeAny, {
|
|
57
|
+
at: string;
|
|
58
|
+
tier?: string | undefined;
|
|
59
|
+
inputTokens?: number | undefined;
|
|
60
|
+
outputTokens?: number | undefined;
|
|
61
|
+
}, {
|
|
62
|
+
at: string;
|
|
63
|
+
tier?: string | undefined;
|
|
64
|
+
inputTokens?: number | undefined;
|
|
65
|
+
outputTokens?: number | undefined;
|
|
66
|
+
}>, "many">;
|
|
67
|
+
}, "strict", z.ZodTypeAny, {
|
|
68
|
+
entries: {
|
|
69
|
+
at: string;
|
|
70
|
+
tier?: string | undefined;
|
|
71
|
+
inputTokens?: number | undefined;
|
|
72
|
+
outputTokens?: number | undefined;
|
|
73
|
+
}[];
|
|
74
|
+
runId: string;
|
|
75
|
+
startedAt: string;
|
|
76
|
+
sessionId?: string | undefined;
|
|
77
|
+
}, {
|
|
78
|
+
entries: {
|
|
79
|
+
at: string;
|
|
80
|
+
tier?: string | undefined;
|
|
81
|
+
inputTokens?: number | undefined;
|
|
82
|
+
outputTokens?: number | undefined;
|
|
83
|
+
}[];
|
|
84
|
+
runId: string;
|
|
85
|
+
startedAt: string;
|
|
86
|
+
sessionId?: string | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
export type UsageRecord = z.infer<typeof UsageRecordSchema>;
|
|
89
|
+
/** The persisted store: a bounded, newest-last list of run records. */
|
|
90
|
+
export declare const UsageFileSchema: z.ZodObject<{
|
|
91
|
+
version: z.ZodLiteral<1>;
|
|
92
|
+
runs: z.ZodArray<z.ZodObject<{
|
|
93
|
+
/** Unique id for this run (one `session.send`). */
|
|
94
|
+
runId: z.ZodString;
|
|
95
|
+
/** Groups runs of one interactive session, so `--session` aggregates them. */
|
|
96
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
97
|
+
/** ISO-8601 timestamp the run started. */
|
|
98
|
+
startedAt: z.ZodString;
|
|
99
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
100
|
+
/** The routing tier (C.30) this request ran on; absent when routing is inert. */
|
|
101
|
+
tier: z.ZodOptional<z.ZodString>;
|
|
102
|
+
/** Provider-reported prompt tokens; `undefined` ⇔ no usage was reported. */
|
|
103
|
+
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
104
|
+
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
105
|
+
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
/** ISO-8601 timestamp the request completed. */
|
|
107
|
+
at: z.ZodString;
|
|
108
|
+
}, "strict", z.ZodTypeAny, {
|
|
109
|
+
at: string;
|
|
110
|
+
tier?: string | undefined;
|
|
111
|
+
inputTokens?: number | undefined;
|
|
112
|
+
outputTokens?: number | undefined;
|
|
113
|
+
}, {
|
|
114
|
+
at: string;
|
|
115
|
+
tier?: string | undefined;
|
|
116
|
+
inputTokens?: number | undefined;
|
|
117
|
+
outputTokens?: number | undefined;
|
|
118
|
+
}>, "many">;
|
|
119
|
+
}, "strict", z.ZodTypeAny, {
|
|
120
|
+
entries: {
|
|
121
|
+
at: string;
|
|
122
|
+
tier?: string | undefined;
|
|
123
|
+
inputTokens?: number | undefined;
|
|
124
|
+
outputTokens?: number | undefined;
|
|
125
|
+
}[];
|
|
126
|
+
runId: string;
|
|
127
|
+
startedAt: string;
|
|
128
|
+
sessionId?: string | undefined;
|
|
129
|
+
}, {
|
|
130
|
+
entries: {
|
|
131
|
+
at: string;
|
|
132
|
+
tier?: string | undefined;
|
|
133
|
+
inputTokens?: number | undefined;
|
|
134
|
+
outputTokens?: number | undefined;
|
|
135
|
+
}[];
|
|
136
|
+
runId: string;
|
|
137
|
+
startedAt: string;
|
|
138
|
+
sessionId?: string | undefined;
|
|
139
|
+
}>, "many">;
|
|
140
|
+
}, "strict", z.ZodTypeAny, {
|
|
141
|
+
version: 1;
|
|
142
|
+
runs: {
|
|
143
|
+
entries: {
|
|
144
|
+
at: string;
|
|
145
|
+
tier?: string | undefined;
|
|
146
|
+
inputTokens?: number | undefined;
|
|
147
|
+
outputTokens?: number | undefined;
|
|
148
|
+
}[];
|
|
149
|
+
runId: string;
|
|
150
|
+
startedAt: string;
|
|
151
|
+
sessionId?: string | undefined;
|
|
152
|
+
}[];
|
|
153
|
+
}, {
|
|
154
|
+
version: 1;
|
|
155
|
+
runs: {
|
|
156
|
+
entries: {
|
|
157
|
+
at: string;
|
|
158
|
+
tier?: string | undefined;
|
|
159
|
+
inputTokens?: number | undefined;
|
|
160
|
+
outputTokens?: number | undefined;
|
|
161
|
+
}[];
|
|
162
|
+
runId: string;
|
|
163
|
+
startedAt: string;
|
|
164
|
+
sessionId?: string | undefined;
|
|
165
|
+
}[];
|
|
166
|
+
}>;
|
|
167
|
+
export type UsageFile = z.infer<typeof UsageFileSchema>;
|
|
168
|
+
/**
|
|
169
|
+
* A per-tier price, in the user's own currency, PER MILLION TOKENS. Chosen over
|
|
170
|
+
* per-token so prices read the way LLM pricing is quoted and don't lose
|
|
171
|
+
* precision to tiny floats. Configured by the user under `usage.prices.<tier>`;
|
|
172
|
+
* cruxy ships NO prices (a bundled table would imply upstream identities and go
|
|
173
|
+
* stale) — with none configured, cost is omitted entirely.
|
|
174
|
+
*/
|
|
175
|
+
export interface TierPrice {
|
|
176
|
+
/** Price per 1,000,000 input tokens. */
|
|
177
|
+
input: number;
|
|
178
|
+
/** Price per 1,000,000 output tokens. */
|
|
179
|
+
output: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Prices keyed by TIER ONLY (kavi/vaani/mira) — the type literally cannot name
|
|
183
|
+
* an upstream model, so the U.8 gag holds by construction. Partial: any tier may
|
|
184
|
+
* be unpriced (its cost is then omitted, tokens still shown).
|
|
185
|
+
*/
|
|
186
|
+
export type PriceTable = Partial<Record<(typeof MODEL_TIERS)[number], TierPrice>>;
|
|
187
|
+
/** Aggregated usage for one tier across the summarized runs. */
|
|
188
|
+
export interface TierUsage {
|
|
189
|
+
tier: string;
|
|
190
|
+
/** Sum of KNOWN input tokens; `undefined` if no request on this tier reported usage. */
|
|
191
|
+
inputTokens?: number;
|
|
192
|
+
/** Sum of KNOWN output tokens; `undefined` if none reported. */
|
|
193
|
+
outputTokens?: number;
|
|
194
|
+
/** Requests attributed to this tier. */
|
|
195
|
+
requests: number;
|
|
196
|
+
/** How many of those reported no usage (surfaced, never silently dropped). */
|
|
197
|
+
requestsWithoutUsage: number;
|
|
198
|
+
/** Cost, ONLY when this tier is priced AND has known tokens; else omitted. */
|
|
199
|
+
cost?: number;
|
|
200
|
+
}
|
|
201
|
+
/** The aggregate over one or more runs — what the summary renderer consumes. */
|
|
202
|
+
export interface UsageSummary {
|
|
203
|
+
perTier: TierUsage[];
|
|
204
|
+
/** Sum of KNOWN input tokens across all runs; `undefined` if none known. */
|
|
205
|
+
totalInputTokens?: number;
|
|
206
|
+
/** Sum of KNOWN output tokens across all runs; `undefined` if none known. */
|
|
207
|
+
totalOutputTokens?: number;
|
|
208
|
+
/** Sum of per-tier costs; `undefined` unless ≥1 tier was priced. */
|
|
209
|
+
totalCost?: number;
|
|
210
|
+
/** True iff at least one tier had a configured price (cost is shown). */
|
|
211
|
+
priced: boolean;
|
|
212
|
+
/** Currency label to prefix costs with; "" when the user configured none. */
|
|
213
|
+
currency: string;
|
|
214
|
+
/** Total requests across all runs that reported no usage (rendered visibly). */
|
|
215
|
+
requestsWithoutUsage: number;
|
|
216
|
+
/** Total requests across all runs. */
|
|
217
|
+
requests: number;
|
|
218
|
+
/** How many runs were aggregated. */
|
|
219
|
+
runCount: number;
|
|
220
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Usage telemetry + cost tracking (C.22): types for LOCAL, honest usage
|
|
4
|
+
* accounting. Every number here is a real figure the provider reported — a token
|
|
5
|
+
* count is present only when the provider returned usage for that request, and
|
|
6
|
+
* `undefined` (never `0`) when it did not. Nothing in this module is ever
|
|
7
|
+
* transmitted anywhere (see the module barrel's no-phone-home note).
|
|
8
|
+
*/
|
|
9
|
+
/** Bump when the on-disk usage file shape changes (enables future migration). */
|
|
10
|
+
export const USAGE_FILE_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* One model request's usage. A request that reported no usage keeps
|
|
13
|
+
* `inputTokens`/`outputTokens` as `undefined` — the honest "unknown", never
|
|
14
|
+
* zero-filled or re-tokenized. A provider-reported `0` is stored as `0` (a real
|
|
15
|
+
* count), so the two cases stay distinguishable downstream.
|
|
16
|
+
*/
|
|
17
|
+
export const UsageEntrySchema = z
|
|
18
|
+
.object({
|
|
19
|
+
/** The routing tier (C.30) this request ran on; absent when routing is inert. */
|
|
20
|
+
tier: z.string().optional(),
|
|
21
|
+
/** Provider-reported prompt tokens; `undefined` ⇔ no usage was reported. */
|
|
22
|
+
inputTokens: z.number().int().nonnegative().optional(),
|
|
23
|
+
/** Provider-reported completion tokens; `undefined` ⇔ no usage was reported. */
|
|
24
|
+
outputTokens: z.number().int().nonnegative().optional(),
|
|
25
|
+
/** ISO-8601 timestamp the request completed. */
|
|
26
|
+
at: z.string(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
/** One run's usage: an ordered list of per-request entries. */
|
|
30
|
+
export const UsageRecordSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
/** Unique id for this run (one `session.send`). */
|
|
33
|
+
runId: z.string(),
|
|
34
|
+
/** Groups runs of one interactive session, so `--session` aggregates them. */
|
|
35
|
+
sessionId: z.string().optional(),
|
|
36
|
+
/** ISO-8601 timestamp the run started. */
|
|
37
|
+
startedAt: z.string(),
|
|
38
|
+
entries: z.array(UsageEntrySchema),
|
|
39
|
+
})
|
|
40
|
+
.strict();
|
|
41
|
+
/** The persisted store: a bounded, newest-last list of run records. */
|
|
42
|
+
export const UsageFileSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
version: z.literal(USAGE_FILE_VERSION),
|
|
45
|
+
runs: z.array(UsageRecordSchema),
|
|
46
|
+
})
|
|
47
|
+
.strict();
|