@askalf/dario 6.19.2 → 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 CHANGED
@@ -10,6 +10,7 @@
10
10
  <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/v/@askalf/dario?color=6f42c1&label=npm&logo=npm" alt="npm version"></a>
11
11
  <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario?color=6f42c1" alt="Downloads"></a>
12
12
  <a href="https://github.com/askalf/dario/actions/workflows/ci.yml"><img src="https://github.com/askalf/dario/actions/workflows/ci.yml/badge.svg?branch=master" alt="CI"></a>
13
+ <a href="https://pitlane.askalf.org/"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fpitlane.askalf.org%2Fbadge.json" alt="CI on pitlane"></a>
13
14
  <a href="https://scorecard.dev/viewer/?uri=github.com/askalf/dario"><img src="https://api.scorecard.dev/projects/github.com/askalf/dario/badge" alt="OpenSSF Scorecard"></a>
14
15
  <a href="https://github.com/askalf/dario/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@askalf/dario?color=6f42c1" alt="License"></a>
15
16
  </p>
@@ -117,7 +118,7 @@ dario runs entirely on your machine, authenticates as you with your own Claude l
117
118
  | Dependencies | **0 runtime.** Verify: `npm ls --production` |
118
119
  | Provenance | Every release [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions + Sigstore, published with OIDC trusted publishing: no long-lived npm token exists to leak |
119
120
  | Scanning | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) on every push to master, every PR and weekly · [ClusterFuzzLite](./.github/workflows/cflite.yml) fuzzes the SSE translator, the rejection parsers and the cch stamper weekly · the [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/askalf/dario) badge above and the [Best Practices](https://www.bestpractices.dev/projects/13638) entry are live |
120
- | Tests | **285 test files** run in parallel by `npm test` on Node 18, 20 and 22; the live e2e / compat / stealth suites have their own entry points. Green on every release |
121
+ | Tests | **287 test files** run in parallel by `npm test` on Node 18, 20 and 22; the live e2e / compat / stealth suites have their own entry points. Green on every release |
121
122
  | Credentials | Your own subscription tokens, never logged, redacted from errors, `0600` on disk in `0700` dirs |
122
123
  | Network | Binds `127.0.0.1` by default; upstream only to configured backends over HTTPS; hardcoded SSRF allow-list; refuses a non-loopback bind without `DARIO_API_KEY`; refuses cross-origin and DNS-rebound browser requests |
123
124
  | Telemetry | **None.** No analytics, no tracking, nothing phones home |
@@ -168,7 +168,7 @@ export interface AdminCodexAccountRecord extends CodexSeatState {
168
168
  usage: CodexUsageView | null;
169
169
  }
170
170
  export interface AdminAuditEvent {
171
- action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited' | 'key_create' | 'key_revoke' | 'key_rotate' | 'key_budget';
171
+ action: 'login_start' | 'login_complete' | 'account_remove' | 'auth_reject' | 'rate_limited' | 'key_create' | 'key_revoke' | 'key_rotate' | 'key_budget' | 'key_effort';
172
172
  /** Which engine's credentials the event touched; absent means Claude, the only engine before codex joined (dario#1009). */
173
173
  engine?: 'codex';
174
174
  ok: boolean;
package/dist/admin-api.js CHANGED
@@ -5,7 +5,7 @@ import { startAddCodexAccount, completeAddCodexAccount, removeCodexAccount, load
5
5
  import { codexUsageView } from './codex-usage.js';
6
6
  import { parseManualPaste } from './oauth.js';
7
7
  import { grantAge } from './refresh-grant.js';
8
- import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, setKeyBudget, normalizeBudget, KEY_NAME_RE } from './keys.js';
8
+ import { createKey, revokeKey, rotateKey, parseExpiry, publicKey, setKeyBudget, setKeyEffort, isKeyEffort, normalizeBudget, KEY_NAME_RE, KEY_EFFORT_VALUES } from './keys.js';
9
9
  /**
10
10
  * `{ budget_usd_per_day, budget_tokens_per_day }` (numbers, or numeric strings)
11
11
  * → a KeyBudget; undefined when neither is present; throws with a 400-worthy
@@ -267,8 +267,10 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
267
267
  const isKeyRotate = keyTarget !== null && keyTarget.endsWith('/rotate');
268
268
  // POST /admin/keys/<name>/budget — set or clear a key's daily caps (dario#1318 follow-up).
269
269
  const isKeyBudget = keyTarget !== null && keyTarget.endsWith('/budget');
270
- const keyName = keyTarget === null ? null : isKeyRotate ? keyTarget.slice(0, -'/rotate'.length) : isKeyBudget ? keyTarget.slice(0, -'/budget'.length) : keyTarget;
271
- const isKeyRevoke = keyTarget !== null && !isKeyRotate && !isKeyBudget && method === 'DELETE';
270
+ // POST /admin/keys/<name>/effort: set or clear a key's default effort.
271
+ const isKeyEffortRoute = keyTarget !== null && keyTarget.endsWith('/effort');
272
+ const keyName = keyTarget === null ? null : isKeyRotate ? keyTarget.slice(0, -'/rotate'.length) : isKeyBudget ? keyTarget.slice(0, -'/budget'.length) : isKeyEffortRoute ? keyTarget.slice(0, -'/effort'.length) : keyTarget;
273
+ const isKeyRevoke = keyTarget !== null && !isKeyRotate && !isKeyBudget && !isKeyEffortRoute && method === 'DELETE';
272
274
  const known = urlPath === '/admin/login/start' ||
273
275
  urlPath === '/admin/login/start-needed' ||
274
276
  urlPath === '/admin/login/complete' ||
@@ -281,6 +283,7 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
281
283
  isAccountDelete ||
282
284
  isKeyRotate ||
283
285
  isKeyBudget ||
286
+ isKeyEffortRoute ||
284
287
  isKeyRevoke;
285
288
  if (!known)
286
289
  return false;
@@ -313,7 +316,7 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
313
316
  // accounts' credentials for the price of one throttle token.
314
317
  const isMutation = urlPath === '/admin/login/start' || isAccountDelete
315
318
  || urlPath === '/admin/codex/login/start' || isCodexAccountDelete
316
- || (urlPath === '/admin/keys' && method === 'POST') || isKeyRotate || isKeyBudget || isKeyRevoke;
319
+ || (urlPath === '/admin/keys' && method === 'POST') || isKeyRotate || isKeyBudget || isKeyEffortRoute || isKeyRevoke;
317
320
  if (isMutation) {
318
321
  const wait = deps.rateLimit?.('mutation') ?? 0;
319
322
  if (wait > 0) {
@@ -642,8 +645,16 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
642
645
  send(res, 400, { error: err.message });
643
646
  return true;
644
647
  }
648
+ let effort;
649
+ if (body.effort !== undefined && body.effort !== null) {
650
+ if (!isKeyEffort(body.effort)) {
651
+ send(res, 400, { error: `invalid "effort": one of ${KEY_EFFORT_VALUES.join(', ')}` });
652
+ return true;
653
+ }
654
+ effort = body.effort;
655
+ }
645
656
  try {
646
- const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, now }));
657
+ const made = store.mutate((file) => createKey(file, name, { seat, models, expiresAt, budget, effort, now }));
647
658
  deps.audit?.({ action: 'key_create', ok: true, status: 201, key: made.record.name, remote, detail: seat ? `seat=${seat}` : undefined });
648
659
  send(res, 201, { key: publicKey(made.record, now), secret: made.secret, note: 'the secret is shown once and is not stored' });
649
660
  }
@@ -702,6 +713,30 @@ export async function handleAdminRequest(req, res, urlPath, deps) {
702
713
  send(res, 200, { key: publicKey(updated, now) });
703
714
  return true;
704
715
  }
716
+ // POST /admin/keys/<name>/effort { effort: "low" | ... | "max" | null }; null or absent clears.
717
+ if (isKeyEffortRoute) {
718
+ if (method !== 'POST') {
719
+ send(res, 405, { error: 'Method not allowed (use POST)' });
720
+ return true;
721
+ }
722
+ const body = await readJsonBody(req);
723
+ let effort = null;
724
+ if (body.effort !== undefined && body.effort !== null) {
725
+ if (!isKeyEffort(body.effort)) {
726
+ send(res, 400, { error: `invalid "effort": one of ${KEY_EFFORT_VALUES.join(', ')}` });
727
+ return true;
728
+ }
729
+ effort = body.effort;
730
+ }
731
+ const updated = store.mutate((file) => setKeyEffort(file, keyName, effort));
732
+ deps.audit?.({ action: 'key_effort', ok: updated !== null, status: updated ? 200 : 404, key: keyName, remote, detail: effort ?? 'cleared' });
733
+ if (!updated) {
734
+ send(res, 404, { error: `no key named "${keyName}"` });
735
+ return true;
736
+ }
737
+ send(res, 200, { key: publicKey(updated, now) });
738
+ return true;
739
+ }
705
740
  // DELETE /admin/keys/<name> — revoked, kept for the list.
706
741
  if (isKeyRevoke) {
707
742
  const revoked = store.mutate((file) => revokeKey(file, keyName));
@@ -18,6 +18,12 @@
18
18
  */
19
19
  import { EventEmitter } from 'node:events';
20
20
  import { type RequestTiming, type TimingStats } from './timing.js';
21
+ /**
22
+ * The billed thinking count inside a usage block, or undefined when the block
23
+ * carries none: Anthropic's `output_tokens_details.thinking_tokens`. The
24
+ * number is a share of `output_tokens`, never an addition to it.
25
+ */
26
+ export declare function thinkingTokensFromUsage(usage: unknown): number | undefined;
21
27
  export interface RequestRecord {
22
28
  timestamp: number;
23
29
  /**
@@ -341,7 +347,16 @@ export declare class Analytics extends EventEmitter {
341
347
  * empty list.
342
348
  */
343
349
  recent(n?: number): RequestRecord[];
344
- /** Parse usage from a non-streaming Anthropic response body. */
350
+ /**
351
+ * Parse usage from a non-streaming Anthropic response body.
352
+ *
353
+ * Thinking tokens come from `usage.output_tokens_details.thinking_tokens`
354
+ * when the response carries it. The older estimate, a quarter of the
355
+ * characters in the `thinking` blocks, stays as the fallback for responses
356
+ * without the field, but it reads ZERO on every current model, because
357
+ * they return thinking blocks with empty text unless the caller asked for a
358
+ * summary. The usage field counts what was billed.
359
+ */
345
360
  static parseUsage(body: Record<string, unknown>): {
346
361
  inputTokens: number;
347
362
  outputTokens: number;
package/dist/analytics.js CHANGED
@@ -19,6 +19,20 @@
19
19
  import { EventEmitter } from 'node:events';
20
20
  import { createHash } from 'node:crypto';
21
21
  import { timingStats } from './timing.js';
22
+ /**
23
+ * The billed thinking count inside a usage block, or undefined when the block
24
+ * carries none: Anthropic's `output_tokens_details.thinking_tokens`. The
25
+ * number is a share of `output_tokens`, never an addition to it.
26
+ */
27
+ export function thinkingTokensFromUsage(usage) {
28
+ if (!usage || typeof usage !== 'object')
29
+ return undefined;
30
+ const details = usage.output_tokens_details;
31
+ if (!details || typeof details !== 'object')
32
+ return undefined;
33
+ const n = details.thinking_tokens;
34
+ return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? Math.round(n) : undefined;
35
+ }
22
36
  export function continuationStats(records) {
23
37
  const out = { attempted: 0, finished: 0, unfinished: 0, failed: 0, noTarget: 0 };
24
38
  for (const r of records) {
@@ -440,14 +454,23 @@ export class Analytics extends EventEmitter {
440
454
  recent(n = 100) {
441
455
  return this.records.slice(-n);
442
456
  }
443
- /** Parse usage from a non-streaming Anthropic response body. */
457
+ /**
458
+ * Parse usage from a non-streaming Anthropic response body.
459
+ *
460
+ * Thinking tokens come from `usage.output_tokens_details.thinking_tokens`
461
+ * when the response carries it. The older estimate, a quarter of the
462
+ * characters in the `thinking` blocks, stays as the fallback for responses
463
+ * without the field, but it reads ZERO on every current model, because
464
+ * they return thinking blocks with empty text unless the caller asked for a
465
+ * summary. The usage field counts what was billed.
466
+ */
444
467
  static parseUsage(body) {
445
468
  const u = body.usage;
446
469
  const content = body.content;
447
470
  const thinkingChars = content
448
471
  ?.filter(b => b.type === 'thinking')
449
472
  .reduce((s, b) => s + (b.thinking?.length ?? 0), 0) ?? 0;
450
- const thinkingTokens = Math.round(thinkingChars / 4);
473
+ const thinkingTokens = thinkingTokensFromUsage(u) ?? Math.round(thinkingChars / 4);
451
474
  return {
452
475
  inputTokens: u?.input_tokens ?? 0,
453
476
  outputTokens: u?.output_tokens ?? 0,
@@ -387,6 +387,8 @@ export interface ResponsesResponse {
387
387
  } | null;
388
388
  usage?: ResponsesUsage | null;
389
389
  }
390
+ /** thinking → reasoning effort, at the same cut points as the chat path. */
391
+ export declare function thinkingToReasoningEffort(thinking: AnthropicThinkingConfig | undefined): 'low' | 'medium' | 'high' | undefined;
390
392
  /**
391
393
  * Extra max_output_tokens reserved for reasoning tokens, by effort, so the
392
394
  * client's intended visible-output budget survives on a reasoning model
@@ -111,7 +111,7 @@ export function anthropicUsageFromResponses(u) {
111
111
  // Thresholds are IMPORTED so the two translators stay in lock-step.
112
112
  // ─────────────────────────────────────────────────────────────────────
113
113
  /** thinking → reasoning effort, at the same cut points as the chat path. */
114
- function thinkingToReasoningEffort(thinking) {
114
+ export function thinkingToReasoningEffort(thinking) {
115
115
  if (!thinking || thinking.type !== 'enabled')
116
116
  return undefined;
117
117
  const budget = thinking.budget_tokens;
@@ -156,12 +156,13 @@ export declare const CC_AGENT_IDENTITY: string;
156
156
  * - undefined / 'verbatim' — CC's prompt unchanged (default; existing
157
157
  * setups don't regress).
158
158
  * - 'partial' — strip purely behavioral constraints, leaving every
159
- * refusal reminder and tool description intact. On the compact CC
160
- * prompt (2.1.x+) the lone behavioral constraint is the comment-
161
- * density / match-surrounding-style line, swapped for a positive
162
- * "be thorough" instruction; on older verbose prompts the
163
- * Tone-and-style + Text-output sections and the Doing-tasks bullets
164
- * are removed as well. Recovers the output capability the
159
+ * refusal reminder and tool description intact. The strip runs on
160
+ * the prompt CC sends for `model`: a one-line comment rule (compact
161
+ * prompts, the fable variant) is swapped for a positive "be thorough"
162
+ * instruction; a verbose prompt (the sonnet-5 variant) also loses its
163
+ * Tone-and-style and Text-output sections and the scope, verbosity
164
+ * and comment bullets in Doing tasks. A prompt with none of these is
165
+ * sent unchanged. Recovers the output capability the
165
166
  * constraint-removal research test measured.
166
167
  * - 'aggressive' — partial + remove the prompt-level RLHF reminder (the
167
168
  * IMPORTANT: line re-stating refusal categories) and the caution
@@ -177,7 +178,7 @@ export declare const CC_AGENT_IDENTITY: string;
177
178
  * Precedence framing inserted between CC's persona prompt and the client's
178
179
  * own system text in the merged block-3 system prompt.
179
180
  *
180
- * Why this exists (observed 2026-06-12, deepdive planner regression): a bare
181
+ * Why this exists (observed 2026-06-12, a downstream planner regression): a bare
181
182
  * `\n\n` append silently stopped working on claude-sonnet-4-6 — the model
182
183
  * followed the CC persona and treated the appended client instructions as
183
184
  * ignorable boilerplate, deterministically (0/6 on a trivial "reply with
@@ -330,7 +331,7 @@ export declare function detectTextToolClient(systemText: string): string | null;
330
331
  * own native tools (CC_NATIVE_NAMES), so it can never present a fully-foreign
331
332
  * surface; only a genuinely non-CC tool set can. This catches the small
332
333
  * in-house clients the 3-tool rule below
333
- * misses, e.g. forge inspection agents dispatched with just their capability
334
+ * misses, e.g. in-house inspection agents dispatched with just their capability
334
335
  * floor ([memory_store, db_query]). Before this, those 2-tool surfaces fell
335
336
  * under a len<3 guard and got round-robined onto CC fallback slots, which
336
337
  * silently corrupts every call (the model upstream never sees the real tool).
@@ -209,12 +209,13 @@ export const CC_AGENT_IDENTITY = TEMPLATE.agent_identity;
209
209
  * - undefined / 'verbatim' — CC's prompt unchanged (default; existing
210
210
  * setups don't regress).
211
211
  * - 'partial' — strip purely behavioral constraints, leaving every
212
- * refusal reminder and tool description intact. On the compact CC
213
- * prompt (2.1.x+) the lone behavioral constraint is the comment-
214
- * density / match-surrounding-style line, swapped for a positive
215
- * "be thorough" instruction; on older verbose prompts the
216
- * Tone-and-style + Text-output sections and the Doing-tasks bullets
217
- * are removed as well. Recovers the output capability the
212
+ * refusal reminder and tool description intact. The strip runs on
213
+ * the prompt CC sends for `model`: a one-line comment rule (compact
214
+ * prompts, the fable variant) is swapped for a positive "be thorough"
215
+ * instruction; a verbose prompt (the sonnet-5 variant) also loses its
216
+ * Tone-and-style and Text-output sections and the scope, verbosity
217
+ * and comment bullets in Doing tasks. A prompt with none of these is
218
+ * sent unchanged. Recovers the output capability the
218
219
  * constraint-removal research test measured.
219
220
  * - 'aggressive' — partial + remove the prompt-level RLHF reminder (the
220
221
  * IMPORTANT: line re-stating refusal categories) and the caution
@@ -230,7 +231,7 @@ export const CC_AGENT_IDENTITY = TEMPLATE.agent_identity;
230
231
  * Precedence framing inserted between CC's persona prompt and the client's
231
232
  * own system text in the merged block-3 system prompt.
232
233
  *
233
- * Why this exists (observed 2026-06-12, deepdive planner regression): a bare
234
+ * Why this exists (observed 2026-06-12, a downstream planner regression): a bare
234
235
  * `\n\n` append silently stopped working on claude-sonnet-4-6 — the model
235
236
  * followed the CC persona and treated the appended client instructions as
236
237
  * ignorable boilerplate, deterministically (0/6 on a trivial "reply with
@@ -281,35 +282,48 @@ export function resolveSystemPrompt(arg, model) {
281
282
  * Port of scripts/research/test-constraint-removal.mjs:stripConstraints. Pure over
282
283
  * its input; returns the input unchanged if no target matches (so a CC
283
284
  * bump that renames sections degrades to verbatim rather than producing
284
- * an unpredictable strip). Handles both the verbose pre-2.1 prompt
285
- * (`# Tone and style` etc.) and the compact 2.1.x+ prompt; the patterns
286
- * for the era not in play are simply no-ops.
285
+ * an unpredictable strip). CC sends a different prompt per model family
286
+ * and each words its constraints differently: the verbose shape (headed
287
+ * sections and Doing-tasks bullets, e.g. the sonnet-5 variant), the
288
+ * compact shape's comment-density line, and the fable variant's comment
289
+ * paragraph. Patterns for a shape not in play are no-ops, so a prompt
290
+ * carrying none of them (the 2.1.316 base and the opus-5 variant) comes
291
+ * back unchanged. test/system-prompt-modes.mjs lists which bundled prompt
292
+ * carries which constraint, so a rebake that rewords one fails there.
287
293
  */
288
294
  function stripBehavioralConstraints(input, level) {
289
295
  let s = input;
290
- // ── Legacy (pre-2.1 verbose prompt): no-ops on the compact prompt ──
291
- s = s.replace(/# Tone and style[\s\S]*?(?=\n# |\n$|$)/m, '');
292
- s = s.replace(/# Text output[^\n]*\n[\s\S]*?(?=\n# |\n$|$)/m, '');
296
+ // Verbose shape: whole style sections, then the Doing-tasks bullets. A
297
+ // section runs to the next top-level heading or the end of the prompt.
298
+ // The end test is (?![\s\S]), not $: under the m flag $ matches at every
299
+ // line end, which cut the match at the heading and left the body in.
300
+ s = s.replace(/^# Tone and style[\s\S]*?(?:\n(?=# )|(?![\s\S]))/m, '');
301
+ s = s.replace(/^# Text output[\s\S]*?(?:\n(?=# )|(?![\s\S]))/m, '');
302
+ // Each pattern takes one bullet plus any wrapped or nested lines under
303
+ // it, and stops at the next top-level bullet, heading or blank line. CC
304
+ // writes each bullet on one line and the next bullet is often a neutral
305
+ // one ("Prefer editing existing files"), so a fixed line count would
306
+ // delete it.
293
307
  const doingTasksConstraints = [
294
- /^ - Don't add features, refactor, or introduce abstractions[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n/m,
295
- /^ - Don't add error handling, fallbacks, or validation[^\n]*\n[^\n]*\n/m,
296
- /^ - Default to writing no comments\.[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n/m,
297
- /^ - Don't explain WHAT the code does[^\n]*\n[^\n]*\n/m,
298
- /^ - For exploratory questions[^\n]*\n[^\n]*\n/m,
299
- /^ - Avoid backwards-compatibility hacks[^\n]*\n[^\n]*\n/m,
308
+ /^ - Don't add features, refactor, or introduce abstractions[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
309
+ /^ - Don't add error handling, fallbacks, or validation[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
310
+ /^ - Default to writing no comments\.[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
311
+ /^ - Don't explain WHAT the code does[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
312
+ /^ - For exploratory questions[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
313
+ /^ - Avoid backwards-compatibility hacks[^\n]*\n(?:(?! - |#)[^\n]+\n)*/m,
300
314
  ];
301
315
  for (const re of doingTasksConstraints) {
302
316
  s = s.replace(re, '');
303
317
  }
304
318
  s = s.replace(/^# Doing tasks\n/m, '# Doing tasks\n\nBe thorough. Show your reasoning. Provide the context and explanations the user is likely to find useful. Use as many tokens as the task warrants.\n\n');
305
- // ── Compact prompt (2.1.x+): its one behavioral constraint is the
306
- // comment-density / match-surrounding-style line. Swap it for the same
307
- // positive instruction the legacy Doing-tasks rewrite inserts. ──
308
- s = s.replace(/^Write code that reads like the surrounding code:[^\n]*\n/m, 'Be thorough. Show your reasoning. Provide the context and explanations the user is likely to find useful. Use as many tokens as the task warrants.\n');
319
+ // Compact shape and the fable variant: the comment constraint is one
320
+ // unheaded line. Swap it for the same positive instruction the
321
+ // Doing-tasks rewrite inserts.
322
+ s = s.replace(/^(?:Write code that reads like the surrounding code:|Only write a code comment to state a constraint)[^\n]*\n/gm, 'Be thorough. Show your reasoning. Provide the context and explanations the user is likely to find useful. Use as many tokens as the task warrants.\n');
309
323
  if (level === 'aggressive') {
310
324
  s = s.replace(/^IMPORTANT: Assist with authorized security testing[^\n]*\n/m, '');
311
325
  s = s.replace(/^IMPORTANT: You must NEVER generate or guess URLs[^\n]*\n/m, '');
312
- s = s.replace(/# Executing actions with care[\s\S]*?(?=\n# |\n$|$)/m, '');
326
+ s = s.replace(/^# Executing actions with care[\s\S]*?(?:\n(?=# )|(?![\s\S]))/m, '');
313
327
  // Compact prompt: the caution guidance is a single unheaded paragraph.
314
328
  s = s.replace(/^For actions that are hard to reverse or outward-facing,[^\n]*\n/m, '');
315
329
  }
@@ -712,7 +726,7 @@ export function detectTextToolClient(systemText) {
712
726
  * own native tools (CC_NATIVE_NAMES), so it can never present a fully-foreign
713
727
  * surface; only a genuinely non-CC tool set can. This catches the small
714
728
  * in-house clients the 3-tool rule below
715
- * misses, e.g. forge inspection agents dispatched with just their capability
729
+ * misses, e.g. in-house inspection agents dispatched with just their capability
716
730
  * floor ([memory_store, db_query]). Before this, those 2-tool surfaces fell
717
731
  * under a len<3 guard and got round-robined onto CC fallback slots, which
718
732
  * silently corrupts every call (the model upstream never sees the real tool).
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 (claude-bridge
2029
- /status, CI scripts, etc.) instead of the
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 (claude-bridge /status,
2763
- // deepdive health checks, CI scripts) — no decorative header, no
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');
@@ -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
- * (claude-bridge's /status command, deepdive, CI scripts) consume
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
  */
@@ -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
- * (claude-bridge's /status command, deepdive, CI scripts) consume
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 deepdive planner outage: Anthropic's serving
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. */