@askalf/dario 3.15.0 → 3.16.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
@@ -124,7 +124,7 @@ OAuth-backed Claude Max / Pro, billed against your plan instead of the API. Acti
124
124
  **What it does:**
125
125
 
126
126
  - Every request is replaced with a Claude Code template before it goes upstream — 25 tool definitions, ~25KB system prompt, exact CC field order, exact beta headers, exact metadata structure. Only the conversation content is preserved. Anthropic's classifier sees what looks like a Claude Code session because, from the wire up, it *is* one — and that's what keeps your usage on subscription billing instead of Extra Usage.
127
- - **Live fingerprint extraction** (v3.11.0). Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template (system prompt, tools, user-agent, beta flags, and as of v3.13.0 the exact header insertion order). Eliminates the "Anthropic ships a new CC, dario is stale for 48 hours" window. Cached at `~/.dario/cc-template.live.json` with a 24h TTL. Falls back to the bundled snapshot if CC isn't installed.
127
+ - **Live fingerprint extraction** (v3.11.0). Dario spawns your installed `claude` binary against a loopback MITM endpoint on startup, captures its outbound request, and extracts the live template (system prompt, tools, user-agent, beta flags, and as of v3.13.0 the exact header insertion order — replayed on the wire by the shim since v3.13.0 and by the proxy since v3.16.0). Eliminates the "Anthropic ships a new CC, dario is stale for 48 hours" window. Cached at `~/.dario/cc-template.live.json` with a 24h TTL. Falls back to the bundled snapshot if CC isn't installed.
128
128
  - **Billing tag** reconstructed using CC's own algorithm: `x-anthropic-billing-header: cc_version=<version>.<build_tag>; cc_entrypoint=cli; cch=<5-char-hex>;` where `build_tag = SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)`.
129
129
  - **OAuth config auto-detection** from the installed CC binary. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release.
130
130
  - **Multi-account pool mode** — see below. Automatic when 2+ accounts are configured.
