@askalf/dario 4.8.66 → 4.8.68

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/dist/cli.js CHANGED
@@ -1118,6 +1118,16 @@ async function help() {
1118
1118
  (Sonnet only, Opus only when Anthropic ships
1119
1119
  them), overage. Mirrors the user-dashboard
1120
1120
  usage page. Costs ~1 subscription request.
1121
+ dario doctor --obedience
1122
+ Probe each model family THROUGH the running
1123
+ proxy with a client system prompt ("reply
1124
+ with ONLY the word PONG") and assert the
1125
+ reply obeys. Catches upstream behavioral
1126
+ drift in client-system steering — the
1127
+ 2026-06-12 sonnet class (dario#509) that
1128
+ 200s, billing, and template checks all
1129
+ miss. Needs \`dario proxy\` running; costs
1130
+ at most a few tiny subscription requests.
1121
1131
  dario doctor --json Emit the check report as structured JSON
1122
1132
  for machine consumption (claude-bridge
1123
1133
  /status, CI scripts, etc.) instead of the
@@ -1672,6 +1682,7 @@ async function doctor() {
1672
1682
  const { runChecks, formatChecks, formatChecksJson, exitCodeFor, runAuthCheck } = await import('./doctor.js');
1673
1683
  const probe = args.includes('--probe');
1674
1684
  const usage = args.includes('--usage');
1685
+ const obedience = args.includes('--obedience');
1675
1686
  const asJson = args.includes('--json');
1676
1687
  const authCheck = args.includes('--auth-check');
1677
1688
  const bunBoot = args.includes('--bun-bootstrap');
@@ -1755,7 +1766,7 @@ async function doctor() {
1755
1766
  console.log('');
1756
1767
  process.exit(result.verdict === 'match' ? 0 : 1);
1757
1768
  }
1758
- const checks = await runChecks({ probe, usage });
1769
+ const checks = await runChecks({ probe, usage, obedience });
1759
1770
  if (asJson) {
1760
1771
  // JSON mode is meant for machine consumption (claude-bridge /status,
1761
1772
  // deepdive health checks, CI scripts) — no decorative header, no
package/dist/doctor.d.ts CHANGED
@@ -74,6 +74,28 @@ export interface IdentityDriftInput {
74
74
  }
75
75
  export declare function checkIdentityDrift(input: IdentityDriftInput): Check[];
76
76
  export declare function probeNpmLatestCC(): string | null;
77
+ /**
78
+ * The client system prompt the `--obedience` probe sends. Deliberately
79
+ * trivial: any model that weighs client system text at all can comply,
80
+ * so a miss isolates "the client system prompt is being ignored" from
81
+ * "the instruction was too hard".
82
+ */
83
+ export declare const OBEDIENCE_SYSTEM_PROMPT = "Reply with ONLY the word PONG. No other words, no punctuation, no formatting.";
84
+ /**
85
+ * Join the text blocks of a `/v1/messages` response body. Thinking
86
+ * blocks are excluded — adaptive thinking may prepend them and they are
87
+ * not part of what the client-facing instruction governs. A refusal
88
+ * (empty content) or a malformed body yields ''.
89
+ */
90
+ export declare function extractMessageText(body: unknown): string;
91
+ /**
92
+ * Verdict for one obedience reply. Lenient on case and a single trailing
93
+ * `.`/`!` — the drift class this detects is "the model ignored the client
94
+ * system prompt entirely" (it answers as the CC persona instead), and a
95
+ * stray "Pong!" is obedient in substance. Strict equality would file
96
+ * 6-hourly drift issues over punctuation sampling.
97
+ */
98
+ export declare function isObedientReply(text: string): boolean;
77
99
  export interface RunChecksOptions {
78
100
  /**
79
101
  * Opt-in: hit Anthropic's authorize endpoint with the scope set dario
@@ -95,6 +117,17 @@ export interface RunChecksOptions {
95
117
  * request.
96
118
  */
97
119
  usage?: boolean;
120
+ /**
121
+ * Opt-in: probe each model family THROUGH the running proxy with a
122
+ * client system prompt ("reply with ONLY the word PONG") and assert
123
+ * the reply obeys. Catches upstream behavioral drift in client-system
124
+ * steering — the 2026-06-12 class (dario#509) where sonnet silently
125
+ * stopped following client system text while every other signal
126
+ * (200s, subscription billing, template labels, model smoke) stayed
127
+ * green. Enable with `dario doctor --obedience`; costs at most
128
+ * `families × 3` tiny subscription requests.
129
+ */
130
+ obedience?: boolean;
98
131
  }
99
132
  /**
100
133
  * Run every available health check. Never throws — each check is
package/dist/doctor.js CHANGED
@@ -148,6 +148,52 @@ export function probeNpmLatestCC() {
148
148
  _npmLatestCache = { value, at: Date.now() };
149
149
  return value;
150
150
  }
151
+ /**
152
+ * One representative model per family, shared by the `--usage` probe
153
+ * (Anthropic only returns a family's 7d bucket header on a request TO
154
+ * that family) and the `--obedience` probe (behavioral drift is
155
+ * per-family — the 2026-06-12 regression hit sonnet while haiku obeyed
156
+ * the identical merged body).
157
+ */
158
+ const PROBE_FAMILIES = [
159
+ { family: 'haiku', model: 'claude-haiku-4-5' },
160
+ { family: 'sonnet', model: 'claude-sonnet-4-6' },
161
+ { family: 'opus', model: 'claude-opus-4-8' },
162
+ { family: 'fable', model: 'claude-fable-5' },
163
+ ];
164
+ /**
165
+ * The client system prompt the `--obedience` probe sends. Deliberately
166
+ * trivial: any model that weighs client system text at all can comply,
167
+ * so a miss isolates "the client system prompt is being ignored" from
168
+ * "the instruction was too hard".
169
+ */
170
+ export const OBEDIENCE_SYSTEM_PROMPT = 'Reply with ONLY the word PONG. No other words, no punctuation, no formatting.';
171
+ /**
172
+ * Join the text blocks of a `/v1/messages` response body. Thinking
173
+ * blocks are excluded — adaptive thinking may prepend them and they are
174
+ * not part of what the client-facing instruction governs. A refusal
175
+ * (empty content) or a malformed body yields ''.
176
+ */
177
+ export function extractMessageText(body) {
178
+ const content = body?.content;
179
+ if (!Array.isArray(content))
180
+ return '';
181
+ return content
182
+ .filter((b) => b?.type === 'text' && typeof b.text === 'string')
183
+ .map((b) => b.text)
184
+ .join('')
185
+ .trim();
186
+ }
187
+ /**
188
+ * Verdict for one obedience reply. Lenient on case and a single trailing
189
+ * `.`/`!` — the drift class this detects is "the model ignored the client
190
+ * system prompt entirely" (it answers as the CC persona instead), and a
191
+ * stray "Pong!" is obedient in substance. Strict equality would file
192
+ * 6-hourly drift issues over punctuation sampling.
193
+ */
194
+ export function isObedientReply(text) {
195
+ return /^pong[.!]?$/i.test(text.trim());
196
+ }
151
197
  /**
152
198
  * Run every available health check. Never throws — each check is
153
199
  * individually try/caught so a broken subsystem (e.g. unreadable accounts
@@ -475,12 +521,6 @@ export async function runChecks(opts = {}) {
475
521
  }
476
522
  // Probe each family in parallel. Anthropic only returns the
477
523
  // per-model 7d bucket header on a request TO that family.
478
- const families = [
479
- { family: 'haiku', model: 'claude-haiku-4-5' },
480
- { family: 'sonnet', model: 'claude-sonnet-4-6' },
481
- { family: 'opus', model: 'claude-opus-4-8' },
482
- { family: 'fable', model: 'claude-fable-5' },
483
- ];
484
524
  const probe = async (model) => {
485
525
  const res = await fetch(probeEndpoint, {
486
526
  method: 'POST',
@@ -499,7 +539,7 @@ export async function runChecks(opts = {}) {
499
539
  return null;
500
540
  return parseRateLimits(res.headers);
501
541
  };
502
- const results = await Promise.all(families.map(f => probe(f.model).catch(() => null)));
542
+ const results = await Promise.all(PROBE_FAMILIES.map(f => probe(f.model).catch(() => null)));
503
543
  // Use the first non-null snapshot for the unified view — they
504
544
  // should all agree on the unified buckets (same account, same moment).
505
545
  const firstOk = results.find(s => s !== null);
@@ -554,6 +594,114 @@ export async function runChecks(opts = {}) {
554
594
  });
555
595
  }
556
596
  }
597
+ // ---- Client-system obedience probe (opt-in, --obedience).
598
+ // dario#509 / the 2026-06-12 deepdive planner outage: Anthropic's serving
599
+ // side changed how claude-sonnet-4-6 weighed client system text merged
600
+ // after the CC persona in block 3 — wire bytes identical, every existing
601
+ // check green — and the only symptom was a downstream consumer failing to
602
+ // get the JSON its system prompt demanded. This probe asserts the property
603
+ // those checks miss: a model, reached THROUGH the proxy's template merge,
604
+ // actually follows a client-supplied system instruction.
605
+ //
606
+ // Per family: up to 3 attempts (tolerates sampling), pass on the first
607
+ // obedient reply. Families probe in parallel, so worst-case wall time is
608
+ // bounded by attempts × timeout, not by family count.
609
+ //
610
+ // Requires a running proxy: a direct-to-Anthropic raw client shape would
611
+ // not exercise the cc-template merge seam (and Sonnet/Opus reject non-CC
612
+ // shapes on the subscription path anyway).
613
+ //
614
+ // Verdict semantics (consumed by scripts/check-doctor-drift.mjs):
615
+ // fail = the model ANSWERED but ignored the instruction. Behavioral and
616
+ // upstream-influenced — investigate the system-prompt
617
+ // presentation/merge (CLIENT_SYSTEM_PREFACE, block-3 structure),
618
+ // NOT necessarily a dario bug, and don't start with version
619
+ // bisects (the 2026-06-12 incident burned hours on those; they
620
+ // were all negative because nothing on dario's side changed).
621
+ // warn = the probe never got an answer (network/429/5xx) — infra
622
+ // flake, not drift.
623
+ if (opts.obedience) {
624
+ const dario_base = process.env.DARIO_TEST_URL || 'http://127.0.0.1:3456';
625
+ // Same auth fallback as the --usage probe: the proxy validates
626
+ // Authorization against DARIO_API_KEY when set; literal 'dario' is the
627
+ // documented loopback-only default for no-auth local proxies.
628
+ const dario_auth = process.env.DARIO_API_KEY || 'dario';
629
+ let proxyUp = false;
630
+ try {
631
+ const healthRes = await fetch(`${dario_base}/health`, { signal: AbortSignal.timeout(800) });
632
+ proxyUp = healthRes.ok;
633
+ }
634
+ catch { /* proxy not running */ }
635
+ if (!proxyUp) {
636
+ checks.push({
637
+ status: 'info',
638
+ label: 'Obedience',
639
+ detail: 'dario proxy not running — skipped. The probe must route through the proxy to exercise the client-system merge; start `dario proxy` and re-run.',
640
+ });
641
+ }
642
+ else {
643
+ const ATTEMPTS = 3;
644
+ const probeFamily = async ({ family, model }) => {
645
+ let lastReply = '';
646
+ let lastErr = null;
647
+ for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
648
+ try {
649
+ const res = await fetch(`${dario_base}/v1/messages`, {
650
+ method: 'POST',
651
+ headers: {
652
+ 'content-type': 'application/json',
653
+ 'anthropic-version': '2023-06-01',
654
+ 'authorization': `Bearer ${dario_auth}`,
655
+ },
656
+ body: JSON.stringify({
657
+ model,
658
+ max_tokens: 64,
659
+ system: OBEDIENCE_SYSTEM_PROMPT,
660
+ messages: [{ role: 'user', content: 'ping' }],
661
+ }),
662
+ signal: AbortSignal.timeout(30_000),
663
+ });
664
+ if (!res.ok) {
665
+ await res.text().catch(() => '');
666
+ lastErr = `HTTP ${res.status}`;
667
+ continue;
668
+ }
669
+ const body = await res.json().catch(() => null);
670
+ const text = extractMessageText(body);
671
+ if (isObedientReply(text)) {
672
+ return {
673
+ status: 'ok',
674
+ label: `Obedience (${family})`,
675
+ detail: `"${text}" (attempt ${attempt}/${ATTEMPTS})`,
676
+ };
677
+ }
678
+ // A completed-but-disobedient reply (including a refusal's
679
+ // empty content) is the drift signal — remember it so it
680
+ // dominates over any later transport error.
681
+ lastReply = text;
682
+ lastErr = null;
683
+ }
684
+ catch (err) {
685
+ lastErr = err.message;
686
+ }
687
+ }
688
+ if (lastErr !== null && lastReply === '') {
689
+ return {
690
+ status: 'warn',
691
+ label: `Obedience (${family})`,
692
+ detail: `probe could not complete (${lastErr}) — infra flake, not behavioral drift`,
693
+ };
694
+ }
695
+ return {
696
+ status: 'fail',
697
+ label: `Obedience (${family})`,
698
+ detail: `model answered but ignored the client system prompt (last reply: "${lastReply.slice(0, 80)}") — behavioral, upstream-influenced; investigate presentation/merge (CLIENT_SYSTEM_PREFACE seam), not necessarily a dario bug`,
699
+ };
700
+ };
701
+ const rows = await Promise.all(PROBE_FAMILIES.map((f) => probeFamily(f)));
702
+ checks.push(...rows);
703
+ }
704
+ }
557
705
  // ---- Account pool
558
706
  try {
559
707
  const { listAccountAliases, loadAllAccounts } = await import('./accounts.js');
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
282
282
  */
283
283
  export declare const SUPPORTED_CC_RANGE: {
284
284
  readonly min: "1.0.0";
285
- readonly maxTested: "2.1.175";
285
+ readonly maxTested: "2.1.176";
286
286
  };
287
287
  /**
288
288
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
@@ -786,7 +786,7 @@ export function _resetInstalledVersionProbeForTest() {
786
786
  */
787
787
  export const SUPPORTED_CC_RANGE = {
788
788
  min: '1.0.0',
789
- maxTested: '2.1.175',
789
+ maxTested: '2.1.176',
790
790
  };
791
791
  /**
792
792
  * Compare two dotted-numeric version strings. Returns negative if `a<b`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "4.8.66",
3
+ "version": "4.8.68",
4
4
  "description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
5
5
  "type": "module",
6
6
  "bin": {