@askalf/dario 3.4.3 → 3.4.5
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 +22 -17
- package/dist/cc-oauth-detect.js +31 -17
- package/dist/cc-template.d.ts +1 -0
- package/dist/cc-template.js +47 -9
- package/dist/proxy.js +63 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ export ANTHROPIC_API_KEY=dario # or OPENAI_API_KEY=dario
|
|
|
37
37
|
|
|
38
38
|
Opus, Sonnet, Haiku — all models, streaming, tool use. **Zero dependencies.** ~2,000 lines of TypeScript. Works with Cursor, Continue, Aider, LiteLLM, Hermes, OpenClaw, or any tool that speaks the Anthropic or OpenAI API. Auto-launches under [Bun](https://bun.sh) when available for TLS fingerprint fidelity. **Auto-detects OAuth config from your installed CC binary** so dario stays in sync forever — Anthropic can rotate client IDs and dario picks them up on the next run.
|
|
39
39
|
|
|
40
|
-
dario is
|
|
40
|
+
dario is the **per-request layer** — one account, one workload, every request indistinguishable from CC on the wire. Session-level and account-level concerns (multi-account pooling, behavioral-classifier shaping, 24/7 fleets) live a layer above dario, in [askalf](https://askalf.org) — [see below](#askalf). Both are built by the same team on the same OAuth and billing infrastructure.
|
|
41
41
|
|
|
42
42
|
<table>
|
|
43
43
|
<tr>
|
|
@@ -91,7 +91,7 @@ dario is the only proxy that solves this. Instead of transforming your requests
|
|
|
91
91
|
| **Approach** | Template replay — sends CC's actual request | Signal matching or none |
|
|
92
92
|
| **Tools** | CC's exact tool definitions sent upstream | Client tools (detected) |
|
|
93
93
|
| **Max plan limits** | Used correctly | Bypassed — billed separately |
|
|
94
|
-
| **Detection resistance** | Undetectable without flagging CC itself | Detected by tool names, field order, effort level, etc. |
|
|
94
|
+
| **Detection resistance** | Undetectable at the per-request level without flagging CC itself | Detected by tool names, field order, effort level, etc. |
|
|
95
95
|
| **Dependencies** | 0 | Many |
|
|
96
96
|
|
|
97
97
|
<details>
|
|
@@ -408,13 +408,13 @@ Anthropic periodically rotates the OAuth `client_id`, authorize URL, token URL,
|
|
|
408
408
|
|
|
409
409
|
Dario scans the installed CC binary at startup and extracts the current config directly:
|
|
410
410
|
|
|
411
|
-
- **Anchor**: `
|
|
412
|
-
- **Extracted**: `CLIENT_ID`, `CLAUDE_AI_AUTHORIZE_URL`, `TOKEN_URL`, and the full `user:*` scope string.
|
|
413
|
-
- **Cached**: Results stored at `~/.dario/cc-oauth-cache.json` keyed by binary fingerprint (first 64KB sha256 + size + mtime). Cold scan ~500ms, cache hit ~5ms. Re-scans only when CC is upgraded.
|
|
414
|
-
- **Fallback**: If CC is not installed or scanning fails, dario uses
|
|
411
|
+
- **Anchor**: `BASE_API_URL:"https://api.anthropic.com"` — this literal appears only inside CC's live prod OAuth config block, so the scanner reliably lands in the right object even when the minifier reorders fields across CC releases.
|
|
412
|
+
- **Extracted**: `CLIENT_ID`, `CLAUDE_AI_AUTHORIZE_URL`, `TOKEN_URL`, and the full `user:*` scope string. A defensive check rejects any scan result that matches a known-dead internal client_id.
|
|
413
|
+
- **Cached**: Results stored at `~/.dario/cc-oauth-cache-v2.json` keyed by binary fingerprint (first 64KB sha256 + size + mtime). Cold scan ~500ms, cache hit ~5ms. Re-scans only when CC is upgraded.
|
|
414
|
+
- **Fallback**: If CC is not installed or scanning fails, dario uses the CC 2.1.104 prod config values hardcoded in-tree. No user action needed.
|
|
415
415
|
- **Override**: Set `DARIO_CC_PATH=/path/to/claude` to point dario at a non-standard CC binary location.
|
|
416
416
|
|
|
417
|
-
CC ships
|
|
417
|
+
CC ships three OAuth config factories (`local`, `staging`, `prod`) in one binary, selected at runtime by a function that is hardcoded to `prod` in shipped builds. Only the prod block is live; the other two are dead code paths CC uses when pointing at internal dev/staging infrastructure. The scanner targets the prod block specifically.
|
|
418
418
|
|
|
419
419
|
End-to-end verification lives at [`test/oauth-detector.mjs`](test/oauth-detector.mjs).
|
|
420
420
|
|
|
@@ -439,8 +439,10 @@ End-to-end verification lives at [`test/oauth-detector.mjs`](test/oauth-detector
|
|
|
439
439
|
| `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC tools | off |
|
|
440
440
|
| `--model=MODEL` | Force a model (`opus`, `sonnet`, `haiku`, or full ID) | passthrough |
|
|
441
441
|
| `--port=PORT` | Port to listen on | `3456` |
|
|
442
|
+
| `--host=ADDRESS` / `DARIO_HOST` | Bind address. Use `0.0.0.0` to accept LAN connections, or a specific IP to bind selectively (e.g. a Tailscale interface). When non-loopback, also set `DARIO_API_KEY`. | `127.0.0.1` |
|
|
442
443
|
| `--verbose` / `-v` | Log every request | off |
|
|
443
|
-
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` or `Authorization: Bearer` | unset (open) |
|
|
444
|
+
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` or `Authorization: Bearer`. **Required** when `--host` binds to anything other than loopback. | unset (open) |
|
|
445
|
+
| `DARIO_CORS_ORIGIN` | Override the browser CORS `Access-Control-Allow-Origin`. Useful for browser-based clients (open-webui, librechat) connecting over a mesh network. | `http://localhost:${port}` |
|
|
444
446
|
| `DARIO_NO_BUN` | Disable automatic Bun relaunch (stay on Node.js) | unset |
|
|
445
447
|
| `DARIO_MIN_INTERVAL_MS` | Minimum ms between requests (rate governor) | `500` |
|
|
446
448
|
| `DARIO_CC_PATH` | Override path to Claude Code binary for OAuth detection | auto-detect |
|
|
@@ -506,9 +508,9 @@ curl http://localhost:3456/health
|
|
|
506
508
|
|---------|---------------------|
|
|
507
509
|
| Credential storage | Reads from Claude Code (`~/.claude/.credentials.json`) or its own store (`~/.dario/credentials.json`) with `0600` permissions |
|
|
508
510
|
| OAuth flow | PKCE (Proof Key for Code Exchange) — no client secret needed |
|
|
509
|
-
| OAuth config source | Auto-detected from local CC binary at runtime; cached at `~/.dario/cc-oauth-cache.json`. Detector reads binary in read-only mode, never modifies it. |
|
|
511
|
+
| OAuth config source | Auto-detected from local CC binary at runtime; cached at `~/.dario/cc-oauth-cache-v2.json`. Detector reads binary in read-only mode, never modifies it. |
|
|
510
512
|
| Token exposure | Tokens never logged; redacted from all error messages. |
|
|
511
|
-
| Network binding | Binds
|
|
513
|
+
| Network binding | Binds to `127.0.0.1` by default. Override with `--host` / `DARIO_HOST` for mesh/LAN use; dario refuses to treat non-loopback binds as safe and requires you to set `DARIO_API_KEY` to avoid an unauthenticated LAN-reachable proxy. Upstream traffic goes only to `api.anthropic.com` over HTTPS. |
|
|
512
514
|
| Auth timing | `timingSafeEqual` used for `DARIO_API_KEY` comparison. |
|
|
513
515
|
| SSRF protection | Only `/v1/messages` and `/v1/complete` are proxied upstream — hardcoded allowlist. |
|
|
514
516
|
| Body size | 10MB hard cap per request. 30s read timeout prevents slow-loris. |
|
|
@@ -519,11 +521,11 @@ curl http://localhost:3456/health
|
|
|
519
521
|
|
|
520
522
|
## askalf
|
|
521
523
|
|
|
522
|
-
dario
|
|
524
|
+
**dario and askalf solve different layers of the same problem.**
|
|
523
525
|
|
|
524
|
-
|
|
526
|
+
dario is the per-request layer: one account, one workload, every request on the wire indistinguishable from Claude Code. It's what you reach for when you want your Max/Pro subscription usable from any tool that speaks the Anthropic or OpenAI API. It does not pool accounts, shape sessions, distribute load, or care about cumulative behavioral signals — those are not per-request concerns, and solving them at the per-request layer is a category error.
|
|
525
527
|
|
|
526
|
-
**askalf** is the
|
|
528
|
+
**askalf** is the layer above that: multi-account pooling behind one endpoint, session and workload shaping to stay under Anthropic's session-level classifiers, persistent browser and desktop sessions, scheduling, and a hosted fleet that runs 24/7. Built on the same OAuth and billing infrastructure as dario.
|
|
527
529
|
|
|
528
530
|
| | dario | askalf |
|
|
529
531
|
|---|---|---|
|
|
@@ -571,18 +573,21 @@ Optional but recommended. If [Bun](https://bun.sh) is installed, dario auto-rela
|
|
|
571
573
|
Dario auto-refreshes tokens 30 minutes before expiry. You should never see an auth error in normal use. If something goes wrong, `dario refresh` forces an immediate refresh.
|
|
572
574
|
|
|
573
575
|
**What happens when Anthropic rotates the OAuth client_id or URL?**
|
|
574
|
-
Dario auto-detects OAuth config from your installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next startup — no dario release needed. The detector is cached at `~/.dario/cc-oauth-cache.json` and only re-scans when the binary fingerprint changes. If CC isn't installed, dario falls back to
|
|
576
|
+
Dario auto-detects OAuth config from your installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next startup — no dario release needed. The detector is cached at `~/.dario/cc-oauth-cache-v2.json` and only re-scans when the binary fingerprint changes. If CC isn't installed, dario falls back to hardcoded CC 2.1.104 prod values.
|
|
575
577
|
|
|
576
578
|
**I'm hitting rate limits. What do I do?**
|
|
577
579
|
Claude subscriptions have rolling 5-hour and 7-day usage windows. Check your utilization with Claude Code's `/usage` command or the [statusline](https://code.claude.com/docs/en/statusline). Rate limit errors from dario include utilization percentages and reset times so you can see exactly when capacity returns.
|
|
578
580
|
|
|
581
|
+
**My multi-agent workload is getting reclassified to overage even though dario template-replays per request. Why?**
|
|
582
|
+
Because reclassification at high agent volume is not a per-request problem. Anthropic's classifier operates on cumulative per-OAuth-session behavioral aggregates — token throughput, conversation depth, streaming duration, inter-arrival timing, thinking-block volume. Dario can make each individual request indistinguishable from Claude Code and still hit this wall on a long-running agent session, because the wall isn't at the request level. Thorough diagnostic work on this was contributed by [@belangertrading](https://github.com/belangertrading) in [#23](https://github.com/askalf/dario/issues/23), including the per-request v3.4.3 hardening that landed as a result. For the session-layer shaping itself — multi-account pooling, session rotation, workload distribution that keeps any single account from concentrating the behavioral signal — that's what [askalf](https://askalf.org) is built for. Different layer, different tool.
|
|
583
|
+
|
|
579
584
|
If you're running a multi-agent workload and consistently hitting limits, [askalf](https://askalf.org) distributes load across multiple accounts automatically.
|
|
580
585
|
|
|
581
586
|
**What are the usage limits?**
|
|
582
587
|
Claude subscriptions have rolling 5-hour and 7-day usage windows shared across claude.ai and Claude Code. See [Anthropic's docs](https://support.claude.com/en/articles/11647753-how-do-usage-and-length-limits-work) for details.
|
|
583
588
|
|
|
584
589
|
**Can I run this on a server?**
|
|
585
|
-
Dario binds to localhost by default. For server use, handle the initial login on a machine with a browser, then copy `~/.claude/.credentials.json` (or `~/.dario/credentials.json`) to your server. Auto-refresh will keep it alive from there.
|
|
590
|
+
Dario binds to localhost by default. For server use, handle the initial login on a machine with a browser, then copy `~/.claude/.credentials.json` (or `~/.dario/credentials.json`) to your server. Auto-refresh will keep it alive from there. If you want dario reachable from other machines on the same LAN or a Tailscale mesh, pass `--host=0.0.0.0` (or a specific interface IP) and set `DARIO_API_KEY` to gate access.
|
|
586
591
|
|
|
587
592
|
**Why "dario"?**
|
|
588
593
|
Named after [Dario Amodei](https://en.wikipedia.org/wiki/Dario_Amodei), CEO of Anthropic.
|
|
@@ -622,7 +627,7 @@ Dario handles your OAuth tokens. Here's why you can trust it:
|
|
|
622
627
|
| **npm provenance** | Every release is [SLSA attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
|
|
623
628
|
| **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
|
|
624
629
|
| **Credential handling** | Tokens never logged, redacted from errors, stored with 0600 permissions |
|
|
625
|
-
| **Network scope** | Binds to 127.0.0.1
|
|
630
|
+
| **Network scope** | Binds to 127.0.0.1 by default; `--host` / `DARIO_HOST` allows LAN/mesh exposure with `DARIO_API_KEY` gating. Upstream traffic goes exclusively to `api.anthropic.com` over HTTPS |
|
|
626
631
|
| **No telemetry** | Zero analytics, tracking, or data collection of any kind |
|
|
627
632
|
| **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
|
|
628
633
|
| **Branch protection** | CI must pass before merge. CODEOWNERS enforces review |
|
|
@@ -673,7 +678,7 @@ npm run dev # runs with tsx (no build needed)
|
|
|
673
678
|
| Who | Contributions |
|
|
674
679
|
|-----|---------------|
|
|
675
680
|
| [@GodsBoy](https://github.com/GodsBoy) | Proxy authentication, token redaction, error sanitization ([#2](https://github.com/askalf/dario/pull/2)) |
|
|
676
|
-
| [@belangertrading](https://github.com/belangertrading) | Billing classification investigation ([#4](https://github.com/askalf/dario/issues/4)), billing reclassification root cause ([#7](https://github.com/askalf/dario/issues/7)) |
|
|
681
|
+
| [@belangertrading](https://github.com/belangertrading) | Billing classification investigation ([#4](https://github.com/askalf/dario/issues/4)), cache_control fingerprinting ([#6](https://github.com/askalf/dario/issues/6)), billing reclassification root cause ([#7](https://github.com/askalf/dario/issues/7)), OAuth client_id discovery ([#12](https://github.com/askalf/dario/issues/12)), multi-agent session-level billing analysis ([#23](https://github.com/askalf/dario/issues/23)) |
|
|
677
682
|
|
|
678
683
|
## License
|
|
679
684
|
|
package/dist/cc-oauth-detect.js
CHANGED
|
@@ -44,12 +44,23 @@ const FALLBACK = {
|
|
|
44
44
|
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
45
45
|
authorizeUrl: 'https://claude.com/cai/oauth/authorize',
|
|
46
46
|
tokenUrl: 'https://platform.claude.com/v1/oauth/token',
|
|
47
|
-
|
|
47
|
+
// Scopes are the full `n36` union from the CC binary, which is the value
|
|
48
|
+
// sent during a normal `claude login` (non-setup-token) flow. In CC's
|
|
49
|
+
// source: `let D = f ? [TI] : n36` where `f = inferenceOnly` (true only
|
|
50
|
+
// for `claude setup-token`). Normal interactive login uses the 6-scope
|
|
51
|
+
// union including `org:create_api_key` — even though that scope is named
|
|
52
|
+
// "Console-only" by convention, CC's own login flow requests it up front.
|
|
53
|
+
// Earlier dario versions (3.2.7 through 3.4.3) dropped `org:create_api_key`
|
|
54
|
+
// from the list based on a misread of the name; the dev-only client_id
|
|
55
|
+
// was lenient enough to accept the shorter list, the prod client_id is not.
|
|
56
|
+
scopes: 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload',
|
|
48
57
|
source: 'fallback',
|
|
49
58
|
};
|
|
50
|
-
// -
|
|
51
|
-
// (
|
|
52
|
-
|
|
59
|
+
// -v3 suffix invalidates v3.4.3 caches that were populated with the wrong
|
|
60
|
+
// 4-scope list (the scanner's regex matched a help-message string literal
|
|
61
|
+
// in the CC binary instead of the real scope array). See the scanner's
|
|
62
|
+
// scope handling below for why scope detection is no longer attempted.
|
|
63
|
+
const CACHE_PATH = join(homedir(), '.dario', 'cc-oauth-cache-v3.json');
|
|
53
64
|
function candidatePaths() {
|
|
54
65
|
const home = homedir();
|
|
55
66
|
if (platform() === 'win32') {
|
|
@@ -139,19 +150,22 @@ export function scanBinaryForOAuthConfig(buf) {
|
|
|
139
150
|
const tokenMatch = /TOKEN_URL\s*:\s*"(https:\/\/[^"]*\/oauth\/token[^"]*)"/.exec(prodBlock);
|
|
140
151
|
if (tokenMatch && tokenMatch[1])
|
|
141
152
|
tokenUrl = tokenMatch[1];
|
|
142
|
-
// Scopes
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// Scopes are NOT detected from the binary. Previous versions of this
|
|
154
|
+
// scanner anchored on `"user:profile ` and regex-captured the first
|
|
155
|
+
// contiguous quoted run of scopes, but that anchor matches an error/help
|
|
156
|
+
// message string literal (used by `claude setup-token` error output) that
|
|
157
|
+
// contains only 4 of the 6 actual scopes. The real scope array is stored
|
|
158
|
+
// as a constant-reference array — `dY8 = [B9H, TI, "user:sessions:...", ...]`
|
|
159
|
+
// — where the first two elements are minified variable references, not
|
|
160
|
+
// literal strings, so no regex can reliably extract the full list. And the
|
|
161
|
+
// runtime-computed union `n36` only exists after `Array.from(new Set(...))`
|
|
162
|
+
// executes, which we can't evaluate from a static scan.
|
|
163
|
+
//
|
|
164
|
+
// Given that scopes rarely change across CC releases (Anthropic adds or
|
|
165
|
+
// removes maybe one per major version), hardcoding them in FALLBACK is
|
|
166
|
+
// more reliable than scanning. If Anthropic changes the scope list, the
|
|
167
|
+
// fix is a one-line FALLBACK update in a dario release.
|
|
168
|
+
return { clientId, authorizeUrl, tokenUrl, scopes: FALLBACK.scopes };
|
|
155
169
|
}
|
|
156
170
|
async function loadCache() {
|
|
157
171
|
try {
|
package/dist/cc-template.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const CC_TOOL_DEFINITIONS: {
|
|
|
15
15
|
export declare const CC_SYSTEM_PROMPT: string;
|
|
16
16
|
/** CC's agent identity string. */
|
|
17
17
|
export declare const CC_AGENT_IDENTITY: string;
|
|
18
|
+
export declare function scrubFrameworkIdentifiers(text: string): string;
|
|
18
19
|
/** Client tool name → CC tool mapping with parameter translation. */
|
|
19
20
|
interface ToolMapping {
|
|
20
21
|
ccTool: string;
|
package/dist/cc-template.js
CHANGED
|
@@ -17,6 +17,28 @@ export const CC_TOOL_DEFINITIONS = TEMPLATE.tools;
|
|
|
17
17
|
export const CC_SYSTEM_PROMPT = TEMPLATE.system_prompt;
|
|
18
18
|
/** CC's agent identity string. */
|
|
19
19
|
export const CC_AGENT_IDENTITY = TEMPLATE.agent_identity;
|
|
20
|
+
// Framework identifiers that would flag non-CC usage. Stripped from the system
|
|
21
|
+
// prompt and from message content text blocks before the request goes upstream.
|
|
22
|
+
const FRAMEWORK_PATTERNS = [
|
|
23
|
+
// Compound/hyphenated patterns run first so their halves can't be eaten
|
|
24
|
+
// by the simpler word-level patterns below.
|
|
25
|
+
/\b(roo[- ]?cline|big[- ]?agi|claude[- ]?bridge)\b/gi,
|
|
26
|
+
/\b(openclaw|hermes|aider|cursor|windsurf|cline|continue|copilot|cody)\b/gi,
|
|
27
|
+
/\b(librechat|typingmind)\b/gi,
|
|
28
|
+
/\b(openai|gpt-4|gpt-3\.5)\b/gi,
|
|
29
|
+
/powered by [a-z]+/gi,
|
|
30
|
+
/\bgateway\b/gi,
|
|
31
|
+
// OC's sessions_* tool-name prefix — flagged as a fingerprint in dario#23.
|
|
32
|
+
/\bsessions_[a-z_]+\b/gi,
|
|
33
|
+
];
|
|
34
|
+
export function scrubFrameworkIdentifiers(text) {
|
|
35
|
+
let result = text;
|
|
36
|
+
for (const pattern of FRAMEWORK_PATTERNS) {
|
|
37
|
+
pattern.lastIndex = 0;
|
|
38
|
+
result = result.replace(pattern, '');
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
20
42
|
const TOOL_MAP = {
|
|
21
43
|
// Direct maps
|
|
22
44
|
bash: { ccTool: 'Bash', translateArgs: (a) => ({ command: a.cmd || a.command || a.c || '' }) },
|
|
@@ -194,15 +216,31 @@ export function buildCCRequest(clientBody, billingTag, cache1h, identity, opts =
|
|
|
194
216
|
.map(b => b.text)
|
|
195
217
|
.join('\n\n');
|
|
196
218
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
219
|
+
systemText = scrubFrameworkIdentifiers(systemText);
|
|
220
|
+
// Also scrub framework identifiers from message content text blocks.
|
|
221
|
+
// Clients often inject their product name into user/tool messages as well,
|
|
222
|
+
// and the system-prompt-only scrub used to miss those.
|
|
223
|
+
for (const msg of messages) {
|
|
224
|
+
if (typeof msg.content === 'string') {
|
|
225
|
+
msg.content = scrubFrameworkIdentifiers(msg.content);
|
|
226
|
+
}
|
|
227
|
+
else if (Array.isArray(msg.content)) {
|
|
228
|
+
for (const block of msg.content) {
|
|
229
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
230
|
+
block.text = scrubFrameworkIdentifiers(block.text);
|
|
231
|
+
}
|
|
232
|
+
if (block.type === 'tool_result' && typeof block.content === 'string') {
|
|
233
|
+
block.content = scrubFrameworkIdentifiers(block.content);
|
|
234
|
+
}
|
|
235
|
+
if (block.type === 'tool_result' && Array.isArray(block.content)) {
|
|
236
|
+
for (const sub of block.content) {
|
|
237
|
+
if (sub.type === 'text' && typeof sub.text === 'string') {
|
|
238
|
+
sub.text = scrubFrameworkIdentifiers(sub.text);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
206
244
|
}
|
|
207
245
|
// ── Build the CC request from template ──
|
|
208
246
|
// Key order matches CC v2.1.104 exactly:
|
package/dist/proxy.js
CHANGED
|
@@ -147,6 +147,7 @@ const ORCHESTRATION_TAG_NAMES = [
|
|
|
147
147
|
'system-reminder', 'env', 'system_information', 'current_working_directory',
|
|
148
148
|
'operating_system', 'default_shell', 'home_directory', 'task_metadata',
|
|
149
149
|
'directories', 'thinking',
|
|
150
|
+
'agent_persona', 'agent_context', 'tool_context', 'persona', 'tool_call',
|
|
150
151
|
];
|
|
151
152
|
const ORCHESTRATION_PATTERNS = ORCHESTRATION_TAG_NAMES.flatMap(tag => [
|
|
152
153
|
new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'),
|
|
@@ -459,6 +460,10 @@ export async function startProxy(opts = {}) {
|
|
|
459
460
|
}
|
|
460
461
|
// Proxy to Anthropic (with concurrency control)
|
|
461
462
|
await semaphore.acquire();
|
|
463
|
+
// Hoisted so the finally block can clean up whatever was set.
|
|
464
|
+
let upstreamTimeout = null;
|
|
465
|
+
let onClientClose = null;
|
|
466
|
+
let upstreamAbortReason = null;
|
|
462
467
|
try {
|
|
463
468
|
const accessToken = await getAccessToken();
|
|
464
469
|
// Read request body with size limit and timeout (prevents slow-loris)
|
|
@@ -562,11 +567,33 @@ export async function startProxy(opts = {}) {
|
|
|
562
567
|
// CC sends 600 on first request per session. With rotation, every request is "first"
|
|
563
568
|
'x-stainless-timeout': '600',
|
|
564
569
|
};
|
|
570
|
+
// Client-disconnect abort: if the client drops the connection before
|
|
571
|
+
// we've finished sending the response, we abort the upstream fetch so
|
|
572
|
+
// Anthropic stops generating (and billing) a response nobody will
|
|
573
|
+
// read. Also carries the 5-minute upstream timeout via the same
|
|
574
|
+
// controller, so a single signal covers both cancellation reasons.
|
|
575
|
+
const upstreamAbort = new AbortController();
|
|
576
|
+
upstreamTimeout = setTimeout(() => {
|
|
577
|
+
if (!upstreamAbort.signal.aborted) {
|
|
578
|
+
upstreamAbortReason = 'timeout';
|
|
579
|
+
upstreamAbort.abort();
|
|
580
|
+
}
|
|
581
|
+
}, UPSTREAM_TIMEOUT_MS);
|
|
582
|
+
onClientClose = () => {
|
|
583
|
+
// 'close' fires on both normal teardown and client disconnect.
|
|
584
|
+
// We only want to abort if we haven't finished our response yet —
|
|
585
|
+
// normal teardown happens AFTER res.writableEnded becomes true.
|
|
586
|
+
if (!res.writableEnded && !upstreamAbort.signal.aborted) {
|
|
587
|
+
upstreamAbortReason = 'client_closed';
|
|
588
|
+
upstreamAbort.abort();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
req.on('close', onClientClose);
|
|
565
592
|
let upstream = await fetch(targetBase, {
|
|
566
593
|
method: req.method ?? 'POST',
|
|
567
594
|
headers,
|
|
568
595
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
569
|
-
signal:
|
|
596
|
+
signal: upstreamAbort.signal,
|
|
570
597
|
});
|
|
571
598
|
// Auto-retry without context-1m if it triggers a long-context billing error.
|
|
572
599
|
// Anthropic returns this as either 400 ("long context beta is not yet available
|
|
@@ -590,7 +617,7 @@ export async function startProxy(opts = {}) {
|
|
|
590
617
|
method: req.method ?? 'POST',
|
|
591
618
|
headers: retryHeaders,
|
|
592
619
|
body: finalBody ? new Uint8Array(finalBody) : undefined,
|
|
593
|
-
signal:
|
|
620
|
+
signal: upstreamAbort.signal,
|
|
594
621
|
});
|
|
595
622
|
// Use the retry response from here on — peeked body is now stale
|
|
596
623
|
upstream = retry;
|
|
@@ -750,12 +777,42 @@ export async function startProxy(opts = {}) {
|
|
|
750
777
|
}
|
|
751
778
|
}
|
|
752
779
|
catch (err) {
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
780
|
+
// Differentiate the three failure modes so each gets the right
|
|
781
|
+
// response (and so we don't spam logs when clients simply drop).
|
|
782
|
+
if (upstreamAbortReason === 'client_closed') {
|
|
783
|
+
if (verbose)
|
|
784
|
+
console.log(`[dario] #${requestCount} aborted (client disconnected)`);
|
|
785
|
+
}
|
|
786
|
+
else if (upstreamAbortReason === 'timeout') {
|
|
787
|
+
console.error(`[dario] #${requestCount} upstream timeout after ${UPSTREAM_TIMEOUT_MS / 1000}s`);
|
|
788
|
+
if (!res.headersSent) {
|
|
789
|
+
res.writeHead(504, JSON_HEADERS);
|
|
790
|
+
res.end(JSON.stringify({ error: 'Upstream timeout', message: `Anthropic did not respond within ${UPSTREAM_TIMEOUT_MS / 1000}s` }));
|
|
791
|
+
}
|
|
792
|
+
else if (!res.writableEnded) {
|
|
793
|
+
res.end();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// Log full error server-side, return generic message to client
|
|
798
|
+
console.error('[dario] Proxy error:', sanitizeError(err));
|
|
799
|
+
if (!res.headersSent) {
|
|
800
|
+
res.writeHead(502, JSON_HEADERS);
|
|
801
|
+
res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
|
|
802
|
+
}
|
|
803
|
+
else if (!res.writableEnded) {
|
|
804
|
+
res.end();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
757
807
|
}
|
|
758
808
|
finally {
|
|
809
|
+
// Always clean up the upstream-abort plumbing if it was set up. The
|
|
810
|
+
// setup happens after the body-read phase, so on fast-path errors
|
|
811
|
+
// (413, body read timeout) these may still be null — guard accordingly.
|
|
812
|
+
if (upstreamTimeout !== null)
|
|
813
|
+
clearTimeout(upstreamTimeout);
|
|
814
|
+
if (onClientClose !== null)
|
|
815
|
+
req.off('close', onClientClose);
|
|
759
816
|
semaphore.release();
|
|
760
817
|
}
|
|
761
818
|
});
|