@@ -248,7 +248,7 @@ Under the hood: `dario shim` spawns the child with `NODE_OPTIONS=--require <dari
248
248
  - **Runtime detection** — `detectRuntime()` checks `globalThis.Bun` / `globalThis.Deno` / `process.versions.node` and logs a warning for non-Node runtimes. Canary for the day Anthropic ships a Bun-compiled CC.
249
249
  - **Template mtime-based auto-reload** — long-running child processes pick up mid-session fingerprint refreshes from dario's live capture without restart.
250
250
  - **Strict defensive `rewriteBody`** — the previous logic accepted `length >= 1` on the system array and invented `[1]`/`[2]` blocks out of thin air. Now requires exactly `length === 3` with all-text blocks; any mismatch passes through unchanged. Passthrough on an unknown shape is safer than blind replacement.
251
- - **`rewriteHeaders` honors captured header order** — the live fingerprint capture now records the exact order CC emits headers on the wire, and the shim replays that order on every outbound request. Header sequence alone is a fingerprint vector; v3.13.0 removes it.
251
+ - **`rewriteHeaders` honors captured header order** — the live fingerprint capture now records the exact order CC emits headers on the wire, and the shim replays that order on every outbound request. Header sequence alone is a fingerprint vector; v3.13.0 removes it from the shim, and v3.16.0 closes the same gap on the proxy via the shared `orderHeadersForOutbound` helper so both transports produce an identical wire shape.
252
252
  - **`checkVersionDrift`** — logs when the child's UA `cc_version` differs from the template's, so stale-cache windows during CC upgrades are visible in debug output.
253
253
 
254
254
  **When to use shim mode:**
@@ -410,11 +410,29 @@ Recognized prefixes:
410
410
 
411
411
  The prefix gets stripped before the request goes upstream — the backend only sees the bare model name. Unrecognized prefixes are ignored, so ollama-style `llama3:8b` passes through untouched. `dario proxy --model=openai:gpt-4o` applies the prefix to every request server-wide.
412
412
 
413
+ ### Agent compatibility
414
+
415
+ As of **v3.15.0**, dario's built-in `TOOL_MAP` has **71 entries** covering the tool schemas of every major coding agent. If you're running one of these, no flag is required on the Claude backend — tool calls translate to CC's native `Bash/Read/Write/Edit/Glob/Grep/WebSearch/WebFetch` on the outbound path (so the subscription fingerprint stays intact) and rebuild to your agent's exact expected shape on the inbound path (so your validator is happy).
416
+
417
+ | Agent | Covered tool names (subset) |
418
+ |---|---|
419
+ | Claude Code | default — CC's own tools |
420
+ | Cline / Roo Code | `execute_command`, `write_to_file`, `replace_in_file`, `apply_diff`, `list_files`, `search_files`, `read_file` |
421
+ | Cursor | `run_terminal_cmd`, `edit_file`, `search_replace`, `codebase_search`, `grep_search`, `file_search`, `list_dir`, `read_file` (`target_file`) |
422
+ | Windsurf | `run_command`, `view_file`, `write_to_file`, `replace_file_content`, `find_by_name`, `grep_search`, `list_dir`, `search_web`, `read_url_content` |
423
+ | Continue.dev | `builtin_run_terminal_command`, `builtin_read_file`, `builtin_create_new_file`, `builtin_edit_existing_file`, `builtin_file_glob_search`, `builtin_grep_search`, `builtin_ls` |
424
+ | GitHub Copilot | `run_in_terminal`, `insert_edit_into_file`, `semantic_search`, `codebase_search`, `list_dir`, `fetch_webpage` |
425
+ | OpenHands | `execute_bash`, `str_replace_editor` |
426
+ | OpenClaw | `exec`, `process`, `web_search`, `web_fetch`, `browser`, `message` |
427
+ | Hermes | `terminal`, `patch`, `web_extract`, `clarify` |
428
+
429
+ If your agent's tool names aren't in this list, you've got two escape hatches below: **`--preserve-tools`** (forward your schema verbatim, lose the CC fingerprint) or **`--hybrid-tools`** (keep the fingerprint, fill request-context fields from headers). Open an issue with your agent's tool schema and we'll add a pre-mapping entry.
430
+
413
431
  ### Custom tool schemas
414
432
 
415
- By default, on the Claude backend, dario replaces your client's tool definitions with the real Claude Code tools (`Bash`, `Read`, `Grep`, `Glob`, `WebSearch`, `WebFetch`) and translates parameters back and forth. That's how dario looks like CC on the wire, which is what lets your request bill against your Claude subscription instead of API pricing.
433
+ By default, on the Claude backend, dario replaces your client's tool definitions with the real Claude Code tools (`Bash`, `Read`, `Write`, `Edit`, `Grep`, `Glob`, `WebSearch`, `WebFetch`) and translates parameters back and forth. That's how dario looks like CC on the wire, which is what lets your request bill against your Claude subscription instead of API pricing. For the agents listed in [Agent compatibility](#agent-compatibility) above, the translation is pre-mapped and runs automatically — nothing to configure.
416
434
 
417
- The trade-off: if your client's tools carry fields CC's schema doesn't have — a `sessionId`, a custom request id, a channel-bound context token those fields don't survive the round trip. The model only ever sees `Bash({command})`, responds with `Bash({command})`, and dario's reverse map rebuilds your tool call without the fields the model never saw. Your validator then rejects the call for a missing required field.
435
+ The trade-off shows up when you're running something that *isn't* in the pre-mapped list and whose tools carry fields CC's schema doesn't have — a `sessionId`, a custom request id, a channel-bound context token, a `confidence` score the model is supposed to emit. Those fields don't survive the round trip. The model only ever sees `Bash({command})`, responds with `Bash({command})`, and dario's reverse map rebuilds your tool call without the fields the model never saw. Your validator then rejects the call for a missing required field.
418
436
 
419
437
  Symptom: your tool calls come back looking stripped-down, or your runtime complains about a required field being absent *only when routed through dario's Claude backend*, while the same tools work fine against a direct API key or the OpenAI-compat backend.
420
438
 
@@ -442,9 +460,10 @@ dario proxy --hybrid-tools
442
460
 
443
461
  | Your situation | Flag | Why |
444
462
  |---|---|---|
463
+ | Your agent is listed in [Agent compatibility](#agent-compatibility) | *(neither)* | Pre-mapped in `TOOL_MAP`; the default path already handles it. |
445
464
  | Your custom fields are request context (session/request/channel/user ids, timestamps) | `--hybrid-tools` | Keeps the CC fingerprint *and* your validator is satisfied. |
446
465
  | Your custom fields need the model's reasoning (e.g. `confidence`, `reasoning_trace`, `tool_selection_rationale`) | `--preserve-tools` | The model has to see the real schema to populate these. Accept the fingerprint loss. |
447
- | Your client's tools are already a subset of CC's `Bash/Read/Grep/Glob/WebSearch/WebFetch` | *(neither)* | Default mode works as-is. |
466
+ | Your client's tools are already a subset of CC's `Bash/Read/Write/Edit/Grep/Glob/WebSearch/WebFetch` | *(neither)* | Default mode works as-is. |
448
467
 
449
468
  Hybrid mode was built to resolve [#29](https://github.com/askalf/dario/issues/29) cleanly for OpenClaw-style agents whose `process` tool declares `sessionId`, after the full provider-comparison diagnostic from [@boeingchoco](https://github.com/boeingchoco) made clear that the problem wasn't fixable in the translation layer alone.
450
469
 
@@ -17,6 +17,34 @@ export declare const CC_TOOL_DEFINITIONS: {
17
17
  export declare const CC_SYSTEM_PROMPT: string;
18
18
  /** CC's agent identity string. */
19
19
  export declare const CC_AGENT_IDENTITY: string;
20
+ /**
21
+ * Apply the live template's captured header_order to an outbound header
22
+ * record. Returns a HeadersInit in one of two forms:
23
+ *
24
+ * - If the template has no header_order (bundled-only install, or capture
25
+ * didn't record rawHeaders), returns the input record unchanged.
26
+ * - If header_order is present, returns an array of [name, value] pairs
27
+ * in the captured order. `fetch()` serializes pairs to the wire in
28
+ * array order; a plain Record or Headers instance doesn't preserve
29
+ * order in the same way (Headers iteration is spec-sorted alphabetically,
30
+ * and while modern V8 iterates own-property keys in insertion order,
31
+ * nothing in the fetch contract guarantees that order reaches the HTTP
32
+ * layer untouched — the array form is the one variant where wire order
33
+ * is part of the spec).
34
+ *
35
+ * Caller-supplied headers that don't appear in the captured order are
36
+ * appended at the tail in their original insertion order so host-set
37
+ * headers (content-type, content-length) aren't silently dropped. Names
38
+ * in the captured order are emitted in the template's exact case; names
39
+ * only in the caller's map keep the caller's case.
40
+ *
41
+ * Matches `rewriteHeaders` in `src/shim/runtime.cjs` — the shim and the
42
+ * proxy are two transports that need to produce the same wire shape.
43
+ *
44
+ * @param headers outbound headers the proxy built
45
+ * @param overrideHeaderOrder test-only override; production callers pass nothing
46
+ */
47
+ export declare function orderHeadersForOutbound(headers: Record<string, string>, overrideHeaderOrder?: string[] | undefined): Record<string, string> | Array<[string, string]>;
20
48
  export declare function scrubFrameworkIdentifiers(text: string): string;
21
49
  /**
22
50
  * Client tool name → CC tool mapping with parameter translation.
@@ -16,6 +16,65 @@ export const CC_TOOL_DEFINITIONS = TEMPLATE.tools;
16
16
  export const CC_SYSTEM_PROMPT = TEMPLATE.system_prompt;
17
17
  /** CC's agent identity string. */
18
18
  export const CC_AGENT_IDENTITY = TEMPLATE.agent_identity;
19
+ /**
20
+ * Apply the live template's captured header_order to an outbound header
21
+ * record. Returns a HeadersInit in one of two forms:
22
+ *
23
+ * - If the template has no header_order (bundled-only install, or capture
24
+ * didn't record rawHeaders), returns the input record unchanged.
25
+ * - If header_order is present, returns an array of [name, value] pairs
26
+ * in the captured order. `fetch()` serializes pairs to the wire in
27
+ * array order; a plain Record or Headers instance doesn't preserve
28
+ * order in the same way (Headers iteration is spec-sorted alphabetically,
29
+ * and while modern V8 iterates own-property keys in insertion order,
30
+ * nothing in the fetch contract guarantees that order reaches the HTTP
31
+ * layer untouched — the array form is the one variant where wire order
32
+ * is part of the spec).
33
+ *
34
+ * Caller-supplied headers that don't appear in the captured order are
35
+ * appended at the tail in their original insertion order so host-set
36
+ * headers (content-type, content-length) aren't silently dropped. Names
37
+ * in the captured order are emitted in the template's exact case; names
38
+ * only in the caller's map keep the caller's case.
39
+ *
40
+ * Matches `rewriteHeaders` in `src/shim/runtime.cjs` — the shim and the
41
+ * proxy are two transports that need to produce the same wire shape.
42
+ *
43
+ * @param headers outbound headers the proxy built
44
+ * @param overrideHeaderOrder test-only override; production callers pass nothing
45
+ */
46
+ export function orderHeadersForOutbound(headers, overrideHeaderOrder) {
47
+ const order = overrideHeaderOrder !== undefined ? overrideHeaderOrder : TEMPLATE.header_order;
48
+ if (!Array.isArray(order) || order.length === 0) {
49
+ return headers;
50
+ }
51
+ const lowerToValue = new Map();
52
+ const lowerToOriginalKey = new Map();
53
+ for (const [k, v] of Object.entries(headers)) {
54
+ const lk = k.toLowerCase();
55
+ lowerToValue.set(lk, v);
56
+ lowerToOriginalKey.set(lk, k);
57
+ }
58
+ const ordered = [];
59
+ const seen = new Set();
60
+ for (const name of order) {
61
+ const key = name.toLowerCase();
62
+ if (seen.has(key))
63
+ continue;
64
+ const value = lowerToValue.get(key);
65
+ if (value !== undefined) {
66
+ ordered.push([name, value]);
67
+ seen.add(key);
68
+ }
69
+ }
70
+ for (const [k, v] of Object.entries(headers)) {
71
+ const lk = k.toLowerCase();
72
+ if (!seen.has(lk)) {
73
+ ordered.push([k, v]);
74
+ }
75
+ }
76
+ return ordered;
77
+ }
19
78
  // Framework identifiers that would flag non-CC usage. Stripped from the system
20
79
  // prompt and from message content text blocks before the request goes upstream.
21
80
  const FRAMEWORK_PATTERNS = [
package/dist/proxy.js CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { arch, platform } from 'node:process';
8
8
  import { getAccessToken, getStatus } from './oauth.js';
9
- import { buildCCRequest, reverseMapResponse, createStreamingReverseMapper } from './cc-template.js';
9
+ import { buildCCRequest, reverseMapResponse, createStreamingReverseMapper, orderHeadersForOutbound } from './cc-template.js';
10
10
  import { AccountPool, computeStickyKey, parseRateLimits } from './pool.js';
11
11
  import { Analytics, billingBucketFromClaim } from './analytics.js';
12
12
  import { loadAllAccounts, loadAccount, refreshAccountToken } from './accounts.js';
@@ -1000,9 +1000,14 @@ export async function startProxy(opts = {}) {
1000
1000
  // the next-best account before surfacing the error to the client.
1001
1001
  // Bounded to pool.size iterations; breaks immediately on any non-429.
1002
1002
  dispatchLoop: while (true) {
1003
+ // Reorder outbound headers to match CC's captured header sequence
1004
+ // when the live template recorded one. No-op on bundled-only installs.
1005
+ // Skipped in passthrough mode — passthrough means "don't shape the
1006
+ // request to look like CC," and reordering is a form of shaping.
1007
+ const outboundHeaders = passthrough ? headers : orderHeadersForOutbound(headers);
1003
1008
  upstream = await fetch(targetBase, {
1004
1009
  method: req.method ?? 'POST',
1005
- headers,
1010
+ headers: outboundHeaders,
1006
1011
  body: finalBody ? new Uint8Array(finalBody) : undefined,
1007
1012
  signal: upstreamAbort.signal,
1008
1013
  });
@@ -1043,7 +1048,7 @@ export async function startProxy(opts = {}) {
1043
1048
  const retryHeaders = { ...headers, 'anthropic-beta': reducedBeta };
1044
1049
  const retry = await fetch(targetBase, {
1045
1050
  method: req.method ?? 'POST',
1046
- headers: retryHeaders,
1051
+ headers: passthrough ? retryHeaders : orderHeadersForOutbound(retryHeaders),
1047
1052
  body: finalBody ? new Uint8Array(finalBody) : undefined,
1048
1053
  signal: upstreamAbort.signal,
1049
1054
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.15.0",
3
+ "version": "3.16.0",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",