@askalf/dario 6.19.2 → 6.20.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 +4 -3
- 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.d.ts +9 -8
- package/dist/cc-template.js +39 -25
- package/dist/cli.js +44 -10
- package/dist/codex-backend.d.ts +13 -0
- package/dist/codex-backend.js +22 -1
- 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 +46 -12
- 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 +2 -2
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
|
package/dist/proxy.js
CHANGED
|
@@ -21,7 +21,7 @@ import { AccountPool, computeStickyKey, parseRateLimits, modelFamily, isInAuthCo
|
|
|
21
21
|
import { backfillIdentity } from './accounts.js';
|
|
22
22
|
import { PoolSync, DEFAULT_POOL_SYNC_INTERVAL_MS } from './pool-sync.js';
|
|
23
23
|
import { AccountSync, DEFAULT_ACCOUNT_SYNC_INTERVAL_MS } from './account-sync.js';
|
|
24
|
-
import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, CODEX_CLAIM, CREDITS_CLAIM } from './analytics.js';
|
|
24
|
+
import { Analytics, billingBucketFromClaim, costOfTokensFailClosed, formatUsageLogLine, SUBSCRIPTION_CLAIMS, consumerFromHeader, consumerFromBody, CONSUMER_HEADER, thinkingTokensFromUsage, CODEX_CLAIM, CREDITS_CLAIM } from './analytics.js';
|
|
25
25
|
import { Ledger, resolveLedgerPath, ledgerDisabledByEnv } from './ledger.js';
|
|
26
26
|
import { renderPrometheus } from './metrics.js';
|
|
27
27
|
import { renderSpendDonuts, renderAnalyticsView, analyticsUiPage } from './donuts.js';
|
|
@@ -36,7 +36,8 @@ import { createTokenBucket } from './rate-limit.js';
|
|
|
36
36
|
import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI, isMiniMaxBackend, MINIMAX_MODELS } from './openai-backend.js';
|
|
37
37
|
import { forwardToCodex, forwardResponsesToCodex, getCodexModelSlugs, peekCodexModelSlugs, isCodexModel, pickCodexFallback, pickClaudeTarget, CODEX_BACKEND_BASE_URL } from './codex-backend.js';
|
|
38
38
|
import { formatCacheTraceLine } from './cache-trace.js';
|
|
39
|
-
import { effortForCodex } from './effort.js';
|
|
39
|
+
import { effortForCodex, requestNamesEffort, withResponsesEffort } from './effort.js';
|
|
40
|
+
import { thinkingToReasoningEffort } from './anthropic-responses-translate.js';
|
|
40
41
|
import { MidstreamGuard, guardFor, loopbackBaseFor, chaosCutFetch, chaosCutState, CONTINUATION_HEADER, CONTINUATION_OF_HEADER, MAX_CONTINUATION_DEPTH, continuationDepth, continuationOfRequest } from './midstream.js';
|
|
41
42
|
/**
|
|
42
43
|
* The continuation fields for a request's analytics row, from its guard.
|
|
@@ -2609,7 +2610,10 @@ export async function startProxy(opts = {}) {
|
|
|
2609
2610
|
inputTokens: o.inputTokens, outputTokens: o.outputTokens,
|
|
2610
2611
|
// Anthropic convention, like every other row: inputTokens is
|
|
2611
2612
|
// net of the cached prefix, which sits in cacheReadTokens.
|
|
2612
|
-
cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
|
|
2613
|
+
cacheReadTokens: o.cacheReadTokens, cacheCreateTokens: o.cacheCreateTokens,
|
|
2614
|
+
// The backend's reasoning tokens, counted inside outputTokens exactly as
|
|
2615
|
+
// Claude's thinking is, so the ledger's thinking share covers both.
|
|
2616
|
+
thinkingTokens: o.reasoningTokens,
|
|
2613
2617
|
// No Anthropic rate-limit headers on this path; the claim
|
|
2614
2618
|
// names the engine and is subscription billing, so the
|
|
2615
2619
|
// overage guard (#288) leaves it alone.
|
|
@@ -2646,7 +2650,7 @@ export async function startProxy(opts = {}) {
|
|
|
2646
2650
|
* account, an unreadable one, or a `poolFallbackModel` that account does not
|
|
2647
2651
|
* list all mean "not mine to serve".
|
|
2648
2652
|
*
|
|
2649
|
-
* Works for BOTH wire shapes, which is the point:
|
|
2653
|
+
* Works for BOTH wire shapes, which is the point: SDK-engine agents and Claude
|
|
2650
2654
|
* Code speak Anthropic, and before v5.5.87 they had nowhere to fail over to
|
|
2651
2655
|
* and simply went dark when the Claude pool filled. The substituted model is
|
|
2652
2656
|
* announced on `x-dario-pool-fallback` exactly as the api-key path does —
|
|
@@ -2788,7 +2792,7 @@ export async function startProxy(opts = {}) {
|
|
|
2788
2792
|
* versions of this logic, and v6.0.0 wired the fallback into only ONE of
|
|
2789
2793
|
* them. The other kept returning the raw upstream 429 — and that unwired
|
|
2790
2794
|
* site is the one reached by every non-passthrough request, i.e. everything
|
|
2791
|
-
* except a literal Claude Code CLI session,
|
|
2795
|
+
* except a literal Claude Code CLI session, SDK-engine clients included.
|
|
2792
2796
|
* That is what left the whole fleet unable to fail over during the live
|
|
2793
2797
|
* outage on 2026-08-30 while `dario doctor` reported failover armed.
|
|
2794
2798
|
*
|
|
@@ -4351,6 +4355,26 @@ export async function startProxy(opts = {}) {
|
|
|
4351
4355
|
// Nothing below is allowed to depend on it: the tee only observes bytes
|
|
4352
4356
|
// already on their way out, the comparison is never awaited by the
|
|
4353
4357
|
// request, and every failure path resolves rather than throws.
|
|
4358
|
+
// A named key's default effort (`dario keys create --effort=low`) fills in
|
|
4359
|
+
// when nobody named one: no suffix on the model, no effort field in the
|
|
4360
|
+
// body. --effort / DARIO_EFFORT keeps winning over it, as it does over the
|
|
4361
|
+
// client, and a client that set its own effort is never overruled by a
|
|
4362
|
+
// default. Why this knob is per key: a reasoning model at its default
|
|
4363
|
+
// effort can spend most of its output thinking on work that did not need
|
|
4364
|
+
// it, and the lane that needs the cap is one consumer, not the proxy.
|
|
4365
|
+
// A Responses client names its effort in `reasoning.effort`, which the
|
|
4366
|
+
// translation above turns into a model suffix, so the raw body is read too.
|
|
4367
|
+
const keyDefaultEffort = requestAuth.key?.effort && !requestNamesEffort(parsedBody) && !requestNamesEffort(responsesBodyRaw)
|
|
4368
|
+
? requestAuth.key.effort : undefined;
|
|
4369
|
+
// The pin outranks a key default only when it names a level. `client`
|
|
4370
|
+
// means "leave the client's choice alone", and a key default exists only
|
|
4371
|
+
// when the client chose nothing, so under `client` the default applies.
|
|
4372
|
+
const pinnedEffort = opts.effort === 'client' && keyDefaultEffort ? undefined : opts.effort;
|
|
4373
|
+
// On the Codex translated route a client's thinking budget IS its effort
|
|
4374
|
+
// choice (it becomes `reasoning.effort` there), so a key default must not
|
|
4375
|
+
// replace it. The proxy pin has never reached the Codex backend.
|
|
4376
|
+
const codexKeyEffort = keyDefaultEffort && thinkingToReasoningEffort(parsedBody?.thinking) === undefined
|
|
4377
|
+
? keyDefaultEffort : undefined;
|
|
4354
4378
|
const compareTarget = readCompareTarget(req.headers);
|
|
4355
4379
|
if (compareTarget) {
|
|
4356
4380
|
const compareShape = isOpenAI ? 'openai' : 'anthropic';
|
|
@@ -4670,14 +4694,18 @@ export async function startProxy(opts = {}) {
|
|
|
4670
4694
|
// outage this change exists to remove, surviving on the one shape Codex
|
|
4671
4695
|
// CLI actually speaks (caught in review of #1288).
|
|
4672
4696
|
if (isResponses && responsesBodyRaw) {
|
|
4673
|
-
served = await forwardResponsesToCodex(
|
|
4697
|
+
served = await forwardResponsesToCodex(
|
|
4698
|
+
// The body as written, plus `reasoning.effort` from the model-name
|
|
4699
|
+
// suffix or the named key's default when the client set none.
|
|
4700
|
+
rawRes, withResponsesEffort({ ...responsesBodyRaw, model: rawModel }, effortForCodex(requestEffort ?? keyDefaultEffort)), codexSeat, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, codexFetch, codexOnDone, codexOnDecline, canDefer || codexPeer !== null, { queueMs, arrivedAt });
|
|
4674
4701
|
}
|
|
4675
4702
|
else {
|
|
4676
4703
|
served = await forwardToCodex(req, res, body, codexSeat, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', codexFetch, canDefer || codexPeer !== null, codexOnDone, codexOnDecline,
|
|
4677
4704
|
// dario#1260 — the effort named by the model-name suffix stripped
|
|
4678
|
-
// above
|
|
4679
|
-
//
|
|
4680
|
-
|
|
4705
|
+
// above, else the named key's default unless the client's thinking
|
|
4706
|
+
// budget already picks one. Undefined for every request that has
|
|
4707
|
+
// neither, which leaves the outbound body exactly as it was.
|
|
4708
|
+
effortForCodex(requestEffort ?? codexKeyEffort), codexGuard, { queueMs, arrivedAt });
|
|
4681
4709
|
}
|
|
4682
4710
|
if (served || !codexPeer)
|
|
4683
4711
|
break;
|
|
@@ -4978,7 +5006,7 @@ export async function startProxy(opts = {}) {
|
|
|
4978
5006
|
sanitizeMessages(client, opts.preserveOrchestrationTags);
|
|
4979
5007
|
const r = (isOpenAI ? openaiToAnthropic(client, target.model) : { ...client, model: target.model });
|
|
4980
5008
|
r.model = stripContext1mTag(typeof r.model === 'string' ? r.model : target.model);
|
|
4981
|
-
const effort = requestEffort ??
|
|
5009
|
+
const effort = requestEffort ?? pinnedEffort ?? keyDefaultEffort;
|
|
4982
5010
|
if (effort) {
|
|
4983
5011
|
const oc = (r.output_config && typeof r.output_config === 'object' && !Array.isArray(r.output_config))
|
|
4984
5012
|
? { ...r.output_config }
|
|
@@ -5128,7 +5156,7 @@ export async function startProxy(opts = {}) {
|
|
|
5128
5156
|
hybridTools: opts.hybridTools ?? false,
|
|
5129
5157
|
mergeTools: opts.mergeTools ?? false,
|
|
5130
5158
|
noAutoDetect: opts.noAutoDetect ?? false,
|
|
5131
|
-
effort: requestEffort ??
|
|
5159
|
+
effort: requestEffort ?? pinnedEffort ?? keyDefaultEffort,
|
|
5132
5160
|
maxTokens: opts.maxTokens,
|
|
5133
5161
|
systemPrompt: opts.systemPrompt,
|
|
5134
5162
|
skipFields,
|
|
@@ -6210,6 +6238,9 @@ export async function startProxy(opts = {}) {
|
|
|
6210
6238
|
let streamCacheReadTokens = 0;
|
|
6211
6239
|
let streamCacheCreateTokens = 0;
|
|
6212
6240
|
let streamThinkingChars = 0;
|
|
6241
|
+
// The billed count from message_delta's usage.output_tokens_details,
|
|
6242
|
+
// when upstream sends it; the chars/4 estimate above is the fallback.
|
|
6243
|
+
let streamThinkingTokens;
|
|
6213
6244
|
const analyticsDecoder = analytics ? new TextDecoder() : null;
|
|
6214
6245
|
let analyticsBuffer = '';
|
|
6215
6246
|
// Stream SSE chunks through
|
|
@@ -6317,6 +6348,9 @@ export async function startProxy(opts = {}) {
|
|
|
6317
6348
|
const u = e.usage;
|
|
6318
6349
|
if (u?.output_tokens)
|
|
6319
6350
|
streamOutputTokens = u.output_tokens;
|
|
6351
|
+
const billedThinking = thinkingTokensFromUsage(u);
|
|
6352
|
+
if (billedThinking !== undefined)
|
|
6353
|
+
streamThinkingTokens = billedThinking;
|
|
6320
6354
|
}
|
|
6321
6355
|
else if (e.type === 'content_block_delta') {
|
|
6322
6356
|
// Mirror the non-streaming parseUsage thinking-token
|
|
@@ -6422,7 +6456,7 @@ export async function startProxy(opts = {}) {
|
|
|
6422
6456
|
model: requestModel,
|
|
6423
6457
|
inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
|
|
6424
6458
|
cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
|
|
6425
|
-
thinkingTokens: Math.round(streamThinkingChars / 4),
|
|
6459
|
+
thinkingTokens: streamThinkingTokens ?? Math.round(streamThinkingChars / 4),
|
|
6426
6460
|
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
6427
6461
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI, timing: timingNow(),
|
|
6428
6462
|
continuation: continuationOf(guard, requestDepth),
|
package/docs/admin-api.md
CHANGED
|
@@ -94,8 +94,9 @@ All endpoints accept the token as `authorization: Bearer <token>` or
|
|
|
94
94
|
| `GET /admin/accounts` | — | `{ accounts: [...], count }` |
|
|
95
95
|
| `DELETE /admin/accounts/<alias>` | — | `{ alias, removed }` (`404` if no such alias) |
|
|
96
96
|
| `GET /admin/keys` | — | `{ keys: [...], count, path }` — every named key, hashes excluded (v6.8) |
|
|
97
|
-
| `POST /admin/keys` | `{ "name": string, "seat"?: string, "models"?: string[] \| "a,b", "expires"?: "30d" \| ISO }` | `201 { key, secret }
|
|
97
|
+
| `POST /admin/keys` | `{ "name": string, "seat"?: string, "models"?: string[] \| "a,b", "expires"?: "30d" \| ISO, "effort"?: "low" \| "medium" \| "high" \| "xhigh" \| "max" }` | `201 { key, secret }`, the secret shown once; `409` if the name exists |
|
|
98
98
|
| `POST /admin/keys/<name>/rotate` | — | `{ key, secret }` — a new secret, the old one dead at once (`404` if unknown) |
|
|
99
|
+
| `POST /admin/keys/<name>/effort` | `{ "effort": "low" \| "medium" \| "high" \| "xhigh" \| "max" \| null }` | `{ key }` (`404` if unknown); null or absent clears the default |
|
|
99
100
|
| `DELETE /admin/keys/<name>` | — | `{ name, revoked }` — refused from now on, kept in the list (`404` if unknown) |
|
|
100
101
|
|
|
101
102
|
The three `/admin/keys` mutations edit the same `~/.dario/keys.json` that
|
package/docs/configuration.md
CHANGED
|
@@ -93,9 +93,10 @@ Off by default, each one a deliberate divergence from what real CC sends.
|
|
|
93
93
|
|
|
94
94
|
### Per-request effort, by model name
|
|
95
95
|
|
|
96
|
-
`DARIO_EFFORT` is process-wide: it applies to every caller
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
`DARIO_EFFORT` is process-wide: it applies to every caller on the Claude path
|
|
97
|
+
(it does not reach the Codex backend; a suffix or a key default does). A
|
|
98
|
+
client that has no way to set `output_config.effort` can instead name the
|
|
99
|
+
level in the model itself, and only that request changes:
|
|
99
100
|
|
|
100
101
|
```
|
|
101
102
|
claude-opus-4-8:high colon form
|
|
@@ -118,6 +119,12 @@ lists, so a real model id that happens to end in an effort word is routed
|
|
|
118
119
|
exactly as written and never re-read. A `--pool-fallback` chain entry may carry
|
|
119
120
|
one too, which sets the effort the failover request runs at.
|
|
120
121
|
|
|
122
|
+
A named key can carry a default as well (`dario keys create <name> --effort=low`,
|
|
123
|
+
`dario keys effort <name> low`). It applies when the request names no effort of
|
|
124
|
+
its own: below the suffix, the operator pin and the client's own field, above
|
|
125
|
+
the `high` fallback. On the Codex path it is added to the Responses body Codex
|
|
126
|
+
CLI sends as well. See [keys.md](keys.md#default-effort).
|
|
127
|
+
|
|
121
128
|
Effort is not free. Measured through dario on one prompt with only the level
|
|
122
129
|
changing: `low` returned 1,982 output tokens in 18s, `high` 3,947 in 35s, and
|
|
123
130
|
`max` 11,160 in 100s. Reasoning is billed as output, so `max` is a real cost
|
package/docs/drift-monitor.md
CHANGED
|
@@ -219,7 +219,7 @@ sudo npm i -g @anthropic-ai/claude-code @askalf/dario
|
|
|
219
219
|
# URL back into the SSH session.
|
|
220
220
|
#
|
|
221
221
|
# SHARE the credential with any other CC clients that auto-refresh
|
|
222
|
-
# (e.g. a
|
|
222
|
+
# (e.g. a dario container running 24/7). Just run the standard
|
|
223
223
|
# `dario login --manual` against /root/.claude/.credentials.json and let
|
|
224
224
|
# that long-running refresh authority keep the token fresh. The workflows
|
|
225
225
|
# read whatever's current at fire time and never attempt a refresh from
|
|
@@ -235,7 +235,7 @@ sudo npm i -g @anthropic-ai/claude-code @askalf/dario
|
|
|
235
235
|
#
|
|
236
236
|
# Sharing with a 24/7 refresh authority (typical setup: the docker-stack
|
|
237
237
|
# dario container) fixes that. The race the isolation was protecting
|
|
238
|
-
# against is rare in practice
|
|
238
|
+
# against is rare in practice: a long-running dario refreshes proactively
|
|
239
239
|
# when the access token has ~1h life remaining, so workflow runs hit a
|
|
240
240
|
# fresh token and don't need to refresh themselves.
|
|
241
241
|
dario login --manual
|
|
@@ -332,13 +332,13 @@ Workflows that exercise live `dario proxy` paths (compat-test, billing canary, f
|
|
|
332
332
|
| `cc-billing-classifier-canary.yml` | daily 06:30 UTC | daily | 1 small haiku request |
|
|
333
333
|
| `compat-test-self-hosted.yml` | per qualifying PR | per qualifying PR | ~11 small requests |
|
|
334
334
|
|
|
335
|
-
**Cron scheduler reality.** GitHub Actions' free-tier cron scheduler is best-effort, not guaranteed. The class-B watcher declares `*/30 * * * *` but in practice GitHub honors it every 2-4 hours on this repo. The liveness alarm (added v4.4.2) has its threshold set to 8h (raised from 3h in v4.7.1) to absorb this skew
|
|
335
|
+
**Cron scheduler reality.** GitHub Actions' free-tier cron scheduler is best-effort, not guaranteed. The class-B watcher declares `*/30 * * * *` but in practice GitHub honors it every 2-4 hours on this repo. The liveness alarm (added v4.4.2) has its threshold set to 8h (raised from 3h in v4.7.1) to absorb this skew; anything past that is signal, not scheduler noise. If you need a tighter SLA (sub-hour), self-host the runner *and* the cron driver (e.g. a cron entry on the same host invoking `gh workflow run` directly).
|
|
336
336
|
|
|
337
337
|
At steady state, this is comfortably inside Pro/Max headroom. The failure mode to watch for is **batched firing** — manually re-triggering the same workflow several times in a single hour, or PRs landing in rapid succession that each fire compat-test. We tripped this during the v4.6.x rollout: a half-dozen manual re-runs in a 2-hour window 429'd the runner credential. Pro/Max accounts have per-hour rate caps as well as per-5h / per-7d pools, and the per-hour cap is what surfaces first.
|
|
338
338
|
|
|
339
339
|
If the runner credential is rate-limited and a workflow run reports 429s across the board, the right diagnosis order is: (a) check `claude --print` directly — if it 429s, the credential pool is dry, just wait an hour; (b) check the credential is still on a subscription account (`dario doctor`); (c) check workflow cadence assumptions haven't changed.
|
|
340
340
|
|
|
341
|
-
The runner shares its OAuth credential with any other long-running CC client on the box (typically
|
|
341
|
+
The runner shares its OAuth credential with any other long-running CC client on the box (typically a long-running dario container, which auto-refreshes 24/7). Sharing is intentional: a workflow that fires sparsely cannot keep its own refresh token alive: Anthropic invalidates idle refresh tokens, and `invalid_grant` then breaks every subsequent run. Letting a 24/7 refresh authority own the token rotation eliminates that failure mode at the cost of competing for the same Pro/Max headroom. With current cadence (drift-template-watch every 30 min + compat per PR + canary daily), runner-side burn on the shared account is a manageable fraction of the headroom available on a Max plan; reducing cadence further is a knob if a particular workload needs more of it.
|
|
342
342
|
|
|
343
343
|
## Why a self-hosted runner
|
|
344
344
|
|