@askalf/dario 3.19.1 → 3.19.3
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 +8 -3
- package/dist/cc-template.d.ts +38 -0
- package/dist/cc-template.js +86 -16
- package/dist/cli.js +11 -2
- package/dist/live-fingerprint.js +8 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +125 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
<a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario" alt="Downloads"></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
+
<p align="center">
|
|
15
|
+
<sub><strong>v4 is not a version bump.</strong> The router was the prerequisite. What comes next uses it as a substrate. — <a href="https://github.com/askalf/dario/discussions/categories/announcements">watch this space</a></sub>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
14
18
|
```bash
|
|
15
19
|
npm install -g @askalf/dario && dario proxy
|
|
16
20
|
```
|
|
@@ -312,12 +316,13 @@ The OpenAI-compat backend forwards tool definitions byte-for-byte and doesn't ne
|
|
|
312
316
|
| Flag / env | Description | Default |
|
|
313
317
|
|---|---|---|
|
|
314
318
|
| `--passthrough` / `--thin` | Thin proxy for the Claude backend — OAuth swap only, no template injection | off |
|
|
315
|
-
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). | off |
|
|
316
|
-
| `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). | off |
|
|
319
|
+
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC's. Required for clients whose tools have fields CC doesn't — see [Custom tool schemas](#custom-tool-schemas). Auto-enabled for Cline / Kilo Code / Roo Code and forks (dario#40 — detected via system-prompt fingerprint). | off (auto for text-tool clients) |
|
|
320
|
+
| `--hybrid-tools` / `--context-inject` | Remap to CC tools **and** inject request-context values (`sessionId`, `requestId`, `channelId`, `userId`, `timestamp`) into client-declared fields CC's schema doesn't carry. See [Hybrid tool mode](#hybrid-tool-mode). Overrides the text-tool auto-detect. | off |
|
|
317
321
|
| `--model=<name>` | Force a model. Shortcuts (`opus`, `sonnet`, `haiku`), full IDs (`claude-opus-4-7`), or a **provider prefix** (`openai:gpt-4o`, `groq:llama-3.3-70b`, `claude:opus`, `local:qwen-coder`) to force the backend server-wide. See [Provider prefix](#provider-prefix). | passthrough |
|
|
318
322
|
| `--port=<n>` | Port to listen on | `3456` |
|
|
319
323
|
| `--host=<addr>` / `DARIO_HOST` | Bind address. Use `0.0.0.0` for LAN, or a specific IP (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
|
|
320
|
-
| `--verbose` / `-v` | Log every request | off |
|
|
324
|
+
| `--verbose` / `-v` | Log every request (one line per request — method + path + billing bucket) | off |
|
|
325
|
+
| `--verbose=2` / `-vv` / `DARIO_LOG_BODIES=1` | Also dump the outbound request body (redacted: bearer tokens, `sk-ant-*` keys, JWTs stripped; capped at 8KB). For wire-level client-compat debugging (dario#40). | off |
|
|
321
326
|
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require a matching `x-api-key` or `Authorization: Bearer` header. Required when `--host` binds non-loopback. | unset (open) |
|
|
322
327
|
| `DARIO_CORS_ORIGIN` | Override browser CORS origin | `http://localhost:${port}` |
|
|
323
328
|
| `DARIO_NO_BUN` | Disable automatic Bun relaunch | unset |
|
package/dist/cc-template.d.ts
CHANGED
|
@@ -49,6 +49,43 @@ export declare const CC_AGENT_IDENTITY: string;
|
|
|
49
49
|
*/
|
|
50
50
|
export declare function orderHeadersForOutbound(headers: Record<string, string>, overrideHeaderOrder?: string[] | undefined): Record<string, string> | Array<[string, string]>;
|
|
51
51
|
export declare function scrubFrameworkIdentifiers(text: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Detect text-tool-protocol clients (Cline, Kilo Code, Roo Code and
|
|
54
|
+
* their forks) by fingerprinting the incoming system prompt.
|
|
55
|
+
*
|
|
56
|
+
* These clients ship their own XML-style tool invocation protocol in
|
|
57
|
+
* the system prompt (`<execute_command>`, `<replace_in_file>`,
|
|
58
|
+
* `<attempt_completion>`, …) and parse the model's output with a
|
|
59
|
+
* regex tuned to that exact shape. When dario's default mode
|
|
60
|
+
* substitutes CC's canonical tools into the `tools` array, the model
|
|
61
|
+
* correctly emits Anthropic's generic `<function_calls><invoke>`
|
|
62
|
+
* wrapper — which is well-formed for a CC-tool request but
|
|
63
|
+
* unparseable for a text-protocol client, so every edit surfaces as
|
|
64
|
+
* an error in the client UI even though the model produced a valid
|
|
65
|
+
* response (dario#40, reported by @ringge).
|
|
66
|
+
*
|
|
67
|
+
* The fix is preserve-tools behavior: skip the CC tool swap so the
|
|
68
|
+
* model sees the client's own schema and emits its native XML shape.
|
|
69
|
+
* Auto-detection saves users from having to discover the
|
|
70
|
+
* `--preserve-tools` flag exists; the flag is still honored as an
|
|
71
|
+
* explicit override and `--hybrid-tools` outranks detection.
|
|
72
|
+
*
|
|
73
|
+
* Detection must run BEFORE `scrubFrameworkIdentifiers` so brand
|
|
74
|
+
* names like "Cline" / "Roo" are still present. Tool-protocol
|
|
75
|
+
* markers are scrub-proof on their own.
|
|
76
|
+
*
|
|
77
|
+
* Returns the matched family (`cline` / `kilo` / `roo` / `cline-like`)
|
|
78
|
+
* or null when no text-tool protocol signature is present.
|
|
79
|
+
*/
|
|
80
|
+
export declare function detectTextToolClient(systemText: string): string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Flatten an Anthropic-shaped `system` field (string or array of text
|
|
83
|
+
* blocks) to a single joined string. Skips the billing-tag block so
|
|
84
|
+
* captured billing metadata isn't conflated with the operator's own
|
|
85
|
+
* prompt. Used both by the main request-build path (post-scrub) and
|
|
86
|
+
* by the early text-tool-client detector (pre-scrub).
|
|
87
|
+
*/
|
|
88
|
+
export declare function extractSystemText(clientBody: Record<string, unknown>): string;
|
|
52
89
|
/**
|
|
53
90
|
* Client tool name → CC tool mapping with parameter translation.
|
|
54
91
|
*
|
|
@@ -117,6 +154,7 @@ export declare function buildCCRequest(clientBody: Record<string, unknown>, bill
|
|
|
117
154
|
body: Record<string, unknown>;
|
|
118
155
|
toolMap: Map<string, ToolMapping>;
|
|
119
156
|
unmappedTools: string[];
|
|
157
|
+
detectedClient?: string;
|
|
120
158
|
};
|
|
121
159
|
/**
|
|
122
160
|
* Reverse-map CC tool calls in a non-streaming response back to the
|
package/dist/cc-template.js
CHANGED
|
@@ -116,6 +116,73 @@ export function scrubFrameworkIdentifiers(text) {
|
|
|
116
116
|
}
|
|
117
117
|
return result;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Detect text-tool-protocol clients (Cline, Kilo Code, Roo Code and
|
|
121
|
+
* their forks) by fingerprinting the incoming system prompt.
|
|
122
|
+
*
|
|
123
|
+
* These clients ship their own XML-style tool invocation protocol in
|
|
124
|
+
* the system prompt (`<execute_command>`, `<replace_in_file>`,
|
|
125
|
+
* `<attempt_completion>`, …) and parse the model's output with a
|
|
126
|
+
* regex tuned to that exact shape. When dario's default mode
|
|
127
|
+
* substitutes CC's canonical tools into the `tools` array, the model
|
|
128
|
+
* correctly emits Anthropic's generic `<function_calls><invoke>`
|
|
129
|
+
* wrapper — which is well-formed for a CC-tool request but
|
|
130
|
+
* unparseable for a text-protocol client, so every edit surfaces as
|
|
131
|
+
* an error in the client UI even though the model produced a valid
|
|
132
|
+
* response (dario#40, reported by @ringge).
|
|
133
|
+
*
|
|
134
|
+
* The fix is preserve-tools behavior: skip the CC tool swap so the
|
|
135
|
+
* model sees the client's own schema and emits its native XML shape.
|
|
136
|
+
* Auto-detection saves users from having to discover the
|
|
137
|
+
* `--preserve-tools` flag exists; the flag is still honored as an
|
|
138
|
+
* explicit override and `--hybrid-tools` outranks detection.
|
|
139
|
+
*
|
|
140
|
+
* Detection must run BEFORE `scrubFrameworkIdentifiers` so brand
|
|
141
|
+
* names like "Cline" / "Roo" are still present. Tool-protocol
|
|
142
|
+
* markers are scrub-proof on their own.
|
|
143
|
+
*
|
|
144
|
+
* Returns the matched family (`cline` / `kilo` / `roo` / `cline-like`)
|
|
145
|
+
* or null when no text-tool protocol signature is present.
|
|
146
|
+
*/
|
|
147
|
+
export function detectTextToolClient(systemText) {
|
|
148
|
+
if (!systemText)
|
|
149
|
+
return null;
|
|
150
|
+
if (/\bYou are Cline\b/.test(systemText))
|
|
151
|
+
return 'cline';
|
|
152
|
+
if (/\bYou are Kilo Code\b/.test(systemText))
|
|
153
|
+
return 'kilo';
|
|
154
|
+
if (/\bYou are Roo\b/.test(systemText))
|
|
155
|
+
return 'roo';
|
|
156
|
+
// Protocol-signature fallback — unique to the Cline family and its
|
|
157
|
+
// forks; survives a forked system prompt that edited the identity
|
|
158
|
+
// string out but kept the tool protocol intact.
|
|
159
|
+
if (/<attempt_completion>/.test(systemText))
|
|
160
|
+
return 'cline-like';
|
|
161
|
+
if (/<ask_followup_question>/.test(systemText))
|
|
162
|
+
return 'cline-like';
|
|
163
|
+
if (/<<<<<<< SEARCH\b/.test(systemText))
|
|
164
|
+
return 'cline-like';
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Flatten an Anthropic-shaped `system` field (string or array of text
|
|
169
|
+
* blocks) to a single joined string. Skips the billing-tag block so
|
|
170
|
+
* captured billing metadata isn't conflated with the operator's own
|
|
171
|
+
* prompt. Used both by the main request-build path (post-scrub) and
|
|
172
|
+
* by the early text-tool-client detector (pre-scrub).
|
|
173
|
+
*/
|
|
174
|
+
export function extractSystemText(clientBody) {
|
|
175
|
+
const sys = clientBody.system;
|
|
176
|
+
if (typeof sys === 'string')
|
|
177
|
+
return sys;
|
|
178
|
+
if (Array.isArray(sys)) {
|
|
179
|
+
return sys
|
|
180
|
+
.filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
|
|
181
|
+
.map(b => b.text)
|
|
182
|
+
.join('\n\n');
|
|
183
|
+
}
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
119
186
|
/**
|
|
120
187
|
* Map from client-declared field name (lowercase) to the RequestContext
|
|
121
188
|
* key that supplies its value. A field declared on the client's tool
|
|
@@ -580,6 +647,16 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
580
647
|
const messages = clientBody.messages || [];
|
|
581
648
|
const clientTools = clientBody.tools;
|
|
582
649
|
const stream = clientBody.stream ?? false;
|
|
650
|
+
// ── Detect text-tool-protocol clients up-front ──
|
|
651
|
+
// Cline / Kilo Code / Roo Code (and forks) ship an XML tool-invocation
|
|
652
|
+
// protocol in the system prompt. Peek at it before scrubbing so the
|
|
653
|
+
// brand name is still present, decide whether to auto-switch into
|
|
654
|
+
// preserve-tools behavior below. Explicit --hybrid-tools outranks the
|
|
655
|
+
// heuristic (operator opt-in wins). dario#40.
|
|
656
|
+
const rawSystemForDetection = extractSystemText(clientBody);
|
|
657
|
+
const detectedClient = detectTextToolClient(rawSystemForDetection) ?? undefined;
|
|
658
|
+
const autoPreserve = Boolean(detectedClient) && !opts.hybridTools;
|
|
659
|
+
const effectivePreserveTools = Boolean(opts.preserveTools) || autoPreserve;
|
|
583
660
|
// ── Strip thinking from history ──
|
|
584
661
|
for (const msg of messages) {
|
|
585
662
|
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
@@ -622,7 +699,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
622
699
|
// the fingerprint risk on their own account.
|
|
623
700
|
const activeToolMap = new Map();
|
|
624
701
|
const unmappedTools = [];
|
|
625
|
-
if (clientTools && !
|
|
702
|
+
if (clientTools && !effectivePreserveTools) {
|
|
626
703
|
// Two passes so the unmapped-tool distributor can avoid colliding with
|
|
627
704
|
// CC tools the client already uses directly. Without this, a client
|
|
628
705
|
// sending both `WebSearch` and some unmapped tool like `memory_get`
|
|
@@ -705,7 +782,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
705
782
|
}
|
|
706
783
|
// ── Remap tool_use and tool_result references in message history ──
|
|
707
784
|
// Skip in preserveTools mode — leave conversation history untouched.
|
|
708
|
-
if (!
|
|
785
|
+
if (!effectivePreserveTools) {
|
|
709
786
|
for (const msg of messages) {
|
|
710
787
|
if (Array.isArray(msg.content)) {
|
|
711
788
|
for (const block of msg.content) {
|
|
@@ -754,18 +831,11 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
754
831
|
}
|
|
755
832
|
}
|
|
756
833
|
// ── Merge system prompt ──
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
else if (Array.isArray(sys)) {
|
|
763
|
-
systemText = sys
|
|
764
|
-
.filter(b => b.text && !b.text.includes('x-anthropic-billing-header:'))
|
|
765
|
-
.map(b => b.text)
|
|
766
|
-
.join('\n\n');
|
|
767
|
-
}
|
|
768
|
-
systemText = scrubFrameworkIdentifiers(systemText);
|
|
834
|
+
// rawSystemForDetection holds the same text already used by the
|
|
835
|
+
// up-front detector above — reuse it here so we don't reparse the
|
|
836
|
+
// system array a second time per request. Scrub applies at this
|
|
837
|
+
// point so framework identifiers don't leak upstream.
|
|
838
|
+
let systemText = scrubFrameworkIdentifiers(rawSystemForDetection);
|
|
769
839
|
// Also scrub framework identifiers from message content text blocks.
|
|
770
840
|
// Clients often inject their product name into user/tool messages as well,
|
|
771
841
|
// and the system-prompt-only scrub used to miss those.
|
|
@@ -815,7 +885,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
815
885
|
// preserveTools mode: pass client tools through unchanged (better for real
|
|
816
886
|
// agents with custom schemas, but loses the CC tool fingerprint).
|
|
817
887
|
if (clientTools && clientTools.length > 0) {
|
|
818
|
-
ccRequest.tools =
|
|
888
|
+
ccRequest.tools = effectivePreserveTools ? clientTools : CC_TOOL_DEFINITIONS;
|
|
819
889
|
}
|
|
820
890
|
// Metadata
|
|
821
891
|
ccRequest.metadata = {
|
|
@@ -833,7 +903,7 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
833
903
|
ccRequest.output_config = { effort: 'medium' };
|
|
834
904
|
}
|
|
835
905
|
ccRequest.stream = stream;
|
|
836
|
-
return { body: ccRequest, toolMap: activeToolMap, unmappedTools };
|
|
906
|
+
return { body: ccRequest, toolMap: activeToolMap, unmappedTools, detectedClient };
|
|
837
907
|
}
|
|
838
908
|
/**
|
|
839
909
|
* Build the CC-name → {clientName, mapping} reverse lookup used by both
|
package/dist/cli.js
CHANGED
|
@@ -137,7 +137,14 @@ async function proxy() {
|
|
|
137
137
|
console.error('[dario] Invalid --host. Must be an IP address or hostname.');
|
|
138
138
|
process.exit(1);
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
// --verbose=2 / -vv / DARIO_LOG_BODIES=1 → emit redacted request bodies
|
|
141
|
+
// on every POST. -v alone is unchanged (one-line per-request summary).
|
|
142
|
+
// dario#40 (ringge asked for a body-dump mode when debugging client
|
|
143
|
+
// compatibility without having to attach a MITM).
|
|
144
|
+
const verboseBodies = args.includes('-vv')
|
|
145
|
+
|| args.includes('--verbose=2')
|
|
146
|
+
|| process.env.DARIO_LOG_BODIES === '1';
|
|
147
|
+
const verbose = verboseBodies || args.includes('--verbose') || args.includes('-v');
|
|
141
148
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
142
149
|
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
143
150
|
const hybridTools = args.includes('--hybrid-tools') || args.includes('--context-inject');
|
|
@@ -147,7 +154,7 @@ async function proxy() {
|
|
|
147
154
|
}
|
|
148
155
|
const modelArg = args.find(a => a.startsWith('--model='));
|
|
149
156
|
const model = modelArg ? modelArg.split('=')[1] : undefined;
|
|
150
|
-
await startProxy({ port, host, verbose, model, passthrough, preserveTools, hybridTools });
|
|
157
|
+
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools });
|
|
151
158
|
}
|
|
152
159
|
async function accounts() {
|
|
153
160
|
const sub = args[1];
|
|
@@ -387,6 +394,8 @@ async function help() {
|
|
|
387
394
|
--host=ADDRESS Address to bind to (default: 127.0.0.1)
|
|
388
395
|
Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
|
|
389
396
|
--verbose, -v Log all requests
|
|
397
|
+
--verbose=2, -vv Also dump redacted request bodies
|
|
398
|
+
(env: DARIO_LOG_BODIES=1)
|
|
390
399
|
|
|
391
400
|
Quick start:
|
|
392
401
|
dario login # auto-detects Claude Code credentials
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -513,6 +513,14 @@ export function extractTemplate(captured) {
|
|
|
513
513
|
const STATIC_HEADER_EXCLUDE = new Set([
|
|
514
514
|
// Auth — never replay across identities
|
|
515
515
|
'authorization',
|
|
516
|
+
// x-api-key is a CAPTURE ARTIFACT (dario#42). During capture we spawn CC
|
|
517
|
+
// with ANTHROPIC_API_KEY=sk-dario-fingerprint-capture pointing at a loopback
|
|
518
|
+
// MITM, so CC emits `x-api-key: sk-dario-fingerprint-capture`. Replaying
|
|
519
|
+
// that placeholder upstream alongside the real OAuth Bearer used to be a
|
|
520
|
+
// no-op because Anthropic ignored x-api-key when Authorization was present;
|
|
521
|
+
// as of 2026-04-17 some account tiers now 401 with "invalid x-api-key" when
|
|
522
|
+
// both are sent. Never capture it.
|
|
523
|
+
'x-api-key',
|
|
516
524
|
// Body-framing — computed per request
|
|
517
525
|
'content-type', 'content-length', 'transfer-encoding',
|
|
518
526
|
// Host / connection — managed by the HTTP stack
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -363,6 +363,17 @@ export async function startProxy(opts = {}) {
|
|
|
363
363
|
const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
|
|
364
364
|
const verbose = opts.verbose ?? false;
|
|
365
365
|
const passthrough = opts.passthrough ?? false;
|
|
366
|
+
// Text-tool-protocol client families that have already logged a
|
|
367
|
+
// "detected → auto-enabling preserve-tools" banner this session.
|
|
368
|
+
// Set once on first sighting per family so the startup log stays
|
|
369
|
+
// short even under heavy traffic. dario#40.
|
|
370
|
+
const detectedClientsLogged = new Set();
|
|
371
|
+
// Body-dump mode: set via --verbose=2 / -vv or DARIO_LOG_BODIES=1.
|
|
372
|
+
// When on, every request emits a redacted JSON body to stderr so
|
|
373
|
+
// operators can see exactly what dario forwards upstream. Default
|
|
374
|
+
// -v stays quiet because bodies can carry file content and tool
|
|
375
|
+
// output. Reported in dario#40 by @ringge.
|
|
376
|
+
const verboseBodies = Boolean(opts.verboseBodies) || process.env.DARIO_LOG_BODIES === '1';
|
|
366
377
|
// Multi-provider backends (v3.6.0+). Loaded once at startup; the CLI
|
|
367
378
|
// `dario backend add openai --key=…` writes to ~/.dario/backends/.
|
|
368
379
|
// Routing: a GPT-family model arriving on /v1/chat/completions is
|
|
@@ -493,8 +504,17 @@ export async function startProxy(opts = {}) {
|
|
|
493
504
|
// Excludes auth + body-framing + session-scoped keys by construction (see
|
|
494
505
|
// extractStaticHeaderValues in live-fingerprint.ts). No-op when the loaded
|
|
495
506
|
// template predates v2 or the bundled snapshot is in use.
|
|
507
|
+
//
|
|
508
|
+
// `x-api-key` is filtered defensively here too — pre-v3.19.2 captures still
|
|
509
|
+
// carry `x-api-key: sk-dario-fingerprint-capture` from the MITM spawn env.
|
|
510
|
+
// Replaying that placeholder alongside a real OAuth Bearer triggers a
|
|
511
|
+
// "invalid x-api-key" 401 on some account tiers as of 2026-04-17 (dario#42).
|
|
512
|
+
// The capture filter was updated in v3.19.2 to stop storing it, but the
|
|
513
|
+
// per-request skip below lets existing caches self-heal without a refresh.
|
|
496
514
|
if (!passthrough && CC_TEMPLATE.header_values) {
|
|
497
515
|
for (const [k, v] of Object.entries(CC_TEMPLATE.header_values)) {
|
|
516
|
+
if (k.toLowerCase() === 'x-api-key')
|
|
517
|
+
continue;
|
|
498
518
|
staticHeaders[k] = v;
|
|
499
519
|
}
|
|
500
520
|
}
|
|
@@ -507,6 +527,14 @@ export async function startProxy(opts = {}) {
|
|
|
507
527
|
// is the single-account slot. Reported by @boeingchoco in dario#36 — the
|
|
508
528
|
// retry loop was firing on every POST with hybrid-tools + OC.
|
|
509
529
|
const context1mUnavailable = new Set();
|
|
530
|
+
// Per-account cache of anthropic-beta flags the upstream has rejected as
|
|
531
|
+
// "Unexpected value(s)". The live-captured template lifts whatever CC emits
|
|
532
|
+
// verbatim — including flags gated to higher-tier accounts (e.g.
|
|
533
|
+
// `afk-mode-2026-01-31` is rejected on Max 5x as of 2026-04-17). On the
|
|
534
|
+
// first rejection we parse the flag out of the error message, strip it,
|
|
535
|
+
// retry once, and cache it so subsequent requests on the same account don't
|
|
536
|
+
// re-pay the 400 round-trip. Keyed by account alias (pool) or `__default__`.
|
|
537
|
+
const unavailableBetas = new Map();
|
|
510
538
|
const ACCOUNT_KEY_SINGLE = '__default__';
|
|
511
539
|
// Beta flag set — sourced from the live template when the capture recorded
|
|
512
540
|
// one (schema v2+), else falls back to the v2.1.104 bundled default. Same
|
|
@@ -514,7 +542,18 @@ export async function startProxy(opts = {}) {
|
|
|
514
542
|
// never diverge on the wire). Computed once per proxy because it's a
|
|
515
543
|
// function of the loaded template, not of the request.
|
|
516
544
|
const BETA_FALLBACK = 'claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24';
|
|
517
|
-
|
|
545
|
+
let betaBase = CC_TEMPLATE.anthropic_beta || BETA_FALLBACK;
|
|
546
|
+
// `oauth-2025-04-20` is CC's OAuth-enablement beta flag. It is NOT present in
|
|
547
|
+
// the live-captured beta set because dario's fingerprint capture spawns CC
|
|
548
|
+
// with a placeholder `ANTHROPIC_API_KEY`, and CC only appends the oauth beta
|
|
549
|
+
// when it's actually using an OAuth bearer token. The proxy always uses
|
|
550
|
+
// OAuth upstream, so the flag is required — force it in if the captured
|
|
551
|
+
// template didn't carry it. As of 2026-04-17 some account tiers (Max 20x,
|
|
552
|
+
// Pro) return `authentication_error: invalid x-api-key` without this flag
|
|
553
|
+
// even when a valid Bearer is sent (dario#42).
|
|
554
|
+
if (!passthrough && !betaBase.split(',').includes('oauth-2025-04-20')) {
|
|
555
|
+
betaBase = betaBase ? `${betaBase},oauth-2025-04-20` : 'oauth-2025-04-20';
|
|
556
|
+
}
|
|
518
557
|
const betaWithoutContext1m = betaBase.split(',').filter((t) => t !== 'context-1m-2025-08-07').join(',');
|
|
519
558
|
// Rate governor — minimum 500ms between requests. Fast enough for agents,
|
|
520
559
|
// slow enough to not look like a scripted flood of identical traffic.
|
|
@@ -940,10 +979,22 @@ export async function startProxy(opts = {}) {
|
|
|
940
979
|
const bodyIdentity = poolAccount
|
|
941
980
|
? poolAccount.identity
|
|
942
981
|
: { deviceId: identity.deviceId, accountUuid: identity.accountUuid, sessionId: SESSION_ID };
|
|
943
|
-
const { body: ccBody, toolMap } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
|
|
982
|
+
const { body: ccBody, toolMap, detectedClient } = buildCCRequest(r, billingTag, CACHE_1H, bodyIdentity, {
|
|
944
983
|
preserveTools: opts.preserveTools ?? false,
|
|
945
984
|
hybridTools: opts.hybridTools ?? false,
|
|
946
985
|
});
|
|
986
|
+
// Log the auto-preserve-tools switch once per text-tool
|
|
987
|
+
// client family. Skip when the operator already opted into
|
|
988
|
+
// --preserve-tools or --hybrid-tools — they know what they
|
|
989
|
+
// picked and don't need a "hey, we heuristically agree"
|
|
990
|
+
// line on every new client seen. dario#40.
|
|
991
|
+
if (detectedClient
|
|
992
|
+
&& !opts.preserveTools
|
|
993
|
+
&& !opts.hybridTools
|
|
994
|
+
&& !detectedClientsLogged.has(detectedClient)) {
|
|
995
|
+
detectedClientsLogged.add(detectedClient);
|
|
996
|
+
console.log(`[dario] detected ${detectedClient}-style text-tool protocol — auto-enabling preserve-tools for this client (pass --hybrid-tools to override, --preserve-tools to silence)`);
|
|
997
|
+
}
|
|
947
998
|
// Store tool map for response reverse-mapping
|
|
948
999
|
ccToolMap = toolMap;
|
|
949
1000
|
// Replace request body entirely with CC template
|
|
@@ -959,6 +1010,20 @@ export async function startProxy(opts = {}) {
|
|
|
959
1010
|
const modelInfo = modelOverride ? ` (model: ${modelOverride})` : '';
|
|
960
1011
|
console.log(`[dario] #${requestCount} ${req.method} ${urlPath}${modelInfo}`);
|
|
961
1012
|
}
|
|
1013
|
+
// Body dump — -vv / DARIO_LOG_BODIES=1. Runs on the outbound
|
|
1014
|
+
// body after the template build so operators see what actually
|
|
1015
|
+
// lands on the wire. sanitizeError's redaction strips bearer
|
|
1016
|
+
// tokens, sk-ant-* keys, and JWT triples in case any leaked
|
|
1017
|
+
// into the body (e.g. user pasted a curl). 8KB cap because the
|
|
1018
|
+
// CC system prompt alone is 25KB and dumping it every request
|
|
1019
|
+
// buries the useful content. dario#40.
|
|
1020
|
+
if (verboseBodies && finalBody) {
|
|
1021
|
+
const rendered = finalBody.toString('utf8');
|
|
1022
|
+
const capped = rendered.length > 8192
|
|
1023
|
+
? rendered.slice(0, 8192) + `\n[...truncated ${rendered.length - 8192} bytes]`
|
|
1024
|
+
: rendered;
|
|
1025
|
+
console.log(`[dario] #${requestCount} request body:\n${sanitizeError(capped)}`);
|
|
1026
|
+
}
|
|
962
1027
|
// Beta headers
|
|
963
1028
|
const clientBeta = req.headers['anthropic-beta'];
|
|
964
1029
|
let beta;
|
|
@@ -985,6 +1050,14 @@ export async function startProxy(opts = {}) {
|
|
|
985
1050
|
if (filtered)
|
|
986
1051
|
beta += ',' + filtered;
|
|
987
1052
|
}
|
|
1053
|
+
// Strip any beta flags the upstream has previously rejected on this
|
|
1054
|
+
// account so we don't re-pay the 400 round-trip (dario#42 afk-mode
|
|
1055
|
+
// fallout: captured templates carry tier-gated flags whose availability
|
|
1056
|
+
// we only learn at request time).
|
|
1057
|
+
const rejectedSet = unavailableBetas.get(acctKey);
|
|
1058
|
+
if (rejectedSet && rejectedSet.size > 0) {
|
|
1059
|
+
beta = beta.split(',').filter((t) => t.length > 0 && !rejectedSet.has(t)).join(',');
|
|
1060
|
+
}
|
|
988
1061
|
}
|
|
989
1062
|
// Rate governor — prevent inhuman request cadence
|
|
990
1063
|
const now = Date.now();
|
|
@@ -1086,7 +1159,56 @@ export async function startProxy(opts = {}) {
|
|
|
1086
1159
|
const isLongContextError = peekedBody.includes('long context')
|
|
1087
1160
|
|| peekedBody.includes('Extra usage is required')
|
|
1088
1161
|
|| peekedBody.includes('long_context');
|
|
1089
|
-
|
|
1162
|
+
// Detect "Unexpected value(s) `flag-name` for the `anthropic-beta` header"
|
|
1163
|
+
// — the upstream's way of saying this account tier doesn't have the
|
|
1164
|
+
// flag. Parse out the offending tokens (there can be more than one),
|
|
1165
|
+
// cache them, strip, and retry.
|
|
1166
|
+
const betaRejectedFlags = [];
|
|
1167
|
+
if (upstream.status === 400 && peekedBody.includes('anthropic-beta')) {
|
|
1168
|
+
const re = /Unexpected value\(s\)\s+((?:`[^`]+`(?:\s*,\s*)?)+)\s+for the `anthropic-beta` header/;
|
|
1169
|
+
const m = peekedBody.match(re);
|
|
1170
|
+
if (m) {
|
|
1171
|
+
for (const tok of m[1].matchAll(/`([^`]+)`/g))
|
|
1172
|
+
betaRejectedFlags.push(tok[1]);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (betaRejectedFlags.length > 0) {
|
|
1176
|
+
const acctKey = poolAccount?.alias ?? ACCOUNT_KEY_SINGLE;
|
|
1177
|
+
let set = unavailableBetas.get(acctKey);
|
|
1178
|
+
if (!set) {
|
|
1179
|
+
set = new Set();
|
|
1180
|
+
unavailableBetas.set(acctKey, set);
|
|
1181
|
+
}
|
|
1182
|
+
const newFlags = [];
|
|
1183
|
+
for (const f of betaRejectedFlags) {
|
|
1184
|
+
if (!set.has(f)) {
|
|
1185
|
+
set.add(f);
|
|
1186
|
+
newFlags.push(f);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (verbose && newFlags.length > 0)
|
|
1190
|
+
console.log(`[dario] #${requestCount} anthropic-beta rejected (${newFlags.join(',')}) — retrying without (cached for session)`);
|
|
1191
|
+
const reducedBeta = beta.split(',').filter((t) => t.length > 0 && !set.has(t)).join(',');
|
|
1192
|
+
const retryHeaders = { ...headers, 'anthropic-beta': reducedBeta };
|
|
1193
|
+
const retry = await fetch(targetBase, {
|
|
1194
|
+
method: req.method ?? 'POST',
|
|
1195
|
+
headers: passthrough ? retryHeaders : orderHeadersForOutbound(retryHeaders),
|
|
1196
|
+
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
1197
|
+
signal: upstreamAbort.signal,
|
|
1198
|
+
});
|
|
1199
|
+
upstream = retry;
|
|
1200
|
+
peekedBody = null;
|
|
1201
|
+
if (pool && poolAccount) {
|
|
1202
|
+
const retrySnapshot = parseRateLimits(upstream.headers);
|
|
1203
|
+
if (upstream.status === 429) {
|
|
1204
|
+
pool.markRejected(poolAccount.alias, retrySnapshot);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
pool.updateRateLimits(poolAccount.alias, retrySnapshot);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
else if (isLongContextError) {
|
|
1090
1212
|
// Cache the rejection so future requests on this account skip
|
|
1091
1213
|
// context-1m up front instead of re-paying the 400/429 round-trip.
|
|
1092
1214
|
const acctKey = poolAccount?.alias ?? ACCOUNT_KEY_SINGLE;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.19.
|
|
3
|
+
"version": "3.19.3",
|
|
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/tool-schema-contract.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 && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs",
|
|
24
|
+
"test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.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 && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs",
|
|
25
25
|
"audit": "npm audit --production --audit-level=high",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"start": "node dist/cli.js",
|