@askalf/dario 6.9.3 → 6.10.1
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 +1 -1
- package/dist/admin-api.d.ts +8 -1
- package/dist/admin-api.js +61 -5
- package/dist/cc-template.d.ts +8 -0
- package/dist/cc-template.js +12 -0
- package/dist/cli.js +65 -6
- package/dist/keys.d.ts +109 -0
- package/dist/keys.js +140 -1
- package/dist/ledger.d.ts +17 -0
- package/dist/ledger.js +29 -0
- package/dist/metrics.d.ts +7 -0
- package/dist/metrics.js +8 -0
- package/dist/pool.d.ts +50 -5
- package/dist/pool.js +138 -27
- package/dist/proxy.js +115 -9
- package/docs/keys.md +45 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -382,7 +382,7 @@ dario keys create alice
|
|
|
382
382
|
dario keys create bob --seat=bobs-max --models=claude-sonnet-5,claude-haiku*
|
|
383
383
|
```
|
|
384
384
|
|
|
385
|
-
The secret is printed once and only its hash is kept, in `~/.dario/keys.json`. The request authenticated with alice's key *is* alice's in `/analytics`, in the ledger (`dario usage --by-key`) and on every log line; a key can prefer one pool seat (taken while it has headroom, normal routing otherwise, so a developer's conversations ride their own subscription) and can be held to a model allowlist (`403` before anything goes upstream, in either wire shape). The running proxy picks up a created, rotated or revoked key on its next request; the root `DARIO_API_KEY` keeps working beside them; `/admin/keys` does the same over HTTP. Details: [keys.md](./docs/keys.md).
|
|
385
|
+
Since 6.10 a key can carry a **daily budget** — `--budget=$5/day`, `--budget-tokens=2M/day` — read from the ledger, refused with a `429` and a `retry-after` at UTC midnight, with the headroom on every response as `x-dario-budget-*` headers ([details](./docs/keys.md#budgets)). The secret is printed once and only its hash is kept, in `~/.dario/keys.json`. The request authenticated with alice's key *is* alice's in `/analytics`, in the ledger (`dario usage --by-key`) and on every log line; a key can prefer one pool seat (taken while it has headroom, normal routing otherwise, so a developer's conversations ride their own subscription) and can be held to a model allowlist (`403` before anything goes upstream, in either wire shape). The running proxy picks up a created, rotated or revoked key on its next request; the root `DARIO_API_KEY` keeps working beside them; `/admin/keys` does the same over HTTP. Details: [keys.md](./docs/keys.md).
|
|
386
386
|
|
|
387
387
|
### Watch it happen
|
|
388
388
|
|
package/dist/admin-api.d.ts
CHANGED
|
@@ -122,6 +122,13 @@ export interface AdminAccountLive {
|
|
|
122
122
|
rejectedCount: number;
|
|
123
123
|
/** Epoch ms of the most recent 429 on this account, or `null` if never. */
|
|
124
124
|
lastRejectedAt: number | null;
|
|
125
|
+
/**
|
|
126
|
+
* Per-model buckets keeping their families off this seat while the seat
|
|
127
|
+
* itself still serves (`['oi']`: Fable parked on a Pro seat whose included
|
|
128
|
+
* overage is spent, Opus unaffected). Empty when none. Optional: a peer's
|
|
129
|
+
* snapshot from before the field behaves as before.
|
|
130
|
+
*/
|
|
131
|
+
parkedBuckets?: string[];
|
|
125
132
|
/** Organization observed on this seat's responses, or `null` if none yet (dario#1244). */
|
|
126
133
|
organizationId: string | null;
|
|
127
134
|
/**
|
|
@@ -158,7 +165,7 @@ export interface AdminCodexAccountRecord extends CodexSeatState {
|
|
|
158
165
|
needsRefresh: boolean;
|
|
159
166
|
}
|
|
160
167
|
export interface AdminAuditEvent {
|
|
161
|
-
action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited' | 'key_create' | 'key_revoke' | 'key_rotate';
|
|
168
|
+
action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited' | 'key_create' | 'key_revoke' | 'key_rotate' | 'key_budget';
|
|
162
169
|
/** Which engine's credentials the event touched; absent means Claude, the only engine before codex joined (dario#1009). */
|
|
163
170
|
engine?: 'codex';
|
|
164
171
|
ok: boolean;
|
package/dist/admin-api.js
CHANGED
|
@@ -4,7 +4,27 @@ import { startAddAccount, completeAddAccount, removeAccount, listAccountAliases,
|
|
|
4
4
|
import { startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, loadAllCodexAccounts, listCodexAccountAliases, codexAccountNeedsRefresh, codexSeatStatus, parseCodexManualPaste, } from './codex-accounts.js';
|
|
5
5
|
import { parseManualPaste } from './oauth.js';
|
|
6
6
|
import { grantAge } from './refresh-grant.js';
|
|
7
|
-
import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, KEY_NAME_RE } from './keys.js';
|
|
7
|
+
import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, setKeyBudget, normalizeBudget, KEY_NAME_RE } from './keys.js';
|
|
8
|
+
/**
|
|
9
|
+
* `{ budget_usd_per_day, budget_tokens_per_day }` (numbers, or numeric strings)
|
|
10
|
+
* → a KeyBudget; undefined when neither is present; throws with a 400-worthy
|
|
11
|
+
* message when one is present and not a positive number.
|
|
12
|
+
*/
|
|
13
|
+
function budgetFromBody(body) {
|
|
14
|
+
const num = (v, field) => {
|
|
15
|
+
if (v === undefined || v === null || v === '')
|
|
16
|
+
return undefined;
|
|
17
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? Number(v.replace(/^\$/, '')) : NaN;
|
|
18
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
19
|
+
throw new Error(`invalid "${field}": a positive number`);
|
|
20
|
+
return n;
|
|
21
|
+
};
|
|
22
|
+
const usdPerDay = num(body.budget_usd_per_day, 'budget_usd_per_day');
|
|
23
|
+
const tokensPerDay = num(body.budget_tokens_per_day, 'budget_tokens_per_day');
|
|
24
|
+
if (usdPerDay === undefined && tokensPerDay === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
return normalizeBudget({ usdPerDay, tokensPerDay });
|
|
27
|
+
}
|
|
8
28
|
const PENDING_TTL_MS = 10 * 60_000;
|
|
9
29
|
const MAX_PENDING = 64; // backstop against unbounded growth (distinct aliases)
|
|
10
30
|
const ACCOUNTS_PREFIX = '/admin/accounts/';
|
|
@@ -244,8 +264,10 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
244
264
|
? decodeURIComponent(urlPath.slice(KEYS_PREFIX.length))
|
|
245
265
|
: null;
|
|
246
266
|
const isKeyRotate = keyTarget !== null && keyTarget.endsWith('/rotate');
|
|
247
|
-
|
|
248
|
-
const
|
|
267
|
+
// POST /admin/keys/<name>/budget — set or clear a key's daily caps (dario#1318 follow-up).
|
|
268
|
+
const isKeyBudget = keyTarget !== null && keyTarget.endsWith('/budget');
|
|
269
|
+
const keyName = keyTarget === null ? null : isKeyRotate ? keyTarget.slice(0, -'/rotate'.length) : isKeyBudget ? keyTarget.slice(0, -'/budget'.length) : keyTarget;
|
|
270
|
+
const isKeyRevoke = keyTarget !== null && !isKeyRotate && !isKeyBudget && method === 'DELETE';
|
|
249
271
|
const known = urlPath === '/admin/login/start' ||
|
|
250
272
|
urlPath === '/admin/login/start-needed' ||
|
|
251
273
|
urlPath === '/admin/login/complete' ||
|
|
@@ -257,6 +279,7 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
257
279
|
isCodexAccountDelete ||
|
|
258
280
|
isAccountDelete ||
|
|
259
281
|
isKeyRotate ||
|
|
282
|
+
isKeyBudget ||
|
|
260
283
|
isKeyRevoke;
|
|
261
284
|
if (!known)
|
|
262
285
|
return false;
|
|
@@ -289,7 +312,7 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
289
312
|
// accounts' credentials for the price of one throttle token.
|
|
290
313
|
const isMutation = urlPath === '/admin/login/start' || isAccountDelete
|
|
291
314
|
|| urlPath === '/admin/codex/login/start' || isCodexAccountDelete
|
|
292
|
-
|| (urlPath === '/admin/keys' && method === 'POST') || isKeyRotate || isKeyRevoke;
|
|
315
|
+
|| (urlPath === '/admin/keys' && method === 'POST') || isKeyRotate || isKeyBudget || isKeyRevoke;
|
|
293
316
|
if (isMutation) {
|
|
294
317
|
const wait = deps.rateLimit?.('mutation') ?? 0;
|
|
295
318
|
if (wait > 0) {
|
|
@@ -546,6 +569,7 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
546
569
|
request_count: l.requestCount,
|
|
547
570
|
rejected_count: l.rejectedCount ?? 0,
|
|
548
571
|
last_rejected_at: l.lastRejectedAt ?? null,
|
|
572
|
+
parked_buckets: l.parkedBuckets ?? [],
|
|
549
573
|
consecutive_auth_failures: l.consecutiveAuthFailures,
|
|
550
574
|
} : {}),
|
|
551
575
|
};
|
|
@@ -609,8 +633,16 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
609
633
|
}
|
|
610
634
|
expiresAt = parsed;
|
|
611
635
|
}
|
|
636
|
+
let budget;
|
|
637
|
+
try {
|
|
638
|
+
budget = budgetFromBody(body);
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
send(res, 400, { error: err.message });
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
612
644
|
try {
|
|
613
|
-
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, now }));
|
|
645
|
+
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, now }));
|
|
614
646
|
deps.audit?.({ action: 'key_create', ok: true, status: 201, key: made.record.name, remote, detail: seat ? `seat=${seat}` : undefined });
|
|
615
647
|
send(res, 201, { key: publicKey(made.record, now), secret: made.secret, note: 'the secret is shown once and is not stored' });
|
|
616
648
|
}
|
|
@@ -645,6 +677,30 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
|
|
|
645
677
|
send(res, 200, { key: publicKey(rotated.record, now), secret: rotated.secret, note: 'the secret is shown once and is not stored' });
|
|
646
678
|
return true;
|
|
647
679
|
}
|
|
680
|
+
// POST /admin/keys/<name>/budget { budget_usd_per_day?, budget_tokens_per_day? } — both absent/null clears.
|
|
681
|
+
if (isKeyBudget) {
|
|
682
|
+
if (method !== 'POST') {
|
|
683
|
+
send(res, 405, { error: 'Method not allowed (use POST)' });
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
const body = await readJsonBody(req);
|
|
687
|
+
let budget;
|
|
688
|
+
try {
|
|
689
|
+
budget = budgetFromBody(body);
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
send(res, 400, { error: err.message });
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const updated = store.mutate((file) => setKeyBudget(file, keyName, budget ?? null));
|
|
696
|
+
deps.audit?.({ action: 'key_budget', ok: updated !== null, status: updated ? 200 : 404, key: keyName, remote, detail: budget ? JSON.stringify(budget) : 'cleared' });
|
|
697
|
+
if (!updated) {
|
|
698
|
+
send(res, 404, { error: `no key named "${keyName}"` });
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
send(res, 200, { key: publicKey(updated, now) });
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
648
704
|
// DELETE /admin/keys/<name> — revoked, kept for the list.
|
|
649
705
|
if (isKeyRevoke) {
|
|
650
706
|
const revoked = store.mutate((file) => revokeKey(file, keyName));
|
package/dist/cc-template.d.ts
CHANGED
|
@@ -48,6 +48,14 @@ export declare const CC_TOOL_DEFINITIONS: {
|
|
|
48
48
|
export declare const CC_TOOL_DEFINITIONS_UNION: {
|
|
49
49
|
name: string;
|
|
50
50
|
}[];
|
|
51
|
+
/**
|
|
52
|
+
* The most the template can add to an outbound prompt, in bytes: the largest
|
|
53
|
+
* system prompt the bundle carries plus every advertisable tool definition.
|
|
54
|
+
* The key-budget reservation (keys.ts requestBudgetReservation) adds this to
|
|
55
|
+
* the client's own body so a request is bounded by what dario SENDS, not by
|
|
56
|
+
* what the client sent — the template's prompt is billed to the key too.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CC_TEMPLATE_PROMPT_BYTES: number;
|
|
51
59
|
/** Every name the bundle knows — including one whose definition is not advertisable (dario#1376). */
|
|
52
60
|
export declare const CC_NATIVE_NAMES_UNION: Set<string>;
|
|
53
61
|
/** CC's own tool names, EXACT case ("Read", "Bash", "Agent", …). A CC client's
|
package/dist/cc-template.js
CHANGED
|
@@ -62,6 +62,18 @@ export const CC_TOOL_DEFINITIONS = filterToolsForPlatform(TEMPLATE.tools.filter(
|
|
|
62
62
|
* with no client declaration to mirror: the full-template fallback, the
|
|
63
63
|
* merge-mode base array, and Fable's no-tools shape. */
|
|
64
64
|
export const CC_TOOL_DEFINITIONS_UNION = TEMPLATE.tools.filter(isAdvertisableToolDefinition);
|
|
65
|
+
/**
|
|
66
|
+
* The most the template can add to an outbound prompt, in bytes: the largest
|
|
67
|
+
* system prompt the bundle carries plus every advertisable tool definition.
|
|
68
|
+
* The key-budget reservation (keys.ts requestBudgetReservation) adds this to
|
|
69
|
+
* the client's own body so a request is bounded by what dario SENDS, not by
|
|
70
|
+
* what the client sent — the template's prompt is billed to the key too.
|
|
71
|
+
*/
|
|
72
|
+
export const CC_TEMPLATE_PROMPT_BYTES = (() => {
|
|
73
|
+
const t = TEMPLATE;
|
|
74
|
+
const sizes = [JSON.stringify(t.system_prompt ?? '').length, ...Object.values(t.system_prompt_variants ?? {}).map((v) => JSON.stringify(v ?? '').length)];
|
|
75
|
+
return Math.max(0, ...sizes) + JSON.stringify(CC_TOOL_DEFINITIONS_UNION).length;
|
|
76
|
+
})();
|
|
65
77
|
/** Every name the bundle knows — including one whose definition is not advertisable (dario#1376). */
|
|
66
78
|
export const CC_NATIVE_NAMES_UNION = new Set(TEMPLATE.tools.map((t) => String(t.name)));
|
|
67
79
|
/** CC's own tool names, EXACT case ("Read", "Bash", "Agent", …). A CC client's
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,38 @@
|
|
|
19
19
|
import { unlink, writeFile } from 'node:fs/promises';
|
|
20
20
|
import { formatLedgerSummary, formatLedgerConsumers, formatUsd, renderLedgerCard, readLedgerFile, resolveLedgerPath, summarizeLedger } from './ledger.js';
|
|
21
21
|
import { renderSpendDonuts } from './donuts.js';
|
|
22
|
-
import { KeyStore, createKey, revokeKey, rotateKey, deleteKey, parseExpiry, publicKey, resolveKeysPath, KEY_NAME_RE } from './keys.js';
|
|
22
|
+
import { KeyStore, createKey, revokeKey, rotateKey, deleteKey, parseExpiry, publicKey, resolveKeysPath, KEY_NAME_RE, setKeyBudget, parseUsdBudget, parseTokenBudget, formatBudget } from './keys.js';
|
|
23
|
+
/**
|
|
24
|
+
* `--budget=$5/day` / `--budget-tokens=2M/day` → a KeyBudget, or undefined when
|
|
25
|
+
* neither flag is present. A flag that does not parse exits 1 with the accepted
|
|
26
|
+
* forms, like --expires does.
|
|
27
|
+
*/
|
|
28
|
+
function readBudgetFlags(args) {
|
|
29
|
+
const usdArg = args.find((a) => a.startsWith('--budget='));
|
|
30
|
+
const tokArg = args.find((a) => a.startsWith('--budget-tokens='));
|
|
31
|
+
if (!usdArg && !tokArg)
|
|
32
|
+
return undefined;
|
|
33
|
+
const budget = {};
|
|
34
|
+
if (usdArg) {
|
|
35
|
+
const v = usdArg.slice('--budget='.length);
|
|
36
|
+
const n = parseUsdBudget(v);
|
|
37
|
+
if (n === null) {
|
|
38
|
+
console.error(`[dario] --budget: "${v}" is not a dollar amount per day ($5, 5.00, $5/day).`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
budget.usdPerDay = n;
|
|
42
|
+
}
|
|
43
|
+
if (tokArg) {
|
|
44
|
+
const v = tokArg.slice('--budget-tokens='.length);
|
|
45
|
+
const n = parseTokenBudget(v);
|
|
46
|
+
if (n === null) {
|
|
47
|
+
console.error(`[dario] --budget-tokens: "${v}" is not a token count per day (250k, 2M, 2000000).`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
budget.tokensPerDay = n;
|
|
51
|
+
}
|
|
52
|
+
return budget;
|
|
53
|
+
}
|
|
23
54
|
import { loadAllAccounts as loadAllAccountsForIdentity, regenerateClientIdentity } from './accounts.js';
|
|
24
55
|
import { maskEmail, parsePoolHeadroomFloor } from './pool.js';
|
|
25
56
|
import { realpathSync, readFileSync } from 'node:fs';
|
|
@@ -785,9 +816,9 @@ async function keys() {
|
|
|
785
816
|
return;
|
|
786
817
|
}
|
|
787
818
|
const w = Math.max(4, ...list.map((k) => k.name.length));
|
|
788
|
-
console.log(` ${'NAME'.padEnd(w)} ${'STATUS'.padEnd(7)} ${'SEAT'.padEnd(12)} ${'LAST USED'.padEnd(10)} ${'EXPIRES'.padEnd(10)} MODELS`);
|
|
819
|
+
console.log(` ${'NAME'.padEnd(w)} ${'STATUS'.padEnd(7)} ${'SEAT'.padEnd(12)} ${'LAST USED'.padEnd(10)} ${'EXPIRES'.padEnd(10)} ${'BUDGET'.padEnd(20)} MODELS`);
|
|
789
820
|
for (const k of list) {
|
|
790
|
-
console.log(` ${k.name.padEnd(w)} ${k.status.padEnd(7)} ${(k.seat ?? '-').padEnd(12)} ${fmtAgo(k.last_used).padEnd(10)} ${fmtDay(k.expires).padEnd(10)} ${k.models.length ? k.models.join(', ') : 'any'}`);
|
|
821
|
+
console.log(` ${k.name.padEnd(w)} ${k.status.padEnd(7)} ${(k.seat ?? '-').padEnd(12)} ${fmtAgo(k.last_used).padEnd(10)} ${fmtDay(k.expires).padEnd(10)} ${formatBudget(k.budget ? { usdPerDay: k.budget.usd_per_day ?? undefined, tokensPerDay: k.budget.tokens_per_day ?? undefined } : null).padEnd(20)} ${k.models.length ? k.models.join(', ') : 'any'}`);
|
|
791
822
|
}
|
|
792
823
|
console.log('');
|
|
793
824
|
console.log(` ${list.length} key${list.length === 1 ? '' : 's'} in ${path}. Spend per key: dario usage --by-key`);
|
|
@@ -798,7 +829,7 @@ async function keys() {
|
|
|
798
829
|
const name = args[2];
|
|
799
830
|
if (!name || name.startsWith('--')) {
|
|
800
831
|
console.error('');
|
|
801
|
-
console.error(' Usage: dario keys create <name> [--seat=<alias>] [--models=a,b,prefix*] [--expires=30d|12h|2w|<ISO date>]');
|
|
832
|
+
console.error(' Usage: dario keys create <name> [--seat=<alias>] [--models=a,b,prefix*] [--expires=30d|12h|2w|<ISO date>] [--budget=$5/day] [--budget-tokens=2M/day]');
|
|
802
833
|
console.error('');
|
|
803
834
|
process.exit(1);
|
|
804
835
|
}
|
|
@@ -816,8 +847,9 @@ async function keys() {
|
|
|
816
847
|
}
|
|
817
848
|
expiresAt = parsed;
|
|
818
849
|
}
|
|
850
|
+
const budget = readBudgetFlags(args);
|
|
819
851
|
try {
|
|
820
|
-
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, now }));
|
|
852
|
+
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, now }));
|
|
821
853
|
printSecret('created', publicKey(made.record, now), made.secret);
|
|
822
854
|
}
|
|
823
855
|
catch (err) {
|
|
@@ -826,6 +858,33 @@ async function keys() {
|
|
|
826
858
|
}
|
|
827
859
|
return;
|
|
828
860
|
}
|
|
861
|
+
// dario keys budget <name> --budget=$5/day --budget-tokens=2M/day | --clear
|
|
862
|
+
if (sub === 'budget') {
|
|
863
|
+
const name = args[2];
|
|
864
|
+
if (!name || !KEY_NAME_RE.test(name) || (!args.includes('--clear') && !args.some((a) => a.startsWith('--budget=') || a.startsWith('--budget-tokens=')))) {
|
|
865
|
+
console.error('');
|
|
866
|
+
console.error(' Usage: dario keys budget <name> [--budget=$5/day] [--budget-tokens=2M/day] | --clear');
|
|
867
|
+
console.error('');
|
|
868
|
+
console.error(' Caps are per UTC day and read from the ledger (dario usage --by-key); a request that');
|
|
869
|
+
console.error(' would start past a cap is refused with 429 until midnight UTC.');
|
|
870
|
+
console.error('');
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
const budget = args.includes('--clear') ? null : (readBudgetFlags(args) ?? null);
|
|
875
|
+
const updated = store.mutate((file) => setKeyBudget(file, name, budget));
|
|
876
|
+
if (!updated) {
|
|
877
|
+
console.error(`[dario] No key named "${name}".`);
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
console.log(budget ? `[dario] Key "${name}" budget: ${formatBudget(updated.budget)}` : `[dario] Key "${name}" budget cleared.`);
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
console.error(`[dario] ${err instanceof Error ? err.message : String(err)}`);
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
829
888
|
if (sub === 'rotate' || sub === 'revoke' || sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
830
889
|
const name = args[2];
|
|
831
890
|
if (!name || !KEY_NAME_RE.test(name)) {
|
|
@@ -867,7 +926,7 @@ async function keys() {
|
|
|
867
926
|
return;
|
|
868
927
|
}
|
|
869
928
|
console.error(`[dario] Unknown keys subcommand: ${sub}`);
|
|
870
|
-
console.error('Usage: dario keys [list|create <name> [--seat=..] [--models=..] [--expires=..]|revoke <name>|rotate <name>|remove <name>] [--json] [--keys-path=<file>]');
|
|
929
|
+
console.error('Usage: dario keys [list|create <name> [--seat=..] [--models=..] [--expires=..] [--budget=..] [--budget-tokens=..]|budget <name> ..|revoke <name>|rotate <name>|remove <name>] [--json] [--keys-path=<file>]');
|
|
871
930
|
process.exit(1);
|
|
872
931
|
}
|
|
873
932
|
/**
|
package/dist/keys.d.ts
CHANGED
|
@@ -21,7 +21,109 @@ export interface KeyRecord {
|
|
|
21
21
|
seat?: string;
|
|
22
22
|
/** Model allowlist: exact ids, or `prefix*`. Empty / absent = any model. */
|
|
23
23
|
models?: string[];
|
|
24
|
+
/** Daily caps, UTC day, enforced from the ledger (dario#1318 follow-up). Absent = unlimited. */
|
|
25
|
+
budget?: KeyBudget;
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A key's daily budget. Both caps are per UTC day and both are read from the
|
|
29
|
+
* ledger's per-consumer rows at request time, so they survive a restart and
|
|
30
|
+
* every dollar can be traced to `dario usage --by-key`. `usdPerDay` is the
|
|
31
|
+
* API-equivalent price of the key's traffic (covered + metered); `tokensPerDay`
|
|
32
|
+
* counts every token the key sent or received, cache reads included.
|
|
33
|
+
*/
|
|
34
|
+
export interface KeyBudget {
|
|
35
|
+
usdPerDay?: number;
|
|
36
|
+
tokensPerDay?: number;
|
|
37
|
+
}
|
|
38
|
+
/** What the ledger says a key has used today; the budget is compared against this. */
|
|
39
|
+
export interface KeyBudgetUsage {
|
|
40
|
+
usd: number;
|
|
41
|
+
tokens: number;
|
|
42
|
+
requests: number;
|
|
43
|
+
}
|
|
44
|
+
export interface KeyBudgetVerdict {
|
|
45
|
+
over: boolean;
|
|
46
|
+
/** Which cap tripped first. */
|
|
47
|
+
reason: 'usd' | 'tokens' | null;
|
|
48
|
+
/** Completed rows only — what the ledger has. */
|
|
49
|
+
usage: KeyBudgetUsage;
|
|
50
|
+
/** Requests admitted and not yet completed when this verdict was made, and what was reserved for them. */
|
|
51
|
+
inflight: KeyBudgetReservation;
|
|
52
|
+
/** `usage` plus the in-flight reservations — what `over` was decided on. */
|
|
53
|
+
projected: KeyBudgetUsage;
|
|
54
|
+
budget: KeyBudget;
|
|
55
|
+
/** Epoch ms of the next UTC midnight — when the day's counters reset. */
|
|
56
|
+
resetAt: number;
|
|
57
|
+
retryAfterSec: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What a request is charged against the budget while it is in flight: an
|
|
61
|
+
* UPPER BOUND on what it can cost, so a burst of admitted requests can never
|
|
62
|
+
* complete for more than the cap plus one request. The ledger prices a
|
|
63
|
+
* request only once its response is in; until then what dario will SEND
|
|
64
|
+
* bounds both sides. Prompt: the client's body plus whatever the template
|
|
65
|
+
* adds (system prompt, tool definitions), at BUDGET_BYTES_PER_TOKEN bytes per
|
|
66
|
+
* token, priced as cache-create — the highest input-side rate, so any mix of
|
|
67
|
+
* input, cache-read and cache-create tokens (all of which are prompt tokens,
|
|
68
|
+
* and so all inside this byte count) costs no more. Output: the max_tokens
|
|
69
|
+
* dario will put on the wire (the template's default when it pins one, the
|
|
70
|
+
* client's when it does not; BUDGET_DEFAULT_MAX_TOKENS when nothing is set),
|
|
71
|
+
* at the output rate — thinking is billed as output and lives under the same
|
|
72
|
+
* cap. Tokens reserve the same two counts.
|
|
73
|
+
*/
|
|
74
|
+
export interface KeyBudgetReservation {
|
|
75
|
+
count: number;
|
|
76
|
+
usd: number;
|
|
77
|
+
tokens: number;
|
|
78
|
+
}
|
|
79
|
+
/** A conservative bytes-per-token for the reservation: prose is ~4, code and CJK are lower. */
|
|
80
|
+
export declare const BUDGET_BYTES_PER_TOKEN = 3;
|
|
81
|
+
/** Reserved output when the client sends no max_tokens / max_completion_tokens / max_output_tokens. */
|
|
82
|
+
export declare const BUDGET_DEFAULT_MAX_TOKENS = 8192;
|
|
83
|
+
export declare const EMPTY_RESERVATION: KeyBudgetReservation;
|
|
84
|
+
/**
|
|
85
|
+
* The reservation for one request, from what is known before it is sent.
|
|
86
|
+
* `priceOf` is analytics' costOfTokens, injected so this module stays free of
|
|
87
|
+
* the pricing table (the ledger injects the same way).
|
|
88
|
+
*/
|
|
89
|
+
export declare function requestBudgetReservation(model: string, bodyBytes: number, maxTokens: number | null | undefined, priceOf: (model: string, atMs: number, cell: {
|
|
90
|
+
requests: number;
|
|
91
|
+
inputTokens: number;
|
|
92
|
+
outputTokens: number;
|
|
93
|
+
cacheReadTokens: number;
|
|
94
|
+
cacheCreateTokens: number;
|
|
95
|
+
}) => number, now?: number,
|
|
96
|
+
/** Bytes dario adds to the prompt beyond the client's body (the template's system prompt and tools). */
|
|
97
|
+
extraPromptBytes?: number): KeyBudgetReservation;
|
|
98
|
+
export declare function addReservation(a: KeyBudgetReservation, b: KeyBudgetReservation): KeyBudgetReservation;
|
|
99
|
+
export declare function subtractReservation(a: KeyBudgetReservation, b: KeyBudgetReservation): KeyBudgetReservation;
|
|
100
|
+
/** Throws on a cap that is not a positive finite number; returns undefined when neither cap is set. */
|
|
101
|
+
export declare function normalizeBudget(b: KeyBudget | null | undefined): KeyBudget | undefined;
|
|
102
|
+
/** `$5`, `5`, `5.00`, `$5/day`, `5/d` → dollars per day; null when unparseable. */
|
|
103
|
+
export declare function parseUsdBudget(value: string): number | null;
|
|
104
|
+
/** `250000`, `250k`, `2M`, `1.5m/day` → tokens per day (integer); null when unparseable. */
|
|
105
|
+
export declare function parseTokenBudget(value: string): number | null;
|
|
106
|
+
/** `{ usdPerDay: 5, tokensPerDay: 2_000_000 }` → `$5/day · 2.0M tok/day`; `-` for none. */
|
|
107
|
+
export declare function formatBudget(b: KeyBudget | null | undefined): string;
|
|
108
|
+
/** Next UTC midnight after `now`, epoch ms. */
|
|
109
|
+
export declare function nextUtcMidnight(now: number): number;
|
|
110
|
+
/**
|
|
111
|
+
* Over or under, given what the ledger has counted for the key today AND what
|
|
112
|
+
* is reserved for the requests already admitted but not yet completed. The
|
|
113
|
+
* ledger only knows a request once its response is in, so a burst of N
|
|
114
|
+
* simultaneous requests would all read the same completed total and all
|
|
115
|
+
* pass; every in-flight request is therefore held at its reservation — an
|
|
116
|
+
* upper bound on its cost (requestBudgetReservation) — until it completes.
|
|
117
|
+
* A request is admitted while completed + reserved is under the cap, so the
|
|
118
|
+
* most a key can complete in a day is the cap plus ONE request, whatever the
|
|
119
|
+
* burst size or the size of the requests in it. `retryAfterSec` is the time
|
|
120
|
+
* to the UTC day boundary, when the counters reset.
|
|
121
|
+
*/
|
|
122
|
+
export declare function budgetVerdict(budget: KeyBudget, usage: KeyBudgetUsage, now?: number, inflight?: KeyBudgetReservation): KeyBudgetVerdict;
|
|
123
|
+
/** The response headers a budgeted key's request carries, served or refused. */
|
|
124
|
+
export declare function budgetHeaders(v: KeyBudgetVerdict, keyName: string): Record<string, string>;
|
|
125
|
+
/** Replace (or clear, with null) a key's budget. Null result: no such key. */
|
|
126
|
+
export declare function setKeyBudget(file: KeysFile, name: string, budget: KeyBudget | null): KeyRecord | null;
|
|
25
127
|
export interface KeysFile {
|
|
26
128
|
version: number;
|
|
27
129
|
keys: KeyRecord[];
|
|
@@ -53,6 +155,8 @@ export declare function writeKeysFile(path: string, file: KeysFile): void;
|
|
|
53
155
|
export interface CreateKeyOptions {
|
|
54
156
|
seat?: string;
|
|
55
157
|
models?: string[];
|
|
158
|
+
/** Daily caps; see KeyBudget. */
|
|
159
|
+
budget?: KeyBudget;
|
|
56
160
|
/** Absolute expiry, epoch ms. */
|
|
57
161
|
expiresAt?: number;
|
|
58
162
|
now?: number;
|
|
@@ -92,6 +196,11 @@ export interface KeyPublic {
|
|
|
92
196
|
expires: string | null;
|
|
93
197
|
seat: string | null;
|
|
94
198
|
models: string[];
|
|
199
|
+
/** Daily caps, or null when the key has none. */
|
|
200
|
+
budget: {
|
|
201
|
+
usd_per_day: number | null;
|
|
202
|
+
tokens_per_day: number | null;
|
|
203
|
+
} | null;
|
|
95
204
|
}
|
|
96
205
|
export declare function publicKey(k: KeyRecord, now?: number): KeyPublic;
|
|
97
206
|
/** `--expires=30d` / `12h` / `2026-12-31` → epoch ms, or null when unparseable. */
|
package/dist/keys.js
CHANGED
|
@@ -37,6 +37,131 @@ export const KEY_PREFIX = 'dk_';
|
|
|
37
37
|
export const KEY_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_\-.]{0,63}$/;
|
|
38
38
|
export const KEYS_FLUSH_DELAY_MS = 3_000;
|
|
39
39
|
const SECRET_BYTES = 24;
|
|
40
|
+
/** A conservative bytes-per-token for the reservation: prose is ~4, code and CJK are lower. */
|
|
41
|
+
export const BUDGET_BYTES_PER_TOKEN = 3;
|
|
42
|
+
/** Reserved output when the client sends no max_tokens / max_completion_tokens / max_output_tokens. */
|
|
43
|
+
export const BUDGET_DEFAULT_MAX_TOKENS = 8_192;
|
|
44
|
+
export const EMPTY_RESERVATION = { count: 0, usd: 0, tokens: 0 };
|
|
45
|
+
/**
|
|
46
|
+
* The reservation for one request, from what is known before it is sent.
|
|
47
|
+
* `priceOf` is analytics' costOfTokens, injected so this module stays free of
|
|
48
|
+
* the pricing table (the ledger injects the same way).
|
|
49
|
+
*/
|
|
50
|
+
export function requestBudgetReservation(model, bodyBytes, maxTokens, priceOf, now = Date.now(),
|
|
51
|
+
/** Bytes dario adds to the prompt beyond the client's body (the template's system prompt and tools). */
|
|
52
|
+
extraPromptBytes = 0) {
|
|
53
|
+
const inputTokens = Math.ceil((Math.max(0, bodyBytes) + Math.max(0, extraPromptBytes)) / BUDGET_BYTES_PER_TOKEN);
|
|
54
|
+
const outputTokens = Number.isFinite(maxTokens) && maxTokens > 0 ? Math.ceil(maxTokens) : BUDGET_DEFAULT_MAX_TOKENS;
|
|
55
|
+
const usd = priceOf(model, now, { requests: 1, inputTokens: 0, outputTokens, cacheReadTokens: 0, cacheCreateTokens: inputTokens });
|
|
56
|
+
return { count: 1, usd: Number.isFinite(usd) ? usd : 0, tokens: inputTokens + outputTokens };
|
|
57
|
+
}
|
|
58
|
+
export function addReservation(a, b) {
|
|
59
|
+
return { count: a.count + b.count, usd: a.usd + b.usd, tokens: a.tokens + b.tokens };
|
|
60
|
+
}
|
|
61
|
+
export function subtractReservation(a, b) {
|
|
62
|
+
const count = Math.max(0, a.count - b.count);
|
|
63
|
+
return count === 0 ? { ...EMPTY_RESERVATION } : { count, usd: Math.max(0, a.usd - b.usd), tokens: Math.max(0, a.tokens - b.tokens) };
|
|
64
|
+
}
|
|
65
|
+
/** Throws on a cap that is not a positive finite number; returns undefined when neither cap is set. */
|
|
66
|
+
export function normalizeBudget(b) {
|
|
67
|
+
if (!b)
|
|
68
|
+
return undefined;
|
|
69
|
+
const out = {};
|
|
70
|
+
if (b.usdPerDay !== undefined && b.usdPerDay !== null) {
|
|
71
|
+
if (!Number.isFinite(b.usdPerDay) || b.usdPerDay <= 0)
|
|
72
|
+
throw new Error('budget: usdPerDay must be a positive number');
|
|
73
|
+
out.usdPerDay = Math.round(b.usdPerDay * 100) / 100;
|
|
74
|
+
}
|
|
75
|
+
if (b.tokensPerDay !== undefined && b.tokensPerDay !== null) {
|
|
76
|
+
if (!Number.isFinite(b.tokensPerDay) || b.tokensPerDay <= 0)
|
|
77
|
+
throw new Error('budget: tokensPerDay must be a positive number');
|
|
78
|
+
out.tokensPerDay = Math.round(b.tokensPerDay);
|
|
79
|
+
}
|
|
80
|
+
return out.usdPerDay === undefined && out.tokensPerDay === undefined ? undefined : out;
|
|
81
|
+
}
|
|
82
|
+
/** `$5`, `5`, `5.00`, `$5/day`, `5/d` → dollars per day; null when unparseable. */
|
|
83
|
+
export function parseUsdBudget(value) {
|
|
84
|
+
const m = /^\$?\s*(\d+(?:\.\d{1,2})?)\s*(?:\/\s*(?:day|d))?$/i.exec(value.trim());
|
|
85
|
+
if (!m)
|
|
86
|
+
return null;
|
|
87
|
+
const n = Number(m[1]);
|
|
88
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
89
|
+
}
|
|
90
|
+
/** `250000`, `250k`, `2M`, `1.5m/day` → tokens per day (integer); null when unparseable. */
|
|
91
|
+
export function parseTokenBudget(value) {
|
|
92
|
+
const m = /^(\d+(?:\.\d+)?)\s*([kKmM]?)\s*(?:tok(?:ens)?)?\s*(?:\/\s*(?:day|d))?$/.exec(value.trim());
|
|
93
|
+
if (!m)
|
|
94
|
+
return null;
|
|
95
|
+
const mult = m[2]?.toLowerCase() === 'k' ? 1_000 : m[2]?.toLowerCase() === 'm' ? 1_000_000 : 1;
|
|
96
|
+
const n = Math.round(Number(m[1]) * mult);
|
|
97
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
98
|
+
}
|
|
99
|
+
/** `{ usdPerDay: 5, tokensPerDay: 2_000_000 }` → `$5/day · 2.0M tok/day`; `-` for none. */
|
|
100
|
+
export function formatBudget(b) {
|
|
101
|
+
if (!b || (b.usdPerDay === undefined && b.tokensPerDay === undefined))
|
|
102
|
+
return '-';
|
|
103
|
+
const parts = [];
|
|
104
|
+
if (b.usdPerDay !== undefined)
|
|
105
|
+
parts.push(`$${b.usdPerDay % 1 === 0 ? b.usdPerDay : b.usdPerDay.toFixed(2)}/day`);
|
|
106
|
+
if (b.tokensPerDay !== undefined) {
|
|
107
|
+
const t = b.tokensPerDay;
|
|
108
|
+
const s = t >= 1_000_000 ? `${(t / 1_000_000).toFixed(t % 1_000_000 === 0 ? 0 : 1)}M` : t >= 1_000 ? `${(t / 1_000).toFixed(t % 1_000 === 0 ? 0 : 1)}k` : String(t);
|
|
109
|
+
parts.push(`${s} tok/day`);
|
|
110
|
+
}
|
|
111
|
+
return parts.join(' · ');
|
|
112
|
+
}
|
|
113
|
+
/** Next UTC midnight after `now`, epoch ms. */
|
|
114
|
+
export function nextUtcMidnight(now) {
|
|
115
|
+
const d = new Date(now);
|
|
116
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Over or under, given what the ledger has counted for the key today AND what
|
|
120
|
+
* is reserved for the requests already admitted but not yet completed. The
|
|
121
|
+
* ledger only knows a request once its response is in, so a burst of N
|
|
122
|
+
* simultaneous requests would all read the same completed total and all
|
|
123
|
+
* pass; every in-flight request is therefore held at its reservation — an
|
|
124
|
+
* upper bound on its cost (requestBudgetReservation) — until it completes.
|
|
125
|
+
* A request is admitted while completed + reserved is under the cap, so the
|
|
126
|
+
* most a key can complete in a day is the cap plus ONE request, whatever the
|
|
127
|
+
* burst size or the size of the requests in it. `retryAfterSec` is the time
|
|
128
|
+
* to the UTC day boundary, when the counters reset.
|
|
129
|
+
*/
|
|
130
|
+
export function budgetVerdict(budget, usage, now = Date.now(), inflight = EMPTY_RESERVATION) {
|
|
131
|
+
const resetAt = nextUtcMidnight(now);
|
|
132
|
+
const projected = { usd: usage.usd + inflight.usd, tokens: usage.tokens + inflight.tokens, requests: usage.requests + inflight.count };
|
|
133
|
+
let reason = null;
|
|
134
|
+
if (budget.usdPerDay !== undefined && projected.usd >= budget.usdPerDay)
|
|
135
|
+
reason = 'usd';
|
|
136
|
+
else if (budget.tokensPerDay !== undefined && projected.tokens >= budget.tokensPerDay)
|
|
137
|
+
reason = 'tokens';
|
|
138
|
+
return { over: reason !== null, reason, usage, inflight, projected, budget, resetAt, retryAfterSec: Math.max(1, Math.ceil((resetAt - now) / 1000)) };
|
|
139
|
+
}
|
|
140
|
+
/** The response headers a budgeted key's request carries, served or refused. */
|
|
141
|
+
export function budgetHeaders(v, keyName) {
|
|
142
|
+
const h = { 'x-dario-budget-key': keyName, 'x-dario-budget-resets-at': new Date(v.resetAt).toISOString(), 'x-dario-budget-inflight': String(v.inflight.count) };
|
|
143
|
+
if (v.budget.usdPerDay !== undefined) {
|
|
144
|
+
h['x-dario-budget-usd'] = String(v.budget.usdPerDay);
|
|
145
|
+
h['x-dario-budget-used-usd'] = v.usage.usd.toFixed(4);
|
|
146
|
+
}
|
|
147
|
+
if (v.budget.tokensPerDay !== undefined) {
|
|
148
|
+
h['x-dario-budget-tokens'] = String(v.budget.tokensPerDay);
|
|
149
|
+
h['x-dario-budget-used-tokens'] = String(v.usage.tokens);
|
|
150
|
+
}
|
|
151
|
+
return h;
|
|
152
|
+
}
|
|
153
|
+
/** Replace (or clear, with null) a key's budget. Null result: no such key. */
|
|
154
|
+
export function setKeyBudget(file, name, budget) {
|
|
155
|
+
const k = file.keys.find((x) => x.name === name);
|
|
156
|
+
if (!k)
|
|
157
|
+
return null;
|
|
158
|
+
const normalized = normalizeBudget(budget);
|
|
159
|
+
if (normalized)
|
|
160
|
+
k.budget = normalized;
|
|
161
|
+
else
|
|
162
|
+
delete k.budget;
|
|
163
|
+
return k;
|
|
164
|
+
}
|
|
40
165
|
export function keysPathFor(home = homedir()) {
|
|
41
166
|
return join(home, '.dario', 'keys.json');
|
|
42
167
|
}
|
|
@@ -100,6 +225,16 @@ export function parseKeysFile(text) {
|
|
|
100
225
|
if (models.length > 0)
|
|
101
226
|
rec.models = models;
|
|
102
227
|
}
|
|
228
|
+
// Daily caps (dario#1318 follow-up): kept only when they parse as positive numbers.
|
|
229
|
+
if (k.budget && typeof k.budget === 'object') {
|
|
230
|
+
const b = k.budget;
|
|
231
|
+
try {
|
|
232
|
+
const budget = normalizeBudget({ usdPerDay: typeof b.usdPerDay === 'number' ? b.usdPerDay : undefined, tokensPerDay: typeof b.tokensPerDay === 'number' ? b.tokensPerDay : undefined });
|
|
233
|
+
if (budget)
|
|
234
|
+
rec.budget = budget;
|
|
235
|
+
}
|
|
236
|
+
catch { /* a malformed cap is dropped, never a reason to refuse the whole file */ }
|
|
237
|
+
}
|
|
103
238
|
seen.add(rec.name);
|
|
104
239
|
keys.push(rec);
|
|
105
240
|
}
|
|
@@ -152,6 +287,9 @@ export function createKey(file, name, opts = {}) {
|
|
|
152
287
|
record.seat = opts.seat;
|
|
153
288
|
if (opts.models && opts.models.length > 0)
|
|
154
289
|
record.models = opts.models.map((m) => m.trim()).filter(Boolean);
|
|
290
|
+
const budget = normalizeBudget(opts.budget);
|
|
291
|
+
if (budget)
|
|
292
|
+
record.budget = budget;
|
|
155
293
|
if (opts.expiresAt !== undefined) {
|
|
156
294
|
if (!Number.isFinite(opts.expiresAt) || opts.expiresAt <= (opts.now ?? Date.now()))
|
|
157
295
|
throw new Error('expiry must be in the future');
|
|
@@ -228,7 +366,8 @@ export function keyAllowsModel(k, model) {
|
|
|
228
366
|
}
|
|
229
367
|
export function publicKey(k, now = Date.now()) {
|
|
230
368
|
const status = k.disabled ? 'revoked' : k.expires && Date.parse(k.expires) <= now ? 'expired' : 'active';
|
|
231
|
-
|
|
369
|
+
const budget = k.budget ? { usd_per_day: k.budget.usdPerDay ?? null, tokens_per_day: k.budget.tokensPerDay ?? null } : null;
|
|
370
|
+
return { id: k.id, name: k.name, created: k.created, last_used: k.lastUsed ?? null, status, expires: k.expires ?? null, seat: k.seat ?? null, budget, models: k.models ?? [] };
|
|
232
371
|
}
|
|
233
372
|
/** `--expires=30d` / `12h` / `2026-12-31` → epoch ms, or null when unparseable. */
|
|
234
373
|
export function parseExpiry(value, now = Date.now()) {
|
package/dist/ledger.d.ts
CHANGED
|
@@ -155,6 +155,17 @@ export declare function addToLedger(file: LedgerFile, record: RequestRecord): bo
|
|
|
155
155
|
export declare function pruneLedger(file: LedgerFile, maxDays?: number): void;
|
|
156
156
|
/** The per-consumer split of a file, priced the same way as the headline. */
|
|
157
157
|
export declare function summarizeLedgerConsumers(file: LedgerFile, now?: number): Record<string, LedgerConsumerSummary>;
|
|
158
|
+
/**
|
|
159
|
+
* What one consumer has used so far TODAY (UTC), priced the same way as the
|
|
160
|
+
* headline: covered and metered rows both count, because a budget is about the
|
|
161
|
+
* traffic a key caused, not about who paid for it. Tokens are all four buckets.
|
|
162
|
+
* The key-budget check (src/keys.ts budgetVerdict) reads this per request.
|
|
163
|
+
*/
|
|
164
|
+
export declare function consumerDayUsage(file: LedgerFile, consumer: string, now?: number): {
|
|
165
|
+
usd: number;
|
|
166
|
+
tokens: number;
|
|
167
|
+
requests: number;
|
|
168
|
+
};
|
|
158
169
|
/** `1234` → `1.2k`, `1234567` → `1.2M`; below a thousand, the number itself. */
|
|
159
170
|
export declare function formatTokenCount(n: number): string;
|
|
160
171
|
export declare function summarizeLedger(file: LedgerFile, path: string, now?: number): LedgerSummary;
|
|
@@ -185,6 +196,12 @@ export declare class Ledger {
|
|
|
185
196
|
/** Count a request. Returns false when it was not ledger material. */
|
|
186
197
|
add(record: RequestRecord): boolean;
|
|
187
198
|
summary(now?: number): LedgerSummary;
|
|
199
|
+
/** Today's usage for one consumer — the key-budget check's input. */
|
|
200
|
+
consumerToday(consumer: string, now?: number): {
|
|
201
|
+
usd: number;
|
|
202
|
+
tokens: number;
|
|
203
|
+
requests: number;
|
|
204
|
+
};
|
|
188
205
|
/** The raw per-day table, for /analytics/ledger. */
|
|
189
206
|
snapshot(): LedgerFile;
|
|
190
207
|
private scheduleFlush;
|