@askalf/dario 6.19.1 → 6.20.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/README.md +2 -1
- package/dist/admin-api.d.ts +1 -1
- package/dist/admin-api.js +40 -5
- package/dist/analytics.d.ts +16 -1
- package/dist/analytics.js +25 -2
- package/dist/anthropic-responses-translate.d.ts +2 -0
- package/dist/anthropic-responses-translate.js +1 -1
- package/dist/cc-template-data.json +6 -4
- package/dist/cc-template.d.ts +9 -8
- package/dist/cc-template.js +39 -25
- package/dist/cli.js +44 -10
- package/dist/doctor-core.d.ts +1 -1
- package/dist/doctor-core.js +2 -2
- package/dist/effort.d.ts +18 -0
- package/dist/effort.js +38 -0
- package/dist/keys.d.ts +20 -0
- package/dist/keys.js +27 -1
- package/dist/ledger.d.ts +17 -1
- package/dist/ledger.js +43 -9
- package/dist/live-fingerprint.js +2 -2
- package/dist/proxy.js +42 -11
- package/docs/admin-api.md +2 -1
- package/docs/configuration.md +10 -3
- package/docs/drift-monitor.md +4 -4
- package/docs/keys.md +36 -1
- package/docs/pool-keys-and-analytics.md +3 -0
- package/docs/system-prompt.md +2 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
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, parseKeySocketSpec, setKeyBudget, parseUsdBudget, parseTokenBudget, formatBudget } from './keys.js';
|
|
22
|
+
import { KeyStore, createKey, revokeKey, rotateKey, deleteKey, parseExpiry, publicKey, resolveKeysPath, KEY_NAME_RE, parseKeySocketSpec, setKeyBudget, setKeyEffort, isKeyEffort, KEY_EFFORT_VALUES, parseUsdBudget, parseTokenBudget, formatBudget } from './keys.js';
|
|
23
23
|
/**
|
|
24
24
|
* `--budget=$5/day` / `--budget-tokens=2M/day` → a KeyBudget, or undefined when
|
|
25
25
|
* neither flag is present. A flag that does not parse exits 1 with the accepted
|
|
@@ -875,9 +875,9 @@ async function keys() {
|
|
|
875
875
|
return;
|
|
876
876
|
}
|
|
877
877
|
const w = Math.max(4, ...list.map((k) => k.name.length));
|
|
878
|
-
console.log(` ${'NAME'.padEnd(w)} ${'STATUS'.padEnd(7)} ${'SEAT'.padEnd(12)} ${'LAST USED'.padEnd(10)} ${'EXPIRES'.padEnd(10)} ${'BUDGET'.padEnd(20)} MODELS`);
|
|
878
|
+
console.log(` ${'NAME'.padEnd(w)} ${'STATUS'.padEnd(7)} ${'SEAT'.padEnd(12)} ${'EFFORT'.padEnd(6)} ${'LAST USED'.padEnd(10)} ${'EXPIRES'.padEnd(10)} ${'BUDGET'.padEnd(20)} MODELS`);
|
|
879
879
|
for (const k of list) {
|
|
880
|
-
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'}`);
|
|
880
|
+
console.log(` ${k.name.padEnd(w)} ${k.status.padEnd(7)} ${(k.seat ?? '-').padEnd(12)} ${(k.effort ?? '-').padEnd(6)} ${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'}`);
|
|
881
881
|
}
|
|
882
882
|
console.log('');
|
|
883
883
|
console.log(` ${list.length} key${list.length === 1 ? '' : 's'} in ${path}. Spend per key: dario usage --by-key`);
|
|
@@ -888,7 +888,7 @@ async function keys() {
|
|
|
888
888
|
const name = args[2];
|
|
889
889
|
if (!name || name.startsWith('--')) {
|
|
890
890
|
console.error('');
|
|
891
|
-
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]');
|
|
891
|
+
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] [--effort=low|medium|high|xhigh|max]');
|
|
892
892
|
console.error('');
|
|
893
893
|
process.exit(1);
|
|
894
894
|
}
|
|
@@ -907,8 +907,14 @@ async function keys() {
|
|
|
907
907
|
expiresAt = parsed;
|
|
908
908
|
}
|
|
909
909
|
const budget = readBudgetFlags(args);
|
|
910
|
+
const effortArg = args.find((a) => a.startsWith('--effort='));
|
|
911
|
+
const effort = effortArg ? effortArg.slice('--effort='.length).trim().toLowerCase() : undefined;
|
|
912
|
+
if (effort !== undefined && !isKeyEffort(effort)) {
|
|
913
|
+
console.error(`[dario] --effort: "${effort}" is not one of ${KEY_EFFORT_VALUES.join(', ')}.`);
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
910
916
|
try {
|
|
911
|
-
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, now }));
|
|
917
|
+
const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, effort, now }));
|
|
912
918
|
printSecret('created', publicKey(made.record, now), made.secret);
|
|
913
919
|
}
|
|
914
920
|
catch (err) {
|
|
@@ -917,6 +923,34 @@ async function keys() {
|
|
|
917
923
|
}
|
|
918
924
|
return;
|
|
919
925
|
}
|
|
926
|
+
// dario keys effort <name> low|medium|high|xhigh|max | --clear
|
|
927
|
+
if (sub === 'effort') {
|
|
928
|
+
const name = args[2];
|
|
929
|
+
const raw = args.includes('--clear') ? null : (args[3] ?? '').trim().toLowerCase();
|
|
930
|
+
if (!name || !KEY_NAME_RE.test(name) || (raw !== null && !isKeyEffort(raw))) {
|
|
931
|
+
console.error('');
|
|
932
|
+
console.error(` Usage: dario keys effort <name> ${KEY_EFFORT_VALUES.join('|')} | --clear`);
|
|
933
|
+
console.error('');
|
|
934
|
+
console.error(' The effort this key\'s requests run at when they name none themselves (no model-name');
|
|
935
|
+
console.error(' suffix, no effort field in the body). --effort / DARIO_EFFORT still wins over it.');
|
|
936
|
+
console.error('');
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
const level = raw !== null && isKeyEffort(raw) ? raw : null;
|
|
940
|
+
try {
|
|
941
|
+
const updated = store.mutate((file) => setKeyEffort(file, name, level));
|
|
942
|
+
if (!updated) {
|
|
943
|
+
console.error(`[dario] No key named "${name}".`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
console.log(level ? `[dario] Key "${name}" default effort: ${level}` : `[dario] Key "${name}" default effort cleared.`);
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
console.error(`[dario] ${err instanceof Error ? err.message : String(err)}`);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
920
954
|
// dario keys budget <name> --budget=$5/day --budget-tokens=2M/day | --clear
|
|
921
955
|
if (sub === 'budget') {
|
|
922
956
|
const name = args[2];
|
|
@@ -985,7 +1019,7 @@ async function keys() {
|
|
|
985
1019
|
return;
|
|
986
1020
|
}
|
|
987
1021
|
console.error(`[dario] Unknown keys subcommand: ${sub}`);
|
|
988
|
-
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>]');
|
|
1022
|
+
console.error('Usage: dario keys [list|create <name> [--seat=..] [--models=..] [--expires=..] [--budget=..] [--budget-tokens=..] [--effort=..]|budget <name> ..|effort <name> <level>|--clear|revoke <name>|rotate <name>|remove <name>] [--json] [--keys-path=<file>]');
|
|
989
1023
|
process.exit(1);
|
|
990
1024
|
}
|
|
991
1025
|
/**
|
|
@@ -2025,8 +2059,8 @@ async function help() {
|
|
|
2025
2059
|
miss. Needs \`dario proxy\` running; costs
|
|
2026
2060
|
at most a few tiny subscription requests.
|
|
2027
2061
|
dario doctor --json Emit the check report as structured JSON
|
|
2028
|
-
for machine consumption (
|
|
2029
|
-
|
|
2062
|
+
for machine consumption (status bots,
|
|
2063
|
+
CI scripts, etc.) instead of the
|
|
2030
2064
|
human-readable table.
|
|
2031
2065
|
dario doctor --auth-check
|
|
2032
2066
|
One-shot listener on an ephemeral loopback
|
|
@@ -2759,8 +2793,8 @@ async function doctor() {
|
|
|
2759
2793
|
}
|
|
2760
2794
|
const checks = await runChecks({ probe, usage, obedience });
|
|
2761
2795
|
if (asJson) {
|
|
2762
|
-
// JSON mode is meant for machine consumption (
|
|
2763
|
-
//
|
|
2796
|
+
// JSON mode is meant for machine consumption (status bots, health
|
|
2797
|
+
// checks, CI scripts): no decorative header, no
|
|
2764
2798
|
// trailing prose, stable shape. Exit code is also surfaced in the
|
|
2765
2799
|
// JSON envelope for callers that can't read process exit codes.
|
|
2766
2800
|
process.stdout.write(formatChecksJson(checks) + '\n');
|
package/dist/doctor-core.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export declare function formatChecks(checks: Check[]): string;
|
|
|
108
108
|
export declare function exitCodeFor(checks: Check[]): number;
|
|
109
109
|
/**
|
|
110
110
|
* Serialize a check report as structured JSON. Lets other tools
|
|
111
|
-
* (
|
|
111
|
+
* (chat-bot /status commands, health probes, CI scripts) consume
|
|
112
112
|
* dario's health programmatically instead of scraping the formatted
|
|
113
113
|
* text. Emitted by `dario doctor --json`.
|
|
114
114
|
*/
|
package/dist/doctor-core.js
CHANGED
|
@@ -227,7 +227,7 @@ export function exitCodeFor(checks) {
|
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
229
229
|
* Serialize a check report as structured JSON. Lets other tools
|
|
230
|
-
* (
|
|
230
|
+
* (chat-bot /status commands, health probes, CI scripts) consume
|
|
231
231
|
* dario's health programmatically instead of scraping the formatted
|
|
232
232
|
* text. Emitted by `dario doctor --json`.
|
|
233
233
|
*/
|
|
@@ -1019,7 +1019,7 @@ export async function runChecks(opts = {}) {
|
|
|
1019
1019
|
}
|
|
1020
1020
|
}
|
|
1021
1021
|
// ---- Client-system obedience probe (opt-in, --obedience).
|
|
1022
|
-
// dario#509 / the 2026-06-12
|
|
1022
|
+
// dario#509 / the 2026-06-12 downstream planner outage: Anthropic's serving
|
|
1023
1023
|
// side changed how claude-sonnet-4-6 weighed client system text merged
|
|
1024
1024
|
// after the CC persona in block 3 — wire bytes identical, every existing
|
|
1025
1025
|
// check green — and the only symptom was a downstream consumer failing to
|
package/dist/effort.d.ts
CHANGED
|
@@ -32,6 +32,24 @@ export declare const VALID_EFFORT_VALUES: ReadonlyArray<EffortValue>;
|
|
|
32
32
|
* and the request's own thinking budget decides, as before.
|
|
33
33
|
*/
|
|
34
34
|
export declare function effortForCodex(effort: EffortValue | undefined): 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Did the request name an effort of its own? Three places a client can say it:
|
|
37
|
+
* `output_config.effort` (Messages shape), `reasoning_effort` (chat completions),
|
|
38
|
+
* `reasoning.effort` (Responses). A named key's default effort fills in only when
|
|
39
|
+
* this is false, so a client that chose is never overruled by a default. The
|
|
40
|
+
* model-name suffix is checked separately by the caller (it is parsed off the
|
|
41
|
+
* model before this runs).
|
|
42
|
+
*/
|
|
43
|
+
export declare function requestNamesEffort(body: Record<string, unknown> | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A Responses body for the codex passthrough with `reasoning.effort` filled in.
|
|
46
|
+
* Returned as is when there is no effort to add or when the client already
|
|
47
|
+
* named one (a non-empty `reasoning.effort`); otherwise a copy carries the
|
|
48
|
+
* level and keeps every other `reasoning` field, since Codex CLI sends
|
|
49
|
+
* `summary` there. The passthrough forwards the client's body as written, so
|
|
50
|
+
* this is the only place a suffix or a key default can reach that route.
|
|
51
|
+
*/
|
|
52
|
+
export declare function withResponsesEffort(body: Record<string, unknown>, effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined): Record<string, unknown>;
|
|
35
53
|
export declare function parseEffortSuffix(model: string): {
|
|
36
54
|
model: string;
|
|
37
55
|
effort?: EffortValue;
|
package/dist/effort.js
CHANGED
|
@@ -52,6 +52,44 @@ export function effortForCodex(effort) {
|
|
|
52
52
|
default: return undefined;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Did the request name an effort of its own? Three places a client can say it:
|
|
57
|
+
* `output_config.effort` (Messages shape), `reasoning_effort` (chat completions),
|
|
58
|
+
* `reasoning.effort` (Responses). A named key's default effort fills in only when
|
|
59
|
+
* this is false, so a client that chose is never overruled by a default. The
|
|
60
|
+
* model-name suffix is checked separately by the caller (it is parsed off the
|
|
61
|
+
* model before this runs).
|
|
62
|
+
*/
|
|
63
|
+
export function requestNamesEffort(body) {
|
|
64
|
+
if (!body || typeof body !== 'object')
|
|
65
|
+
return false;
|
|
66
|
+
const oc = body.output_config;
|
|
67
|
+
if (oc && typeof oc === 'object' && typeof oc.effort === 'string')
|
|
68
|
+
return true;
|
|
69
|
+
if (typeof body.reasoning_effort === 'string' && body.reasoning_effort.length > 0)
|
|
70
|
+
return true;
|
|
71
|
+
const reasoning = body.reasoning;
|
|
72
|
+
if (reasoning && typeof reasoning === 'object' && typeof reasoning.effort === 'string')
|
|
73
|
+
return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A Responses body for the codex passthrough with `reasoning.effort` filled in.
|
|
78
|
+
* Returned as is when there is no effort to add or when the client already
|
|
79
|
+
* named one (a non-empty `reasoning.effort`); otherwise a copy carries the
|
|
80
|
+
* level and keeps every other `reasoning` field, since Codex CLI sends
|
|
81
|
+
* `summary` there. The passthrough forwards the client's body as written, so
|
|
82
|
+
* this is the only place a suffix or a key default can reach that route.
|
|
83
|
+
*/
|
|
84
|
+
export function withResponsesEffort(body, effort) {
|
|
85
|
+
if (!effort)
|
|
86
|
+
return body;
|
|
87
|
+
const r = body.reasoning;
|
|
88
|
+
const existing = r !== null && typeof r === 'object' && !Array.isArray(r) ? r : {};
|
|
89
|
+
if (typeof existing.effort === 'string' && existing.effort.length > 0)
|
|
90
|
+
return body;
|
|
91
|
+
return { ...body, reasoning: { ...existing, effort } };
|
|
92
|
+
}
|
|
55
93
|
export function parseEffortSuffix(model) {
|
|
56
94
|
for (const e of SUFFIX_EFFORTS) {
|
|
57
95
|
for (const sep of [':', '-']) {
|
package/dist/keys.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ export declare const KEY_PREFIX = "dk_";
|
|
|
3
3
|
/** Same charset as a pool alias — a key name is printed next to one. */
|
|
4
4
|
export declare const KEY_NAME_RE: RegExp;
|
|
5
5
|
export declare const KEYS_FLUSH_DELAY_MS = 3000;
|
|
6
|
+
/**
|
|
7
|
+
* The effort levels a named key may carry as its default: the wire-valid set,
|
|
8
|
+
* and nothing that means "ask the client" (`client`) or names a client mode
|
|
9
|
+
* (`ultracode`). Applied per request when the request named no effort itself.
|
|
10
|
+
*/
|
|
11
|
+
export type KeyEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
12
|
+
export declare const KEY_EFFORT_VALUES: ReadonlyArray<KeyEffort>;
|
|
13
|
+
export declare function isKeyEffort(v: unknown): v is KeyEffort;
|
|
6
14
|
export interface KeyRecord {
|
|
7
15
|
/** Stable short id (8 hex), for logs and rotation; never the secret. */
|
|
8
16
|
id: string;
|
|
@@ -23,6 +31,12 @@ export interface KeyRecord {
|
|
|
23
31
|
models?: string[];
|
|
24
32
|
/** Daily caps, UTC day, enforced from the ledger (dario#1318 follow-up). Absent = unlimited. */
|
|
25
33
|
budget?: KeyBudget;
|
|
34
|
+
/**
|
|
35
|
+
* Default effort for this key's requests when the request names none itself
|
|
36
|
+
* (no model-name suffix, no effort field in the body). Absent = the proxy's
|
|
37
|
+
* usual resolution. The operator's --effort pin still wins over it.
|
|
38
|
+
*/
|
|
39
|
+
effort?: KeyEffort;
|
|
26
40
|
}
|
|
27
41
|
/**
|
|
28
42
|
* A key's daily budget. Both caps are per UTC day and both are read from the
|
|
@@ -126,6 +140,8 @@ export declare function budgetVerdict(budget: KeyBudget, usage: KeyBudgetUsage,
|
|
|
126
140
|
export declare function budgetHeaders(v: KeyBudgetVerdict, keyName: string): Record<string, string>;
|
|
127
141
|
/** Replace (or clear, with null) a key's budget. Null result: no such key. */
|
|
128
142
|
export declare function setKeyBudget(file: KeysFile, name: string, budget: KeyBudget | null): KeyRecord | null;
|
|
143
|
+
/** Set or clear a key's default effort. Returns null when there is no such key; throws on a level outside KEY_EFFORT_VALUES. */
|
|
144
|
+
export declare function setKeyEffort(file: KeysFile, name: string, effort: KeyEffort | string | null): KeyRecord | null;
|
|
129
145
|
export interface KeysFile {
|
|
130
146
|
version: number;
|
|
131
147
|
keys: KeyRecord[];
|
|
@@ -161,6 +177,8 @@ export interface CreateKeyOptions {
|
|
|
161
177
|
budget?: KeyBudget;
|
|
162
178
|
/** Absolute expiry, epoch ms. */
|
|
163
179
|
expiresAt?: number;
|
|
180
|
+
/** Default effort; one of KEY_EFFORT_VALUES, validated. */
|
|
181
|
+
effort?: string;
|
|
164
182
|
now?: number;
|
|
165
183
|
}
|
|
166
184
|
/** Mint a key. Returns the record (stored) and the secret (shown once). Mutates `file`. */
|
|
@@ -203,6 +221,8 @@ export interface KeyPublic {
|
|
|
203
221
|
usd_per_day: number | null;
|
|
204
222
|
tokens_per_day: number | null;
|
|
205
223
|
} | null;
|
|
224
|
+
/** Default effort, or null when the key has none. */
|
|
225
|
+
effort: KeyEffort | null;
|
|
206
226
|
}
|
|
207
227
|
export declare function publicKey(k: KeyRecord, now?: number): KeyPublic;
|
|
208
228
|
/** `--expires=30d` / `12h` / `2026-12-31` → epoch ms, or null when unparseable. */
|
package/dist/keys.js
CHANGED
|
@@ -37,6 +37,10 @@ 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
|
+
export const KEY_EFFORT_VALUES = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
41
|
+
export function isKeyEffort(v) {
|
|
42
|
+
return typeof v === 'string' && KEY_EFFORT_VALUES.includes(v);
|
|
43
|
+
}
|
|
40
44
|
/** A conservative bytes-per-token for the reservation: prose is ~4, code and CJK are lower. */
|
|
41
45
|
export const BUDGET_BYTES_PER_TOKEN = 3;
|
|
42
46
|
/** Reserved output when the client sends no max_tokens / max_completion_tokens / max_output_tokens. */
|
|
@@ -166,6 +170,20 @@ export function setKeyBudget(file, name, budget) {
|
|
|
166
170
|
delete k.budget;
|
|
167
171
|
return k;
|
|
168
172
|
}
|
|
173
|
+
/** Set or clear a key's default effort. Returns null when there is no such key; throws on a level outside KEY_EFFORT_VALUES. */
|
|
174
|
+
export function setKeyEffort(file, name, effort) {
|
|
175
|
+
const k = file.keys.find((x) => x.name === name);
|
|
176
|
+
if (!k)
|
|
177
|
+
return null;
|
|
178
|
+
if (effort === null || effort === undefined || effort === '') {
|
|
179
|
+
delete k.effort;
|
|
180
|
+
return k;
|
|
181
|
+
}
|
|
182
|
+
if (!isKeyEffort(effort))
|
|
183
|
+
throw new Error(`invalid effort "${effort}": one of ${KEY_EFFORT_VALUES.join(', ')}`);
|
|
184
|
+
k.effort = effort;
|
|
185
|
+
return k;
|
|
186
|
+
}
|
|
169
187
|
export function keysPathFor(home = homedir()) {
|
|
170
188
|
return join(home, '.dario', 'keys.json');
|
|
171
189
|
}
|
|
@@ -239,6 +257,9 @@ export function parseKeysFile(text) {
|
|
|
239
257
|
}
|
|
240
258
|
catch { /* a malformed cap is dropped, never a reason to refuse the whole file */ }
|
|
241
259
|
}
|
|
260
|
+
// A default effort outside the wire-valid set is dropped, like a malformed cap.
|
|
261
|
+
if (isKeyEffort(k.effort))
|
|
262
|
+
rec.effort = k.effort;
|
|
242
263
|
seen.add(rec.name);
|
|
243
264
|
keys.push(rec);
|
|
244
265
|
}
|
|
@@ -294,6 +315,11 @@ export function createKey(file, name, opts = {}) {
|
|
|
294
315
|
const budget = normalizeBudget(opts.budget);
|
|
295
316
|
if (budget)
|
|
296
317
|
record.budget = budget;
|
|
318
|
+
if (opts.effort !== undefined && opts.effort !== '') {
|
|
319
|
+
if (!isKeyEffort(opts.effort))
|
|
320
|
+
throw new Error(`invalid effort "${opts.effort}": one of ${KEY_EFFORT_VALUES.join(', ')}`);
|
|
321
|
+
record.effort = opts.effort;
|
|
322
|
+
}
|
|
297
323
|
if (opts.expiresAt !== undefined) {
|
|
298
324
|
if (!Number.isFinite(opts.expiresAt) || opts.expiresAt <= (opts.now ?? Date.now()))
|
|
299
325
|
throw new Error('expiry must be in the future');
|
|
@@ -371,7 +397,7 @@ export function keyAllowsModel(k, model) {
|
|
|
371
397
|
export function publicKey(k, now = Date.now()) {
|
|
372
398
|
const status = k.disabled ? 'revoked' : k.expires && Date.parse(k.expires) <= now ? 'expired' : 'active';
|
|
373
399
|
const budget = k.budget ? { usd_per_day: k.budget.usdPerDay ?? null, tokens_per_day: k.budget.tokensPerDay ?? null } : null;
|
|
374
|
-
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 ?? [] };
|
|
400
|
+
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, effort: k.effort ?? null, models: k.models ?? [] };
|
|
375
401
|
}
|
|
376
402
|
/** `--expires=30d` / `12h` / `2026-12-31` → epoch ms, or null when unparseable. */
|
|
377
403
|
export function parseExpiry(value, now = Date.now()) {
|
package/dist/ledger.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface LedgerCell {
|
|
|
38
38
|
outputTokens: number;
|
|
39
39
|
cacheReadTokens: number;
|
|
40
40
|
cacheCreateTokens: number;
|
|
41
|
+
/**
|
|
42
|
+
* Thinking tokens INSIDE outputTokens, a share and never an addition (the
|
|
43
|
+
* model's reasoning is billed as output). Counted from the usage block the
|
|
44
|
+
* backend returns; rows written before the field existed read as 0.
|
|
45
|
+
*/
|
|
46
|
+
thinkingTokens: number;
|
|
41
47
|
}
|
|
42
48
|
export type LedgerRow = Partial<Record<LedgerBucket, LedgerCell>>;
|
|
43
49
|
export interface LedgerFile {
|
|
@@ -67,6 +73,9 @@ export interface LedgerConsumerSummary {
|
|
|
67
73
|
outputTokens: number;
|
|
68
74
|
cacheReadTokens: number;
|
|
69
75
|
cacheCreateTokens: number;
|
|
76
|
+
/** Of outputTokens, how many were thinking; and what that thinking would have cost at the model's output rate (both buckets). */
|
|
77
|
+
thinkingTokens: number;
|
|
78
|
+
thinkingCost: number;
|
|
70
79
|
recent: {
|
|
71
80
|
today: number;
|
|
72
81
|
last7d: number;
|
|
@@ -84,6 +93,8 @@ export interface LedgerModelSummary {
|
|
|
84
93
|
outputTokens: number;
|
|
85
94
|
cacheReadTokens: number;
|
|
86
95
|
cacheCreateTokens: number;
|
|
96
|
+
/** Of the covered outputTokens, how many were thinking. */
|
|
97
|
+
thinkingTokens: number;
|
|
87
98
|
/** API-equivalent cost of this model's covered traffic, USD. */
|
|
88
99
|
apiEquivalentCost: number;
|
|
89
100
|
/** What this model's metered traffic cost at list price, USD. */
|
|
@@ -106,13 +117,16 @@ export interface LedgerSummary {
|
|
|
106
117
|
apiEquivalentCost: number;
|
|
107
118
|
/** What metered traffic (API key, paid overage) actually cost at list price, USD. */
|
|
108
119
|
meteredCost: number;
|
|
109
|
-
/** Covered token totals. */
|
|
120
|
+
/** Covered token totals. `thinking` is the share of `output` that was reasoning. */
|
|
110
121
|
tokens: {
|
|
111
122
|
input: number;
|
|
112
123
|
output: number;
|
|
113
124
|
cacheRead: number;
|
|
114
125
|
cacheCreate: number;
|
|
126
|
+
thinking: number;
|
|
115
127
|
};
|
|
128
|
+
/** The part of apiEquivalentCost that paid for thinking: covered thinking tokens at each model's output rate, USD. */
|
|
129
|
+
thinkingCost: number;
|
|
116
130
|
perProvider: Record<PricingProvider, {
|
|
117
131
|
requests: number;
|
|
118
132
|
apiEquivalentCost: number;
|
|
@@ -238,6 +252,8 @@ export declare function shortModelName(model: string): string;
|
|
|
238
252
|
* to match the rest of that command's output.
|
|
239
253
|
*/
|
|
240
254
|
export declare function formatLedgerSummary(s: LedgerSummary): string[];
|
|
255
|
+
/** Thinking as a whole percent of output; 0 when there was no output. */
|
|
256
|
+
export declare function thinkingPercent(thinking: number, output: number): number;
|
|
241
257
|
/**
|
|
242
258
|
* `dario usage --by-key`: the lifetime number per consumer, biggest first.
|
|
243
259
|
* A consumer is a named key's name, an `x-dario-consumer` header, or the
|
package/dist/ledger.js
CHANGED
|
@@ -52,7 +52,7 @@ export function resolveLedgerPath(port, env = process.env) {
|
|
|
52
52
|
export function ledgerDisabledByEnv(env = process.env) {
|
|
53
53
|
return ['0', 'false', 'no', 'off'].includes((env['DARIO_LEDGER'] ?? '').toLowerCase());
|
|
54
54
|
}
|
|
55
|
-
const emptyCell = () => ({ requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0 });
|
|
55
|
+
const emptyCell = () => ({ requests: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0 });
|
|
56
56
|
export function emptyLedger(now = Date.now()) {
|
|
57
57
|
const iso = new Date(now).toISOString();
|
|
58
58
|
return { version: LEDGER_VERSION, since: iso, updated: iso, days: {} };
|
|
@@ -90,6 +90,17 @@ function isCell(v) {
|
|
|
90
90
|
return ['requests', 'inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreateTokens']
|
|
91
91
|
.every((k) => typeof c[k] === 'number' && Number.isFinite(c[k]) && c[k] >= 0);
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A cell as read from disk, with the fields added after the file was written
|
|
95
|
+
* filled in: `thinkingTokens` is 0 on rows older than the field (and never
|
|
96
|
+
* above the output count it is a share of).
|
|
97
|
+
*/
|
|
98
|
+
function cleanCell(c) {
|
|
99
|
+
const raw = c.thinkingTokens;
|
|
100
|
+
const thinking = typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? Math.min(raw, c.outputTokens) : 0;
|
|
101
|
+
return { requests: c.requests, inputTokens: c.inputTokens, outputTokens: c.outputTokens,
|
|
102
|
+
cacheReadTokens: c.cacheReadTokens, cacheCreateTokens: c.cacheCreateTokens, thinkingTokens: thinking };
|
|
103
|
+
}
|
|
93
104
|
/**
|
|
94
105
|
* Parse a ledger file's text, keeping only well-formed rows. A file that is
|
|
95
106
|
* not a ledger at all throws; the caller moves it aside and starts fresh.
|
|
@@ -109,9 +120,9 @@ export function parseLedger(text) {
|
|
|
109
120
|
continue;
|
|
110
121
|
const r = {};
|
|
111
122
|
if (isCell(row.covered))
|
|
112
|
-
r.covered =
|
|
123
|
+
r.covered = cleanCell(row.covered);
|
|
113
124
|
if (isCell(row.metered))
|
|
114
|
-
r.metered =
|
|
125
|
+
r.metered = cleanCell(row.metered);
|
|
115
126
|
if (r.covered || r.metered)
|
|
116
127
|
clean[model] = r;
|
|
117
128
|
}
|
|
@@ -136,9 +147,9 @@ export function parseLedger(text) {
|
|
|
136
147
|
continue;
|
|
137
148
|
const r = {};
|
|
138
149
|
if (isCell(row.covered))
|
|
139
|
-
r.covered =
|
|
150
|
+
r.covered = cleanCell(row.covered);
|
|
140
151
|
if (isCell(row.metered))
|
|
141
|
-
r.metered =
|
|
152
|
+
r.metered = cleanCell(row.metered);
|
|
142
153
|
if (r.covered || r.metered)
|
|
143
154
|
clean[model] = r;
|
|
144
155
|
}
|
|
@@ -160,6 +171,8 @@ function addCell(row, bucket, record) {
|
|
|
160
171
|
cell.outputTokens += record.outputTokens;
|
|
161
172
|
cell.cacheReadTokens += record.cacheReadTokens;
|
|
162
173
|
cell.cacheCreateTokens += record.cacheCreateTokens;
|
|
174
|
+
const thinking = Number.isFinite(record.thinkingTokens) && record.thinkingTokens > 0 ? record.thinkingTokens : 0;
|
|
175
|
+
cell.thinkingTokens = (cell.thinkingTokens ?? 0) + Math.min(thinking, record.outputTokens);
|
|
163
176
|
}
|
|
164
177
|
/** Add one record's tokens to the file in place. Returns false when it was not counted. */
|
|
165
178
|
export function addToLedger(file, record) {
|
|
@@ -203,13 +216,14 @@ export function summarizeLedgerConsumers(file, now = Date.now()) {
|
|
|
203
216
|
for (const [day, byConsumer] of Object.entries(file.consumers ?? {})) {
|
|
204
217
|
const at = dayMs(day);
|
|
205
218
|
for (const [consumer, models] of Object.entries(byConsumer)) {
|
|
206
|
-
const c = (out[consumer] ??= { requests: 0, apiEquivalentCost: 0, meteredCost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, recent: { today: 0, last7d: 0, last30d: 0 }, lastDay: day, models: [], _models: {} });
|
|
219
|
+
const c = (out[consumer] ??= { requests: 0, apiEquivalentCost: 0, meteredCost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0, thinkingCost: 0, recent: { today: 0, last7d: 0, last30d: 0 }, lastDay: day, models: [], _models: {} });
|
|
207
220
|
if (day > c.lastDay)
|
|
208
221
|
c.lastDay = day;
|
|
209
222
|
for (const [model, row] of Object.entries(models)) {
|
|
210
223
|
if (row.covered) {
|
|
211
224
|
const cost = costOfTokens(model, at, row.covered);
|
|
212
225
|
c.apiEquivalentCost += cost;
|
|
226
|
+
c.thinkingCost += costOfTokens(model, at, thinkingOnly(row.covered));
|
|
213
227
|
c.requests += row.covered.requests;
|
|
214
228
|
addTokens(c, row.covered);
|
|
215
229
|
c._models[model] = (c._models[model] ?? 0) + row.covered.requests;
|
|
@@ -222,6 +236,7 @@ export function summarizeLedgerConsumers(file, now = Date.now()) {
|
|
|
222
236
|
}
|
|
223
237
|
if (row.metered) {
|
|
224
238
|
c.meteredCost += costOfTokens(model, at, row.metered);
|
|
239
|
+
c.thinkingCost += costOfTokens(model, at, thinkingOnly(row.metered));
|
|
225
240
|
c.requests += row.metered.requests;
|
|
226
241
|
addTokens(c, row.metered);
|
|
227
242
|
c._models[model] = (c._models[model] ?? 0) + row.metered.requests;
|
|
@@ -239,6 +254,8 @@ export function summarizeLedgerConsumers(file, now = Date.now()) {
|
|
|
239
254
|
outputTokens: c.outputTokens,
|
|
240
255
|
cacheReadTokens: c.cacheReadTokens,
|
|
241
256
|
cacheCreateTokens: c.cacheCreateTokens,
|
|
257
|
+
thinkingTokens: c.thinkingTokens,
|
|
258
|
+
thinkingCost: round(c.thinkingCost),
|
|
242
259
|
recent: { today: round(c.recent.today), last7d: round(c.recent.last7d), last30d: round(c.recent.last30d) },
|
|
243
260
|
lastDay: c.lastDay,
|
|
244
261
|
models: Object.entries(c._models).sort((a, b) => b[1] - a[1]).map(([m]) => m),
|
|
@@ -276,6 +293,11 @@ function addTokens(into, cell) {
|
|
|
276
293
|
into.outputTokens += cell.outputTokens;
|
|
277
294
|
into.cacheReadTokens += cell.cacheReadTokens;
|
|
278
295
|
into.cacheCreateTokens += cell.cacheCreateTokens;
|
|
296
|
+
into.thinkingTokens += cell.thinkingTokens ?? 0;
|
|
297
|
+
}
|
|
298
|
+
/** The cell's thinking alone, shaped for the price list: output tokens and nothing else. */
|
|
299
|
+
function thinkingOnly(cell) {
|
|
300
|
+
return { inputTokens: 0, outputTokens: cell.thinkingTokens ?? 0, cacheReadTokens: 0, cacheCreateTokens: 0 };
|
|
279
301
|
}
|
|
280
302
|
// Round BEFORE choosing the unit. Picking the unit on the raw value and
|
|
281
303
|
// rounding afterwards let 999_600 print as "1000k": once the rounded
|
|
@@ -305,32 +327,36 @@ export function summarizeLedger(file, path, now = Date.now()) {
|
|
|
305
327
|
anthropic: { requests: 0, apiEquivalentCost: 0 },
|
|
306
328
|
openai: { requests: 0, apiEquivalentCost: 0 },
|
|
307
329
|
};
|
|
308
|
-
const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
330
|
+
const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0, thinking: 0 };
|
|
309
331
|
let requests = 0;
|
|
310
332
|
let covered = 0;
|
|
311
333
|
let metered = 0;
|
|
334
|
+
let thinkingCost = 0;
|
|
312
335
|
const recent = { today: 0, last7d: 0, last30d: 0 };
|
|
313
336
|
for (const [day, models] of Object.entries(file.days)) {
|
|
314
337
|
const at = dayMs(day);
|
|
315
338
|
for (const [model, row] of Object.entries(models)) {
|
|
316
339
|
const m = (perModel[model] ??= {
|
|
317
340
|
provider: providerOfModel(model), requests: 0,
|
|
318
|
-
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0,
|
|
341
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0, thinkingTokens: 0,
|
|
319
342
|
apiEquivalentCost: 0, meteredCost: 0,
|
|
320
343
|
});
|
|
321
344
|
if (row.covered) {
|
|
322
345
|
const cost = costOfTokens(model, at, row.covered);
|
|
323
346
|
covered += cost;
|
|
347
|
+
thinkingCost += costOfTokens(model, at, thinkingOnly(row.covered));
|
|
324
348
|
m.apiEquivalentCost += cost;
|
|
325
349
|
m.requests += row.covered.requests;
|
|
326
350
|
m.inputTokens += row.covered.inputTokens;
|
|
327
351
|
m.outputTokens += row.covered.outputTokens;
|
|
328
352
|
m.cacheReadTokens += row.covered.cacheReadTokens;
|
|
329
353
|
m.cacheCreateTokens += row.covered.cacheCreateTokens;
|
|
354
|
+
m.thinkingTokens += row.covered.thinkingTokens ?? 0;
|
|
330
355
|
tokens.input += row.covered.inputTokens;
|
|
331
356
|
tokens.output += row.covered.outputTokens;
|
|
332
357
|
tokens.cacheRead += row.covered.cacheReadTokens;
|
|
333
358
|
tokens.cacheCreate += row.covered.cacheCreateTokens;
|
|
359
|
+
tokens.thinking += row.covered.thinkingTokens ?? 0;
|
|
334
360
|
perProvider[m.provider].requests += row.covered.requests;
|
|
335
361
|
perProvider[m.provider].apiEquivalentCost += cost;
|
|
336
362
|
requests += row.covered.requests;
|
|
@@ -364,6 +390,7 @@ export function summarizeLedger(file, path, now = Date.now()) {
|
|
|
364
390
|
apiEquivalentCost: round(covered),
|
|
365
391
|
meteredCost: round(metered),
|
|
366
392
|
tokens,
|
|
393
|
+
thinkingCost: round(thinkingCost),
|
|
367
394
|
perProvider,
|
|
368
395
|
perModel,
|
|
369
396
|
recent: { today: round(recent.today), last7d: round(recent.last7d), last30d: round(recent.last30d) },
|
|
@@ -559,8 +586,14 @@ export function formatLedgerSummary(s) {
|
|
|
559
586
|
lines.push(` Today ${formatUsd(s.recent.today)} · Last 7d ${formatUsd(s.recent.last7d)} · Last 30d ${formatUsd(s.recent.last30d)}`);
|
|
560
587
|
if (s.meteredCost > 0)
|
|
561
588
|
lines.push(` Paid per token on top (API key / extra usage): ${formatUsd(s.meteredCost)}`);
|
|
589
|
+
if (s.tokens.thinking > 0)
|
|
590
|
+
lines.push(` Thinking: ${formatTokenCount(s.tokens.thinking)} of ${formatTokenCount(s.tokens.output)} output tokens (${thinkingPercent(s.tokens.thinking, s.tokens.output)}%) · ${formatUsd(s.thinkingCost)} of the number above`);
|
|
562
591
|
return lines;
|
|
563
592
|
}
|
|
593
|
+
/** Thinking as a whole percent of output; 0 when there was no output. */
|
|
594
|
+
export function thinkingPercent(thinking, output) {
|
|
595
|
+
return output > 0 ? Math.round((100 * thinking) / output) : 0;
|
|
596
|
+
}
|
|
564
597
|
/**
|
|
565
598
|
* `dario usage --by-key`: the lifetime number per consumer, biggest first.
|
|
566
599
|
* A consumer is a named key's name, an `x-dario-consumer` header, or the
|
|
@@ -576,7 +609,8 @@ export function formatLedgerConsumers(s, limit = 20) {
|
|
|
576
609
|
for (const [consumer, c] of entries.slice(0, limit)) {
|
|
577
610
|
const models = c.models.slice(0, 2).map(shortModelName).join(', ');
|
|
578
611
|
lines.push(` ${consumer.slice(0, width).padEnd(width)} ${formatUsd(c.apiEquivalentCost).padStart(9)} · ${formatUsd(c.recent.today).padStart(8)} · ${formatUsd(c.recent.last7d).padStart(8)} · ${formatUsd(c.recent.last30d).padStart(8)} ${c.requests.toLocaleString('en-US')} req${c.requests === 1 ? '' : 's'}${models ? `, ${models}` : ''}${c.meteredCost > 0 ? `, ${formatUsd(c.meteredCost)} metered` : ''}`);
|
|
579
|
-
|
|
612
|
+
const thinking = c.thinkingTokens > 0 ? ` · thinking ${formatTokenCount(c.thinkingTokens)} (${thinkingPercent(c.thinkingTokens, c.outputTokens)}% of out, ${formatUsd(c.thinkingCost)})` : '';
|
|
613
|
+
lines.push(` ${' '.repeat(width)} in ${formatTokenCount(c.inputTokens)} · out ${formatTokenCount(c.outputTokens)} · cache read ${formatTokenCount(c.cacheReadTokens)} · cache write ${formatTokenCount(c.cacheCreateTokens)}${thinking}`);
|
|
580
614
|
}
|
|
581
615
|
if (entries.length > limit)
|
|
582
616
|
lines.push(` … and ${entries.length - limit} more`);
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -451,7 +451,7 @@ export function loadTemplate(_options) {
|
|
|
451
451
|
// Stale cache: prefer whichever of the live cache and the bundled
|
|
452
452
|
// snapshot was captured more recently — do NOT blindly keep the cache.
|
|
453
453
|
// A frozen live cache must not shadow a newer bundled template, which is
|
|
454
|
-
// exactly what happens in a no-CC deployment (e.g.
|
|
454
|
+
// exactly what happens in a no-CC deployment (e.g. a container deploy):
|
|
455
455
|
// the async refresh can never run there, so the cache stays pinned at its
|
|
456
456
|
// last capture while shipped releases move the bundle ahead. Without this
|
|
457
457
|
// comparison, every bundled-template update is silently ignored until the
|
|
@@ -1069,7 +1069,7 @@ const STATIC_HEADER_EXCLUDE = new Set([
|
|
|
1069
1069
|
// Found 2026-07-25: the bundled template had been baked on Windows for
|
|
1070
1070
|
// several releases (#820/#828/#840/#849/#851 -> x-stainless-os: Windows,
|
|
1071
1071
|
// 30 tools incl. PowerShell), while cc-drift-template-watch runs its live
|
|
1072
|
-
// capture every 30 min on
|
|
1072
|
+
// capture every 30 min on a Linux self-hosted runner. So the Linux box served
|
|
1073
1073
|
// `x-stainless-os: Windows`, and the watch saw permanent drift against a
|
|
1074
1074
|
// bundle it could never match -- auto-rebaking to Linux (#852), which the next
|
|
1075
1075
|
// Windows-side bake (#854) would flip straight back. Excluding these ends
|