@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/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.
|
|
@@ -2646,7 +2647,7 @@ export async function startProxy(opts = {}) {
|
|
|
2646
2647
|
* account, an unreadable one, or a `poolFallbackModel` that account does not
|
|
2647
2648
|
* list all mean "not mine to serve".
|
|
2648
2649
|
*
|
|
2649
|
-
* Works for BOTH wire shapes, which is the point:
|
|
2650
|
+
* Works for BOTH wire shapes, which is the point: SDK-engine agents and Claude
|
|
2650
2651
|
* Code speak Anthropic, and before v5.5.87 they had nowhere to fail over to
|
|
2651
2652
|
* and simply went dark when the Claude pool filled. The substituted model is
|
|
2652
2653
|
* announced on `x-dario-pool-fallback` exactly as the api-key path does —
|
|
@@ -2788,7 +2789,7 @@ export async function startProxy(opts = {}) {
|
|
|
2788
2789
|
* versions of this logic, and v6.0.0 wired the fallback into only ONE of
|
|
2789
2790
|
* them. The other kept returning the raw upstream 429 — and that unwired
|
|
2790
2791
|
* site is the one reached by every non-passthrough request, i.e. everything
|
|
2791
|
-
* except a literal Claude Code CLI session,
|
|
2792
|
+
* except a literal Claude Code CLI session, SDK-engine clients included.
|
|
2792
2793
|
* That is what left the whole fleet unable to fail over during the live
|
|
2793
2794
|
* outage on 2026-08-30 while `dario doctor` reported failover armed.
|
|
2794
2795
|
*
|
|
@@ -4351,6 +4352,26 @@ export async function startProxy(opts = {}) {
|
|
|
4351
4352
|
// Nothing below is allowed to depend on it: the tee only observes bytes
|
|
4352
4353
|
// already on their way out, the comparison is never awaited by the
|
|
4353
4354
|
// request, and every failure path resolves rather than throws.
|
|
4355
|
+
// A named key's default effort (`dario keys create --effort=low`) fills in
|
|
4356
|
+
// when nobody named one: no suffix on the model, no effort field in the
|
|
4357
|
+
// body. --effort / DARIO_EFFORT keeps winning over it, as it does over the
|
|
4358
|
+
// client, and a client that set its own effort is never overruled by a
|
|
4359
|
+
// default. Why this knob is per key: a reasoning model at its default
|
|
4360
|
+
// effort can spend most of its output thinking on work that did not need
|
|
4361
|
+
// it, and the lane that needs the cap is one consumer, not the proxy.
|
|
4362
|
+
// A Responses client names its effort in `reasoning.effort`, which the
|
|
4363
|
+
// translation above turns into a model suffix, so the raw body is read too.
|
|
4364
|
+
const keyDefaultEffort = requestAuth.key?.effort && !requestNamesEffort(parsedBody) && !requestNamesEffort(responsesBodyRaw)
|
|
4365
|
+
? requestAuth.key.effort : undefined;
|
|
4366
|
+
// The pin outranks a key default only when it names a level. `client`
|
|
4367
|
+
// means "leave the client's choice alone", and a key default exists only
|
|
4368
|
+
// when the client chose nothing, so under `client` the default applies.
|
|
4369
|
+
const pinnedEffort = opts.effort === 'client' && keyDefaultEffort ? undefined : opts.effort;
|
|
4370
|
+
// On the Codex translated route a client's thinking budget IS its effort
|
|
4371
|
+
// choice (it becomes `reasoning.effort` there), so a key default must not
|
|
4372
|
+
// replace it. The proxy pin has never reached the Codex backend.
|
|
4373
|
+
const codexKeyEffort = keyDefaultEffort && thinkingToReasoningEffort(parsedBody?.thinking) === undefined
|
|
4374
|
+
? keyDefaultEffort : undefined;
|
|
4354
4375
|
const compareTarget = readCompareTarget(req.headers);
|
|
4355
4376
|
if (compareTarget) {
|
|
4356
4377
|
const compareShape = isOpenAI ? 'openai' : 'anthropic';
|
|
@@ -4670,14 +4691,18 @@ export async function startProxy(opts = {}) {
|
|
|
4670
4691
|
// outage this change exists to remove, surviving on the one shape Codex
|
|
4671
4692
|
// CLI actually speaks (caught in review of #1288).
|
|
4672
4693
|
if (isResponses && responsesBodyRaw) {
|
|
4673
|
-
served = await forwardResponsesToCodex(
|
|
4694
|
+
served = await forwardResponsesToCodex(
|
|
4695
|
+
// The body as written, plus `reasoning.effort` from the model-name
|
|
4696
|
+
// suffix or the named key's default when the client set none.
|
|
4697
|
+
rawRes, withResponsesEffort({ ...responsesBodyRaw, model: rawModel }, effortForCodex(requestEffort ?? keyDefaultEffort)), codexSeat, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, codexFetch, codexOnDone, codexOnDecline, canDefer || codexPeer !== null, { queueMs, arrivedAt });
|
|
4674
4698
|
}
|
|
4675
4699
|
else {
|
|
4676
4700
|
served = await forwardToCodex(req, res, body, codexSeat, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose, isOpenAI ? 'openai' : 'anthropic', codexFetch, canDefer || codexPeer !== null, codexOnDone, codexOnDecline,
|
|
4677
4701
|
// dario#1260 — the effort named by the model-name suffix stripped
|
|
4678
|
-
// above
|
|
4679
|
-
//
|
|
4680
|
-
|
|
4702
|
+
// above, else the named key's default unless the client's thinking
|
|
4703
|
+
// budget already picks one. Undefined for every request that has
|
|
4704
|
+
// neither, which leaves the outbound body exactly as it was.
|
|
4705
|
+
effortForCodex(requestEffort ?? codexKeyEffort), codexGuard, { queueMs, arrivedAt });
|
|
4681
4706
|
}
|
|
4682
4707
|
if (served || !codexPeer)
|
|
4683
4708
|
break;
|
|
@@ -4978,7 +5003,7 @@ export async function startProxy(opts = {}) {
|
|
|
4978
5003
|
sanitizeMessages(client, opts.preserveOrchestrationTags);
|
|
4979
5004
|
const r = (isOpenAI ? openaiToAnthropic(client, target.model) : { ...client, model: target.model });
|
|
4980
5005
|
r.model = stripContext1mTag(typeof r.model === 'string' ? r.model : target.model);
|
|
4981
|
-
const effort = requestEffort ??
|
|
5006
|
+
const effort = requestEffort ?? pinnedEffort ?? keyDefaultEffort;
|
|
4982
5007
|
if (effort) {
|
|
4983
5008
|
const oc = (r.output_config && typeof r.output_config === 'object' && !Array.isArray(r.output_config))
|
|
4984
5009
|
? { ...r.output_config }
|
|
@@ -5128,7 +5153,7 @@ export async function startProxy(opts = {}) {
|
|
|
5128
5153
|
hybridTools: opts.hybridTools ?? false,
|
|
5129
5154
|
mergeTools: opts.mergeTools ?? false,
|
|
5130
5155
|
noAutoDetect: opts.noAutoDetect ?? false,
|
|
5131
|
-
effort: requestEffort ??
|
|
5156
|
+
effort: requestEffort ?? pinnedEffort ?? keyDefaultEffort,
|
|
5132
5157
|
maxTokens: opts.maxTokens,
|
|
5133
5158
|
systemPrompt: opts.systemPrompt,
|
|
5134
5159
|
skipFields,
|
|
@@ -6210,6 +6235,9 @@ export async function startProxy(opts = {}) {
|
|
|
6210
6235
|
let streamCacheReadTokens = 0;
|
|
6211
6236
|
let streamCacheCreateTokens = 0;
|
|
6212
6237
|
let streamThinkingChars = 0;
|
|
6238
|
+
// The billed count from message_delta's usage.output_tokens_details,
|
|
6239
|
+
// when upstream sends it; the chars/4 estimate above is the fallback.
|
|
6240
|
+
let streamThinkingTokens;
|
|
6213
6241
|
const analyticsDecoder = analytics ? new TextDecoder() : null;
|
|
6214
6242
|
let analyticsBuffer = '';
|
|
6215
6243
|
// Stream SSE chunks through
|
|
@@ -6317,6 +6345,9 @@ export async function startProxy(opts = {}) {
|
|
|
6317
6345
|
const u = e.usage;
|
|
6318
6346
|
if (u?.output_tokens)
|
|
6319
6347
|
streamOutputTokens = u.output_tokens;
|
|
6348
|
+
const billedThinking = thinkingTokensFromUsage(u);
|
|
6349
|
+
if (billedThinking !== undefined)
|
|
6350
|
+
streamThinkingTokens = billedThinking;
|
|
6320
6351
|
}
|
|
6321
6352
|
else if (e.type === 'content_block_delta') {
|
|
6322
6353
|
// Mirror the non-streaming parseUsage thinking-token
|
|
@@ -6422,7 +6453,7 @@ export async function startProxy(opts = {}) {
|
|
|
6422
6453
|
model: requestModel,
|
|
6423
6454
|
inputTokens: streamInputTokens, outputTokens: streamOutputTokens,
|
|
6424
6455
|
cacheReadTokens: streamCacheReadTokens, cacheCreateTokens: streamCacheCreateTokens,
|
|
6425
|
-
thinkingTokens: Math.round(streamThinkingChars / 4),
|
|
6456
|
+
thinkingTokens: streamThinkingTokens ?? Math.round(streamThinkingChars / 4),
|
|
6426
6457
|
claim: claimNow(rl), util5h: rl.util5h, util7d: rl.util7d, overageUtil: rl.overageUtil,
|
|
6427
6458
|
latencyMs: Date.now() - startTime, status: upstream.status, isStream: true, isOpenAI, timing: timingNow(),
|
|
6428
6459
|
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
|
|
package/docs/keys.md
CHANGED
|
@@ -34,12 +34,45 @@ on its next request, no restart.
|
|
|
34
34
|
| `--expires=30d` | Refused after this, like a revoked key. `12h`, `2w`, or an ISO date. |
|
|
35
35
|
| `--budget=$5/day` | A daily cap on the API-equivalent price of the key's traffic. See **Budgets**. |
|
|
36
36
|
| `--budget-tokens=2M/day` | A daily cap on tokens, every bucket counted. See **Budgets**. |
|
|
37
|
+
| `--effort=low` | The effort this key's requests run at when they name none themselves (no model-name suffix, no effort field). `low`, `medium`, `high`, `xhigh` or `max`. See **Default effort**. |
|
|
37
38
|
|
|
38
39
|
`dario keys revoke <name>` refuses a key from now on and keeps it in the list;
|
|
39
40
|
`dario keys rotate <name>` prints a new secret under the same name, seat,
|
|
40
41
|
models and expiry, and the old secret stops at once; `dario keys remove
|
|
41
42
|
<name>` forgets it.
|
|
42
43
|
|
|
44
|
+
## Default effort
|
|
45
|
+
|
|
46
|
+
`dario keys create alice --effort=low`, or `dario keys effort alice low` on an
|
|
47
|
+
existing key (`--clear` to remove it), sets the effort the key's requests run
|
|
48
|
+
at when they name none themselves. Resolution per request, first match wins:
|
|
49
|
+
|
|
50
|
+
1. an effort suffix on the model name (`claude-opus-5-5:low`);
|
|
51
|
+
2. the proxy's `--effort` / `DARIO_EFFORT` pin, when set to a level (`client`
|
|
52
|
+
is not a level: under it the key's default still fills in);
|
|
53
|
+
3. the client's own effort field (`output_config.effort`, `reasoning_effort`, `reasoning.effort`);
|
|
54
|
+
4. the key's default;
|
|
55
|
+
5. `high`, the proxy's long-standing fallback.
|
|
56
|
+
|
|
57
|
+
So a key default never overrules a client that chose, and the operator's pin
|
|
58
|
+
still overrules everything, as it always has. On the Claude path the level
|
|
59
|
+
goes out as `output_config.effort`.
|
|
60
|
+
|
|
61
|
+
On the Codex path it goes out as `reasoning.effort`, on both routes: the
|
|
62
|
+
Messages-shape request dario translates, and the Responses body Codex CLI
|
|
63
|
+
sends, which dario otherwise forwards as written (the level is added to its
|
|
64
|
+
`reasoning` object; `summary` and the rest stay). Two differences there. The
|
|
65
|
+
proxy pin has never reached the Codex backend, so it is not in the order. And
|
|
66
|
+
a Messages-shape client's `thinking.budget_tokens` is that client's choice on
|
|
67
|
+
Codex (it becomes `reasoning.effort`), so it outranks the key's default the
|
|
68
|
+
way the client's own field does. With none of those, the backend's own
|
|
69
|
+
default applies, not `high`.
|
|
70
|
+
Why the knob is per key: a reasoning model at its default effort can spend
|
|
71
|
+
most of its output thinking on work that did not need it (the `Thinking:` line
|
|
72
|
+
in `dario usage` shows the share per key), and the lane that needs the cap is
|
|
73
|
+
usually one consumer, not the whole proxy. `dario keys list` shows an EFFORT
|
|
74
|
+
column; `GET /admin/keys` and `dario keys --json` carry `effort`.
|
|
75
|
+
|
|
43
76
|
## Budgets
|
|
44
77
|
|
|
45
78
|
`dario keys create alice --budget=$5/day --budget-tokens=2M/day` — or
|
|
@@ -91,7 +124,9 @@ fields (an empty body clears), audited as `key_budget`.
|
|
|
91
124
|
to the key; the header cannot overrule a credential.
|
|
92
125
|
- **`dario usage --by-key`** — the lifetime API-equivalent number split per
|
|
93
126
|
consumer: named keys, header names, and the `u_…` hash of a client's user
|
|
94
|
-
id, whichever named the request.
|
|
127
|
+
id, whichever named the request. The tokens line under each key includes
|
|
128
|
+
its thinking share: how many of the key's output tokens were the model's
|
|
129
|
+
reasoning, and what they cost at the output rate.
|
|
95
130
|
- **The log file** — `consumer` on every request line; `reject: "key-model"`
|
|
96
131
|
on a model refusal; `event: "admin.key_create|key_rotate|key_revoke"` with
|
|
97
132
|
`key: <name>` when the admin API changed something.
|
|
@@ -69,8 +69,11 @@ The rolling window forgets on every restart; the **ledger** does not. Since 6.6
|
|
|
69
69
|
Claude $388 1,204 reqs (Opus 5 $301 · Sonnet 5 $86.68)
|
|
70
70
|
ChatGPT $24.75 311 reqs (gpt-5.6-terra $24.75)
|
|
71
71
|
Today $48.20 · Last 7d $413 · Last 30d $413
|
|
72
|
+
Thinking: 2.1M of 3.4M output tokens (62%) · $156 of the number above
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
The `Thinking:` line is the share of output tokens that were the model's reasoning, read from the usage block upstream returns on Claude traffic, and what that reasoning would have cost at each model's output rate. It is the first number to look at when a bill surprises you: a reasoning model at its default effort can spend most of its output thinking on work that did not need it, and `output_config.effort` (or a `:low` model-name suffix, see [configuration](configuration.md#per-request-effort-by-model-name)) is a cheaper fix than a smaller model. `dario usage --by-key` shows the share per key, so the lane that is thinking the most is one line away.
|
|
76
|
+
|
|
74
77
|
Only served requests count. Traffic that was metered anyway — an API key upstream, or Anthropic's paid `extra_usage` overage — is kept in its own column and reported as spent, not saved. `dario usage --card` writes the headline as a 640×320 SVG you can drop in a README or a post, and `--donut` writes the same number as three rings — by model, by key, subscription vs metered; `--no-ledger` / `DARIO_LEDGER=0` turns the file off, `DARIO_LEDGER_PATH` moves it, and `GET /analytics/ledger` is the per-day table behind the number. Details: [api-equivalent-spend.md](api-equivalent-spend.md).
|
|
75
78
|
|
|
76
79
|
A request served on a Max plan's API credits (the `credits:<model>` rung of the failover chain, key in `DARIO_CREDITS_API_KEY`, [failover](two-plans.md#failover-between-subscriptions)) is counted as spent the same way in the ledger; `/analytics`, `/metrics` and the dashboard show it under its own bucket, `credits`.
|
package/docs/system-prompt.md
CHANGED
|
@@ -13,6 +13,8 @@ The empirical basis for this feature lives in [`docs/research/system-prompt-clas
|
|
|
13
13
|
| `aggressive` | Partial + remove the prompt-level RLHF restatements (`IMPORTANT: Assist with authorized security testing…`, `IMPORTANT: You must NEVER generate or guess URLs…`) and the `# Executing actions with care` section. | <3% above partial |
|
|
14
14
|
| `<file path>` | Replace the slot entirely with the contents of a file you control. The escape hatch for users running well-defined agent workflows with their own system prompt. | depends on your prompt |
|
|
15
15
|
|
|
16
|
+
What `partial` finds to strip depends on the prompt Claude Code sends for the model. The Sonnet 5 prompt has the `# Tone and style` and `# Text output` sections and the Doing-tasks bullets. The Fable prompt has a one-line rule about code comments, which `partial` swaps for a "be thorough" instruction. The base prompt (Claude Code 2.1.316) and the Opus 5 prompt carry none of these, so `partial` sends them unchanged and only `aggressive` changes them.
|
|
17
|
+
|
|
16
18
|
## Aggressive vs partial — what's the actual difference?
|
|
17
19
|
|
|
18
20
|
Aggressive is provided for completeness, not because it does meaningful work. The added removals are *prompt-level restatements* of refusal categories — reminders the prompt makes about RLHF behavior that's already trained into the model's weights. Removing the reminder doesn't remove the trained behavior. We measured this: 9 trials (3 prompts × 3 strip levels), aggressive vs partial added <3% practical change on benign tasks.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.20.0",
|
|
4
4
|
"description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, opt-in failover when one hits its limit, and a ledger of what each plan absorbed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|