@askalf/dario 3.5.0 → 3.6.1

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
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <h1 align="center">dario</h1>
3
- <p align="center"><strong>Use your Claude subscription inside your local tools and scripts without moving to API billing or rebuilding your workflow around a separate stack.</strong></p>
3
+ <p align="center"><strong>A local LLM router. One endpoint on your machine, every provider behind it, your tools don't need to change.</strong></p>
4
4
  </p>
5
5
 
6
6
  <p align="center">
@@ -14,9 +14,8 @@
14
14
  <p align="center">
15
15
  <a href="#quick-start">Quick Start</a> &bull;
16
16
  <a href="#who-this-is-for">Who it's for</a> &bull;
17
+ <a href="#backends">Backends</a> &bull;
17
18
  <a href="#why-switch">Why switch</a> &bull;
18
- <a href="#how-it-works">How it works</a> &bull;
19
- <a href="#from-standalone-to-askalf">Standalone → askalf</a> &bull;
20
19
  <a href="#trust--transparency">Trust</a> &bull;
21
20
  <a href="#faq">FAQ</a>
22
21
  </p>
@@ -25,63 +24,83 @@
25
24
 
26
25
  ## What it is
27
26
 
28
- dario is a local process that turns your Claude Max or Pro subscription into an API endpoint that any tool on your machine can use. It talks to Anthropic the same way Claude Code does, so your subscription's rate limits are what you spend against not a separate API billing tier.
27
+ Dario runs on your machine and gives every tool you use one local URL that reaches **every LLM you use.** Point Cursor, Continue, Aider, LiteLLM, your own scripts anything that speaks the Anthropic or OpenAI API at `http://localhost:3456`, and dario routes each request to the right backend:
29
28
 
30
- Install it once, log in once (using your existing Claude Code credentials if you have them), and from that point on every tool that speaks the Anthropic API or the OpenAI chat completions API Cursor, Continue, Aider, LiteLLM, your own scripts, whatever — can reach Claude through `http://localhost:3456`.
29
+ - **Claude Max / Pro subscriptions** OAuth-backed, billed against your plan instead of API pricing. Multi-account pooling if you have more than one.
30
+ - **OpenAI** — your API key, routed to `api.openai.com` straight through.
31
+ - **Any OpenAI-compat endpoint** — OpenRouter, Groq, a local LiteLLM, Ollama's openai-compat mode, self-hosted vLLM. Set the backend's `baseUrl` once, done.
31
32
 
32
- **Single-account mode is the default.** You don't need an account anywhere, you don't need to wait for anything, and nothing phones home. Install and run.
33
+ Your tool sees one base URL. `gpt-4` goes to OpenAI. `claude-opus-4-6` goes to your Claude subscription. `llama-3-70b` goes to Groq. None of your tools have to know about any of it.
33
34
 
34
- **Pool mode** (new in v3.5.0) lifts multi-account routing into dario itself. Add two or more Claude subscriptions with `dario accounts add`, and dario starts selecting per request by the account with the most headroom, marking exhausted accounts rejected until they reset. No hosted platform required — you run the pool on your machine, against your own subscriptions. See [Multi-Account Pool Mode](#multi-account-pool-mode) below for the details.
35
-
36
- Separately, [askalf](https://askalf.org) is the hosted platform that does the things a local proxy on your machine can't — browser and desktop control, scheduling, persistent memory, 24/7 hosted fleets. Different problem, different tool. Dario does not depend on askalf, and askalf is not required to use any dario feature.
35
+ **No account anywhere is required.** Single-backend Claude dario works with nothing but `dario login`. Multi-backend dario works with nothing but local config files. Nothing phones home. Zero runtime dependencies. ~2,000 lines of TypeScript.
37
36
 
38
37
  ---
39
38
 
40
39
  ## Who this is for
41
40
 
42
41
  **Best fit:**
43
- - Power users already paying for Claude Max or Pro who want Claude available inside their local stack — editors, terminals, scripts, internal tools — without moving to API billing.
44
- - Small teams that work in terminals and IDEs, not in hosted agent stacks, and want Claude as a drop-in provider for whatever tools they already use.
45
- - Anyone who wants Claude Code's billing behavior on their own requests, from their own code.
42
+
43
+ - **Developers using multiple LLMs across multiple tools** who are tired of juggling base URLs, API keys, and per-tool provider configs.
44
+ - **Claude Max or Pro subscribers** who want their subscription usable anywhere that speaks the Anthropic or OpenAI API — without paying API rates for every request.
45
+ - **Teams running local or hosted OpenAI-compat servers** (LiteLLM, vLLM, Ollama, Groq, OpenRouter) who want one stable local endpoint in front of them that every tool can reuse.
46
+ - **Power users running multi-agent workloads on Claude subscriptions** who want multi-account pooling with headroom-aware routing on their own machine, against their own subscriptions, without a hosted platform.
46
47
 
47
48
  **Not a fit:**
48
- - You need vendor-managed production SLAs on every request. Use the Anthropic API directly.
49
- - You need high-scale agent orchestration, multi-account pooling, or session-level classifier shaping. That's [askalf](https://askalf.org), which dario bridges into.
50
- - You want a hosted chat UI. Use claude.ai.
49
+
50
+ - You need vendor-managed production SLAs on every request. Use the provider APIs directly.
51
+ - You need a hosted multi-tenant routing platform with a dashboard. Try [askalf](https://askalf.org), a separate product in the same family — different problem, different tool.
52
+ - You want a chat UI. Use claude.ai or chatgpt.com.
51
53
 
52
54
  ---
53
55
 
54
56
  ## First use case
55
57
 
56
- > Use Claude in your local automation and developer workflows the way you'd normally reach for an API but backed by the subscription you already pay for.
57
-
58
- You install dario, point your existing tool at `http://localhost:3456`, and that tool now sees Claude. The tool doesn't know it's going through a proxy; from its perspective dario *is* the Anthropic API. Your subscription handles the billing. Your Max plan limits are what count against usage.
58
+ > I install dario, point every tool I already use at `http://localhost:3456`, and every LLM I have access to works through that one URL.
59
59
 
60
60
  Flow on a fresh machine:
61
61
 
62
- 1. `npm install -g @askalf/dario`
63
- 2. `dario login` — detects your installed Claude Code credentials, or runs its own OAuth flow if you don't have CC installed
64
- 3. `dario proxy` — starts the local server on port 3456
65
- 4. Set two environment variables in the tool you already use:
66
- ```bash
67
- ANTHROPIC_BASE_URL=http://localhost:3456
68
- ANTHROPIC_API_KEY=dario
69
- ```
70
- 5. That tool now uses your Claude subscription. Streaming works, tool use works, prompt caching works.
62
+ ```bash
63
+ # Install
64
+ npm install -g @askalf/dario
65
+
66
+ # Optional: log in to your Claude subscription (Max or Pro)
67
+ dario login
68
+
69
+ # Optional: add an OpenAI-compat backend
70
+ dario backend add openai --key=sk-proj-...
71
+
72
+ # Start the proxy
73
+ dario proxy
74
+
75
+ # Use it — set these once, every tool that honors them just works
76
+ export ANTHROPIC_BASE_URL=http://localhost:3456
77
+ export ANTHROPIC_API_KEY=dario
78
+ export OPENAI_BASE_URL=http://localhost:3456/v1
79
+ export OPENAI_API_KEY=dario
80
+ ```
71
81
 
72
- No separate API key. No Extra Usage charges. No rebuilding your workflow around a new provider.
82
+ Now from the same Cursor/Continue/Aider instance:
83
+
84
+ - `gpt-4o` → OpenAI, your key, straight through
85
+ - `claude-opus-4-6` → Claude subscription, billed against your Max plan
86
+ - `opus` → shortcut, same as above
87
+ - `llama-3.1-70b` on OpenRouter → configure `dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1`, done
88
+
89
+ One URL. Your tool doesn't know or care which provider is answering.
73
90
 
74
91
  ---
75
92
 
76
93
  ## Why switch
77
94
 
78
- **Use dario if** you already pay for Claude Max or Pro and you want Claude inside the tools you already use, without paying API rates for every request or routing your work through a second hosted stack.
95
+ **Use dario if** you use more than one LLM provider, or more than one tool, or both and you're tired of configuring each tool with a different base URL and API key per provider.
96
+
97
+ **Use dario if** you pay for Claude Max or Pro and you want that subscription reachable from every tool on your machine, without paying API rates or opening a second billing surface.
79
98
 
80
- **Use dario pool mode if** you're running multi-agent workloads and hitting per-subscription rate limits add 2–N accounts with `dario accounts add` and dario handles headroom-aware routing across them, all on your machine, against your own subscriptions. No hosted stack to sign up for. See [Multi-Account Pool Mode](#multi-account-pool-mode).
99
+ **Use dario pool mode if** you're running multi-agent workloads on Claude subscriptions and hitting per-account rate limits. Add 2–N accounts with `dario accounts add` and dario routes across them by per-account headroom, all on your machine, against your own subscriptions. See [Multi-Account Pool Mode](#multi-account-pool-mode).
81
100
 
82
- **Use the Anthropic API directly if** you need platform-native primitives, vendor-managed production usage, high-scale control, or SLAs your subscription tier doesn't cover. Dario isn't trying to replace the API — it's trying to unlock the subscription you already bought.
101
+ **Use a provider API directly if** you need vendor-managed production SLAs or high-scale orchestration primitives the providers ship themselves. Dario isn't trying to replace their APIs — it's trying to put one local shim in front of all of them so your tools don't care which is which.
83
102
 
84
- **Don't use dario if** you want a subprocess bridge that shells out to `claude --print` under the hood. Those tools (openclaw-claude-bridge and similar) work well for single-team single-machine workloads that can accept a one-subscription rate ceiling and a one-machine deployment. Dario is the API-path alternative, which trades that simplicity for pooling-friendly behavior on the wire. Different tradeoffs, different tool.
103
+ **Don't use dario if** you want a subprocess bridge that shells out to `claude --print` under the hood (openclaw-claude-bridge and similar). That's a valid answer for single-team single-machine workloads that can accept a one-subscription rate ceiling and a one-machine deployment different tradeoffs, different tool.
85
104
 
86
105
  ---
87
106
 
@@ -91,176 +110,157 @@ No separate API key. No Extra Usage charges. No rebuilding your workflow around
91
110
  # Install
92
111
  npm install -g @askalf/dario
93
112
 
94
- # Log in (detects Claude Code credentials if installed)
113
+ # Claude subscription path (detects Claude Code credentials if CC is installed,
114
+ # runs its own OAuth flow otherwise)
95
115
  dario login
96
116
 
117
+ # OpenAI or any OpenAI-compat provider (optional, additive)
118
+ dario backend add openai --key=sk-proj-...
119
+
97
120
  # Start the proxy
98
121
  dario proxy
99
122
 
100
- # Anthropic SDK
123
+ # Point anything that speaks the Anthropic or OpenAI API at localhost:3456
101
124
  export ANTHROPIC_BASE_URL=http://localhost:3456
102
125
  export ANTHROPIC_API_KEY=dario
103
-
104
- # or OpenAI-compatible tools
105
126
  export OPENAI_BASE_URL=http://localhost:3456/v1
106
127
  export OPENAI_API_KEY=dario
107
128
  ```
108
129
 
109
- Opus, Sonnet, Haiku all models, streaming, tool use, prompt caching, extended thinking. **Zero runtime dependencies.** ~2,000 lines of TypeScript. Auto-launches under [Bun](https://bun.sh) when available for TLS fingerprint fidelity with Claude Code's runtime. **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.
130
+ Opus, Sonnet, Haiku, GPT-4o, o1, o3, o4, plus anything the configured OpenAI-compat backend serves. Streaming, tool use, prompt caching, extended thinking. **Zero runtime dependencies.** Auto-launches under [Bun](https://bun.sh) when available for TLS fingerprint fidelity with Claude Code's runtime on the Claude path.
110
131
 
111
132
  ---
112
133
 
113
- ## How It Works
134
+ ## Backends
114
135
 
115
- Dario has two modes: **direct API mode** (the default) and **passthrough mode** (`--passthrough`).
136
+ Dario's routing is organized around **backends**, each with its own auth and its own target. v3.6.0 ships two backends, with more coming.
116
137
 
117
- ### Direct API Mode Template Replay
138
+ ### 1. Claude subscription backend (built in)
118
139
 
119
- This is the mode you want for almost every case. Dario takes each request you send it and replaces it with a Claude Code request: same 25 tool definitions, same 25KB system prompt, same field order, same beta headers, same metadata structure, same device identity. Only your 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.
140
+ OAuth-backed Claude Max / Pro, billed against your plan instead of the API. Activated by `dario login`.
120
141
 
121
- ```
122
- ┌───────────┐ ┌─────────────────────┐ ┌──────────────────┐
123
- │ Your App │ ──> │ dario (proxy) │ ──> │ api.anthropic.com│
124
- │ │ │ localhost:3456 │ │ │
125
- │ sends │ │ │ │ sees a genuine │
126
- │ its own │ │ replaces request │ │ Claude Code │
127
- │ tools & │ │ with CC template │ │ request │
128
- │ params │ │ keeps only content │ │ │
129
- └───────────┘ └─────────────────────┘ └──────────────────┘
130
- ```
142
+ **What it does:**
131
143
 
132
- The details that matter:
144
+ - 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.
145
+ - **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)`.
146
+ - **OAuth config** auto-detected from the installed CC binary at startup. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a release.
147
+ - **Multi-account pool mode** — see below. Automatic when 2+ accounts are configured.
148
+ - **Framework scrubbing** — known fingerprint tokens (`OpenClaw`, `sessions_*` prefixes, orchestration tags) stripped from system prompt and message content before the request leaves your machine.
149
+ - **Bun auto-relaunch** — when Bun is installed, dario relaunches under it so the TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js.
133
150
 
134
- - **Billing tag** reconstructed using CC's own algorithm extracted from the binary: `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)`.
135
- - **Beta set** — CC's exact beta list in CC's order, minus any beta that would require Extra Usage to be enabled on your account.
136
- - **OAuth config** auto-detected from the installed CC binary at startup. When Anthropic rotates `client_id`, authorize URL, or scopes, dario picks up the new values on the next run without needing a new release. Falls back to hardcoded CC 2.1.104 prod values if CC isn't installed. Cache at `~/.dario/cc-oauth-cache-v3.json`, keyed by binary fingerprint.
137
- - **Session ID** rotates per request via `x-claude-code-session-id`, matching how `claude --print` behaves. A persistent session ID across rapid requests is a behavioral signal.
138
- - **Framework scrub** — framework identifiers and known fingerprint tokens (`OpenClaw`, `sessions_*` tool prefixes, orchestration tags, etc.) are stripped from both the system prompt and message content before the request goes upstream.
139
- - **Bun auto-relaunch** — when Bun is installed, dario relaunches under it so its TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js and works fine; the TLS fingerprint is the only difference.
151
+ **Passthrough mode** (`dario proxy --passthrough`) does an OAuth swap and nothing else — no template, no identity, no scrubbing. Use it when the upstream tool already builds a Claude-Code-shaped request on its own and you just need the token auth.
140
152
 
141
- ### Passthrough Mode`--passthrough`
153
+ **Detection scope.** The Claude backend is a per-request layer. Template replay and scrubbing are designed to be indistinguishable from Claude Code at the request level. What they *cannot* defend against is Anthropic's session-level behavioral classifier, which operates on cumulative per-OAuth aggregates (token throughput, conversation depth, streaming duration, inter-arrival timing). The practical answer to that is **pool mode** distributing load across multiple subscriptions so no one account accumulates enough signal to trip anything. See the [FAQ entry](#faq) for the full mechanism.
142
154
 
143
- For tools that need exact Anthropic protocol fidelity with nothing injected. This mode does an OAuth token swap and nothing else: no billing tag, no template, no device identity.
155
+ ### 2. OpenAI-compat backend (v3.6.0+)
156
+
157
+ Any provider that speaks the OpenAI Chat Completions API. Activated by:
144
158
 
145
159
  ```bash
146
- dario proxy --passthrough
160
+ # OpenAI itself (default base URL)
161
+ dario backend add openai --key=sk-proj-...
162
+
163
+ # Groq
164
+ dario backend add groq --key=gsk_... --base-url=https://api.groq.com/openai/v1
165
+
166
+ # OpenRouter
167
+ dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1
168
+
169
+ # Local LiteLLM / vLLM / Ollama openai-compat mode
170
+ dario backend add local --key=anything --base-url=http://127.0.0.1:4000/v1
147
171
  ```
148
172
 
149
- Use it when the upstream tool already builds a Claude-Code-shaped request on its own and you just need the token auth.
173
+ Credentials live at `~/.dario/backends/<name>.json` with mode `0600`.
174
+
175
+ **How it routes.** When the OpenAI-compat backend is configured, each request at `/v1/chat/completions` is checked:
176
+
177
+ | Request model | Route |
178
+ |---|---|
179
+ | `gpt-*`, `o1-*`, `o3-*`, `o4-*`, `chatgpt-*`, `text-davinci-*`, `text-embedding-*` | OpenAI-compat backend |
180
+ | `claude-*` (or `opus` / `sonnet` / `haiku`) | Claude subscription backend |
181
+ | Anything else | Claude backend with OpenAI-compat translation |
150
182
 
151
- ### Detection scope
183
+ Dario's passthrough for the OpenAI-compat backend is literal: client request body goes upstream as-is, only the `Authorization` header is swapped for the configured API key and the URL is pointed at `baseUrl + /chat/completions`. Response body streams back unchanged.
152
184
 
153
- Dario is a **per-request layer**. Every request it sends upstream is designed to be indistinguishable from a Claude Code request, and the per-request scrubbing hardened in v3.4.5 makes that meaningfully harder to fingerprint than it was when v3.0 first shipped. What dario cannot do at the per-request level is defend against Anthropic's session-level behavioral classifiers — those operate on cumulative per-OAuth aggregates (token throughput, conversation depth, streaming duration, inter-arrival timing) and no amount of per-request hardening reaches them. The practical answer to that problem is *distributing* load across multiple subscriptions so no single account accumulates enough signal to trip the classifier — which is what pool mode (below) does.
185
+ ### Coming in a follow-up
186
+
187
+ - **Anthropic → OpenAI request translation** for `/v1/messages` requests with GPT-family model names (tool_use format, streaming delta conversion).
188
+ - **Multiple simultaneous openai-compat backends** with per-model routing rules (`gpt-*` → OpenAI, `llama-*` → Groq, `mixtral-*` → OpenRouter).
189
+ - **Fallback rules.** "If Claude 429s, use Gemini." v3.6.0 ships the routing plumbing; fallback logic layers on top.
154
190
 
155
191
  ---
156
192
 
157
193
  ## Multi-Account Pool Mode
158
194
 
159
- *New in v3.5.0.* Dario can manage multiple Claude subscriptions and route each request to the account with the most headroom. Single-account dario is unchanged and remains the default — pool mode activates **only** when `~/.dario/accounts/` contains 2+ accounts.
195
+ *New in v3.5.0, for the Claude subscription backend.* Dario can manage multiple Claude subscriptions and route each request to the account with the most headroom. Single-account Claude dario is unchanged — pool mode activates **only** when `~/.dario/accounts/` contains 2+ accounts.
160
196
 
161
197
  ```bash
162
- # Add accounts to the pool. Each runs its own OAuth flow.
163
198
  dario accounts add work
164
199
  dario accounts add personal
165
200
  dario accounts add side-project
166
-
167
- # List them
168
201
  dario accounts list
169
-
170
- # Start the proxy — pool mode activates automatically
171
202
  dario proxy
172
203
  ```
173
204
 
174
- ### How it routes
175
-
176
- Each incoming request picks the account with the highest **headroom**:
205
+ Each request picks the account with the highest headroom:
177
206
 
178
207
  ```
179
208
  headroom = 1 - max(util_5h, util_7d)
180
209
  ```
181
210
 
182
- The response's `anthropic-ratelimit-unified-*` headers are parsed back into the pool so the next request sees fresh utilization. An account that returns a 429 is marked `rejected` and routed around until its window resets. When every account is exhausted, incoming requests queue for up to 60 seconds waiting for headroom to reappear, with backoff-aware draining.
211
+ The response's `anthropic-ratelimit-unified-*` headers are parsed back into the pool so the next selection sees fresh utilization. An account that returns a 429 is marked `rejected` and routed around until its window resets. When every account is exhausted, requests queue for up to 60 seconds waiting for headroom to reappear.
183
212
 
184
- Accounts can use different plans — mix Max and Pro accounts freely. The pool doesn't care about tier, only headroom.
213
+ Accounts can mix plans — Max and Pro accounts can sit in the same pool; dario doesn't care about tier, only headroom.
185
214
 
186
- ### Why pool over per-request tricks alone
187
-
188
- Per-request template replay is necessary but not sufficient for multi-agent workloads. Anthropic's classifier operates on cumulative per-OAuth-session aggregates (see the [FAQ entry](#faq) on multi-agent reclassification), and no amount of per-request hardening reaches that layer. The practical answer is *distribution* — spread load so no single account accumulates enough signal to trip anything. Pool mode is the piece that does that, and the headroom-aware selection means you don't have to think about which account is which; dario picks.
189
-
190
- ### Inspection endpoints
215
+ **Pool inspection endpoints:**
191
216
 
192
217
  ```bash
193
- # Live pool snapshot — per-account utilization, claim, status
194
- curl http://localhost:3456/accounts
195
-
196
- # Pool analytics — per-account / per-model stats, burn-rate, exhaustion predictions
197
- curl http://localhost:3456/analytics
218
+ curl http://localhost:3456/accounts # per-account utilization, claim, status
219
+ curl http://localhost:3456/analytics # per-account / per-model stats, burn rate, exhaustion predictions
198
220
  ```
199
221
 
200
- ### Known scope for v3.5.0
201
-
202
- Pool mode v3.5.0 ships **headroom-aware selection across requests**. It does not yet retry a single in-flight request against a different account when that request 429s — that ships in v3.5.1 along with analytics recording wiring. Across-request routing is already effective: a 429 on one request immediately marks that account rejected, and the next request goes somewhere else.
203
-
204
- ---
205
-
206
- ## Dario and askalf
207
-
208
- Dario is fully useful on its own — single-account mode is the default, pool mode (above) scales to as many Claude subscriptions as you want to add, and neither mode requires an account anywhere. Everything dario does is open-source and self-hosted.
209
-
210
- [askalf](https://askalf.org) is the hosted platform built on top of the same OAuth and billing infrastructure, targeting the things a local proxy can't deliver by design:
211
-
212
- | | dario | askalf |
213
- |---|---|---|
214
- | **Accounts** | 1 (single) or N (pool mode) | Managed pool, no setup |
215
- | **Rate limits** | Distributed across your own pool | Distributed across the hosted fleet |
216
- | **Browser / desktop control** | No | Yes — full computer use |
217
- | **Scheduling** | No | Cron, webhooks, triggers |
218
- | **Persistent memory** | No | Per-agent context and state |
219
- | **Hosted dashboard** | No | Yes |
220
- | **Runs where** | Your machine | Hosted |
221
- | **Price** | Free | Paid |
222
-
223
- Pool mode in dario covers the "I want multi-account routing on my own machine with my own subscriptions" case. askalf covers the "I want someone else to run this, with a dashboard, and 24/7 fleet capabilities my own machine can't give me" case. Dario is and will remain open-source and free.
224
-
225
- **[Join the askalf waitlist →](https://askalf.org)**
222
+ **Scope.** v3.5.0 ships headroom-aware selection *across* requests — a 429 on one request marks the account rejected and the next request goes to a different one. Retrying a single in-flight request against a different account when that request 429s (inside-request failover) ships in v3.5.1 along with analytics recording wiring.
226
223
 
227
224
  ---
228
225
 
229
226
  ## Commands
230
227
 
231
228
  | Command | Description |
232
- |---------|-------------|
233
- | `dario login` | Log in (detects CC credentials or runs its own OAuth flow) |
229
+ |---|---|
230
+ | `dario login` | Log in to the Claude backend (detects CC credentials or runs its own OAuth flow) |
234
231
  | `dario proxy` | Start the local API proxy on port 3456 |
235
- | `dario status` | Show OAuth token health and expiry |
236
- | `dario refresh` | Force an immediate token refresh |
237
- | `dario logout` | Delete stored credentials |
232
+ | `dario status` | Show Claude backend OAuth token health and expiry |
233
+ | `dario refresh` | Force an immediate Claude token refresh |
234
+ | `dario logout` | Delete stored Claude credentials |
238
235
  | `dario accounts list` | List accounts in the multi-account pool |
239
- | `dario accounts add <alias>` | Add a new account to the pool (runs OAuth flow) |
236
+ | `dario accounts add <alias>` | Add a Claude account to the pool (runs OAuth flow) |
240
237
  | `dario accounts remove <alias>` | Remove an account from the pool |
238
+ | `dario backend list` | List configured OpenAI-compat backends |
239
+ | `dario backend add <name> --key=<key> [--base-url=<url>]` | Add an OpenAI-compat backend |
240
+ | `dario backend remove <name>` | Remove an OpenAI-compat backend |
241
241
  | `dario help` | Full command reference |
242
242
 
243
243
  ### Proxy options
244
244
 
245
245
  | Flag / env | Description | Default |
246
246
  |---|---|---|
247
- | `--passthrough` / `--thin` | Thin proxy — OAuth swap only, no template injection | off |
248
- | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC tools | off |
249
- | `--model=<name>` | Force a model (`opus`, `sonnet`, `haiku`, or full ID) | passthrough |
247
+ | `--passthrough` / `--thin` | Thin proxy for the Claude backend — OAuth swap only, no template injection | off |
248
+ | `--preserve-tools` / `--keep-tools` | Keep client tool schemas instead of remapping to CC tools (Claude backend) | off |
249
+ | `--model=<name>` | Force a model (`opus`, `sonnet`, `haiku`, or full ID). Applies to the Claude backend. | passthrough |
250
250
  | `--port=<n>` | Port to listen on | `3456` |
251
251
  | `--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` |
252
252
  | `--verbose` / `-v` | Log every request | off |
253
253
  | `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) |
254
- | `DARIO_CORS_ORIGIN` | Override the browser CORS `Access-Control-Allow-Origin`. Useful for browser clients reaching dario over a mesh network. | `http://localhost:${port}` |
254
+ | `DARIO_CORS_ORIGIN` | Override browser CORS origin | `http://localhost:${port}` |
255
255
  | `DARIO_NO_BUN` | Disable automatic Bun relaunch | unset |
256
- | `DARIO_MIN_INTERVAL_MS` | Minimum ms between requests (rate governor) | `500` |
256
+ | `DARIO_MIN_INTERVAL_MS` | Minimum ms between Claude-backend requests (rate governor) | `500` |
257
257
  | `DARIO_CC_PATH` | Override path to the Claude Code binary for OAuth detection | auto-detect |
258
258
 
259
259
  ---
260
260
 
261
261
  ## Usage
262
262
 
263
- ### Python
263
+ ### Python (Anthropic SDK)
264
264
 
265
265
  ```python
266
266
  import anthropic
@@ -278,6 +278,29 @@ msg = client.messages.create(
278
278
  print(msg.content[0].text)
279
279
  ```
280
280
 
281
+ ### Python (OpenAI SDK — same proxy, different provider)
282
+
283
+ ```python
284
+ from openai import OpenAI
285
+
286
+ client = OpenAI(
287
+ base_url="http://localhost:3456/v1",
288
+ api_key="dario",
289
+ )
290
+
291
+ # gpt-4o routes to the configured OpenAI backend
292
+ msg = client.chat.completions.create(
293
+ model="gpt-4o",
294
+ messages=[{"role": "user", "content": "Hello!"}],
295
+ )
296
+
297
+ # claude-opus-4-6 routes to the Claude subscription backend — same SDK, same URL
298
+ claude_msg = client.chat.completions.create(
299
+ model="claude-opus-4-6",
300
+ messages=[{"role": "user", "content": "Hello!"}],
301
+ )
302
+ ```
303
+
281
304
  ### TypeScript / Node.js
282
305
 
283
306
  ```typescript
@@ -295,38 +318,44 @@ const msg = await client.messages.create({
295
318
  });
296
319
  ```
297
320
 
298
- ### OpenAI-compatible (Cursor, Continue, LiteLLM, Aider, …)
321
+ ### OpenAI-compatible tools (Cursor, Continue, Aider, LiteLLM, …)
299
322
 
300
323
  ```bash
301
324
  export OPENAI_BASE_URL=http://localhost:3456/v1
302
325
  export OPENAI_API_KEY=dario
303
326
  ```
304
327
 
305
- Any tool that accepts an OpenAI base URL works as-is. Claude model names pass through directly; GPT-style names (`gpt-4`, `gpt-5.4`, etc.) map to their closest Claude equivalents so tools with hardcoded OpenAI model lists work without code changes.
328
+ Any tool that accepts an OpenAI base URL works. Use Claude model names (`claude-opus-4-6`, `opus`, `sonnet`, `haiku`) for the Claude backend, or GPT-family names for the configured OpenAI-compat backend.
306
329
 
307
330
  ### curl
308
331
 
309
332
  ```bash
333
+ # Claude backend via Anthropic format
310
334
  curl http://localhost:3456/v1/messages \
311
335
  -H "Content-Type: application/json" \
312
336
  -H "anthropic-version: 2023-06-01" \
313
337
  -d '{"model":"claude-opus-4-6","max_tokens":1024,"messages":[{"role":"user","content":"Hello!"}]}'
338
+
339
+ # OpenAI backend via OpenAI format
340
+ curl http://localhost:3456/v1/chat/completions \
341
+ -H "Content-Type: application/json" \
342
+ -H "Authorization: Bearer dario" \
343
+ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello!"}]}'
314
344
  ```
315
345
 
316
346
  ### Streaming, tool use, prompt caching, extended thinking
317
347
 
318
- All supported in both Anthropic and OpenAI SSE formats. Tool-use streaming emits `input_json_delta` events. Prompt caching works as-is. Extended thinking is routed through `reasoning_effort` in OpenAI format or the native `thinking` field in Anthropic format.
348
+ All supported. Claude backend: full Anthropic SSE format plus OpenAI-SSE translation for tool_use streaming. OpenAI-compat backend: streaming body forwarded byte-for-byte.
319
349
 
320
350
  ### Library mode
321
351
 
322
- Dario is also importable:
323
-
324
352
  ```typescript
325
- import { startProxy, getAccessToken, getStatus } from "@askalf/dario";
353
+ import { startProxy, getAccessToken, getStatus, listBackends } from "@askalf/dario";
326
354
 
327
355
  await startProxy({ port: 3456, verbose: true });
328
356
  const token = await getAccessToken();
329
357
  const status = await getStatus();
358
+ const backends = await listBackends();
330
359
  ```
331
360
 
332
361
  ### Health check
@@ -335,43 +364,36 @@ const status = await getStatus();
335
364
  curl http://localhost:3456/health
336
365
  ```
337
366
 
338
- ```json
339
- {
340
- "status": "ok",
341
- "oauth": "healthy",
342
- "expiresIn": "11h 42m",
343
- "requests": 47
344
- }
345
- ```
346
-
347
367
  ---
348
368
 
349
369
  ## Endpoints
350
370
 
351
371
  | Path | Description |
352
- |------|-------------|
353
- | `POST /v1/messages` | Anthropic Messages API |
354
- | `POST /v1/chat/completions` | OpenAI-compatible Chat API |
355
- | `GET /v1/models` | Model list (works with both SDKs) |
372
+ |---|---|
373
+ | `POST /v1/messages` | Anthropic Messages API (Claude backend) |
374
+ | `POST /v1/chat/completions` | OpenAI-compatible Chat API (routes by model name) |
375
+ | `GET /v1/models` | Model list (Claude models OpenAI models come from the OpenAI backend directly) |
356
376
  | `GET /health` | Proxy health + OAuth status + request count |
357
- | `GET /status` | Detailed OAuth token status |
377
+ | `GET /status` | Detailed Claude OAuth token status |
378
+ | `GET /accounts` | Pool snapshot (pool mode only) |
379
+ | `GET /analytics` | Per-account / per-model stats, burn rate, exhaustion predictions (pool mode only) |
358
380
 
359
381
  ---
360
382
 
361
383
  ## Trust & Transparency
362
384
 
363
- Dario handles your OAuth tokens. Here's why you can trust it:
385
+ Dario handles your OAuth tokens and API keys locally. Here's why you can trust it:
364
386
 
365
387
  | Signal | Status |
366
388
  |---|---|
367
- | **Source code** | ~2,000 lines of TypeScript across 7 files — small enough to audit in one sitting |
389
+ | **Source code** | ~2,500 lines of TypeScript across 10 files — small enough to audit in one sitting |
368
390
  | **Dependencies** | 0 runtime dependencies. Verify: `npm ls --production` |
369
391
  | **npm provenance** | Every release is [SLSA-attested](https://www.npmjs.com/package/@askalf/dario) via GitHub Actions |
370
392
  | **Security scanning** | [CodeQL](https://github.com/askalf/dario/actions/workflows/codeql.yml) runs on every push and weekly |
371
- | **Credential handling** | Tokens never logged, redacted from errors, stored with `0600` permissions |
372
- | **OAuth flow** | PKCE (Proof Key for Code Exchange) no client secret |
373
- | **Network scope** | Binds to `127.0.0.1` by default. `--host` allows LAN/mesh with `DARIO_API_KEY` gating. Upstream traffic goes only to `api.anthropic.com` over HTTPS |
374
- | **SSRF protection** | Only `/v1/messages` and `/v1/complete` proxy upstream — hardcoded allowlist |
393
+ | **Credential handling** | Tokens and API keys never logged, redacted from errors, stored with `0600` permissions |
394
+ | **OAuth flow** | PKCE (Proof Key for Code Exchange), no client secret |
395
+ | **Network scope** | Binds to `127.0.0.1` by default. `--host` allows LAN/mesh with `DARIO_API_KEY` gating. Upstream traffic goes only to the configured backend target URLs over HTTPS |
396
+ | **SSRF protection** | `/v1/messages` hits `api.anthropic.com` only; `/v1/chat/completions` hits the configured backend `baseUrl` only — hardcoded allowlist |
375
397
  | **Telemetry** | None. Zero analytics, tracking, or data collection |
376
398
  | **Audit trail** | [CHANGELOG.md](CHANGELOG.md) documents every release |
377
399
 
@@ -388,21 +410,21 @@ cd $(npm root -g)/@askalf/dario && npm ls --production
388
410
  ## FAQ
389
411
 
390
412
  **Does this violate Anthropic's terms of service?**
391
- Dario uses your existing Claude Code credentials with the same OAuth tokens CC uses. It authenticates you as you, with your subscription, through Anthropic's official API endpoints.
413
+ Dario's Claude backend uses your existing Claude Code credentials with the same OAuth tokens CC uses. It authenticates you as you, with your subscription, through Anthropic's official API endpoints.
392
414
 
393
- **What subscription plans work?**
415
+ **What subscription plans work on the Claude backend?**
394
416
  Claude Max and Claude Pro. Any plan that lets you use Claude Code.
395
417
 
396
418
  **Does it work with Team / Enterprise?**
397
419
  Should work if your plan includes Claude Code access. Not widely tested yet — open an issue with results.
398
420
 
399
421
  **Do I need Claude Code installed?**
400
- Recommended, not required. With CC installed, `dario login` picks up your credentials automatically. Without CC, dario runs its own OAuth flow against Anthropic's authorize endpoint.
422
+ Recommended for the Claude backend, not strictly required. With CC installed, `dario login` picks up your credentials automatically. Without CC, dario runs its own OAuth flow against Anthropic's authorize endpoint.
401
423
 
402
424
  **Do I need Bun?**
403
- Optional, recommended. Dario auto-relaunches under Bun when it's available so the TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js and works fine; the TLS fingerprint is the only difference. Install: `curl -fsSL https://bun.sh/install | bash`.
425
+ Optional, recommended for Claude-backend requests. Dario auto-relaunches under Bun when available so the TLS fingerprint matches CC's runtime. Without Bun, dario runs on Node.js and works fine; the TLS fingerprint is the only difference.
404
426
 
405
- **First time setup on a fresh account.**
427
+ **First time setup on a fresh Claude account.**
406
428
  If dario is the first thing you run against a brand-new Claude account, prime the account with a few real Claude Code commands first:
407
429
  ```bash
408
430
  claude --print "hello"
@@ -410,17 +432,20 @@ claude --print "hello"
410
432
  ```
411
433
  This establishes a session baseline. Without priming, brand-new accounts occasionally see billing classification issues on first use.
412
434
 
413
- **What happens when my token expires?**
414
- Dario auto-refreshes tokens 30 minutes before expiry. `dario refresh` forces an immediate refresh if something goes wrong.
415
-
416
- **What happens when Anthropic rotates the OAuth client_id or URL?**
417
- 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 run — no dario release needed. Cache at `~/.dario/cc-oauth-cache-v3.json`, keyed by the CC binary fingerprint. If CC isn't installed, dario falls back to hardcoded CC 2.1.104 prod values.
435
+ **What happens when Anthropic rotates the OAuth config?**
436
+ Dario auto-detects OAuth config from the installed Claude Code binary. When CC ships a new version with rotated values, dario picks them up on the next run. Cache at `~/.dario/cc-oauth-cache-v3.json`, keyed by the CC binary fingerprint. Falls back to hardcoded CC 2.1.104 prod values if CC isn't installed.
418
437
 
419
- **I'm hitting rate limits. What do I do?**
420
- Claude subscriptions have rolling 5-hour and 7-day usage windows. Check utilization with Claude Code's `/usage` command or the [statusline](https://code.claude.com/docs/en/statusline). Dario's rate-limit errors include utilization percentages and reset times so you can see exactly when capacity returns.
438
+ **I'm hitting rate limits on the Claude backend. What do I do?**
439
+ Claude subscriptions have rolling 5-hour and 7-day usage windows. Check utilization with Claude Code's `/usage` command or the [statusline](https://code.claude.com/docs/en/statusline). For multi-agent workloads, add more accounts and let pool mode distribute the load: `dario accounts add <alias>`.
421
440
 
422
441
  **My multi-agent workload is getting reclassified to overage even though dario template-replays per request. Why?**
423
- 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 and v3.4.5 hardening that landed as a result. For the session-layer shaping itselfmulti-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.
442
+ Reclassification at high agent volume is not a per-request problem. Anthropic's classifier operates on cumulative per-OAuth-session aggregates — token throughput, conversation depth, streaming duration, inter-arrival timing, thinking-block volume. Dario's Claude backend 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 v3.4.3/v3.4.5 hardening that landed as a result. The practical answer at the dario layer is **pool mode** distribute load across multiple subscriptions so no single account accumulates enough signal to trip anything. See [Multi-Account Pool Mode](#multi-account-pool-mode).
443
+
444
+ **Can I route non-OpenAI providers through dario?**
445
+ Yes — anything that speaks the OpenAI Chat Completions API. `dario backend add groq --key=... --base-url=https://api.groq.com/openai/v1`, `dario backend add openrouter --key=... --base-url=https://openrouter.ai/api/v1`, or point at a local LiteLLM / vLLM / Ollama-openai-compat server with `--base-url=http://localhost:4000/v1`. v3.6.0 supports one active OpenAI-compat backend at a time; per-model routing to multiple OpenAI-compat backends ships in a follow-up.
446
+
447
+ **Does dario work with only the OpenAI backend, no Claude subscription?**
448
+ Yes. Don't run `dario login`, just run `dario backend add openai --key=...` and `dario proxy`. Claude-backend requests will return an authentication error; OpenAI-compat requests will work normally. Dario becomes a local OpenAI-compat shim with no Claude involvement.
424
449
 
425
450
  **Why "dario"?**
426
451
  It's a name, not an acronym. Don't overthink it.
@@ -441,15 +466,19 @@ Longer-form writing on how dario works and why it works that way:
441
466
 
442
467
  ## Contributing
443
468
 
444
- PRs welcome. The codebase is ~2,000 lines of TypeScript across 7 files:
469
+ PRs welcome. The codebase is ~2,500 lines of TypeScript across 10 files:
445
470
 
446
471
  | File | Purpose |
447
472
  |---|---|
448
- | `src/proxy.ts` | HTTP proxy server, rate governor, billing tag, response forwarding |
449
- | `src/cc-template.ts` | Template engine, tool mapping, orchestration & framework scrubbing |
473
+ | `src/proxy.ts` | HTTP proxy server, request handler, rate governor, Claude backend dispatch |
474
+ | `src/cc-template.ts` | CC request template engine, tool mapping, orchestration & framework scrubbing |
450
475
  | `src/cc-template-data.json` | CC request template data (25 tools, 25KB system prompt) |
451
476
  | `src/cc-oauth-detect.ts` | OAuth config auto-detection from the installed CC binary |
452
- | `src/oauth.ts` | Token storage, PKCE flow, auto-refresh |
477
+ | `src/oauth.ts` | Single-account token storage, PKCE flow, auto-refresh |
478
+ | `src/accounts.ts` | Multi-account credential storage and independent OAuth lifecycle |
479
+ | `src/pool.ts` | Account pool, headroom-aware routing, failover target selection |
480
+ | `src/analytics.ts` | Rolling request history, per-account / per-model stats, burn-rate |
481
+ | `src/openai-backend.ts` | OpenAI-compat backend credential storage and request forwarder |
453
482
  | `src/cli.ts` | CLI entry point, command routing, Bun auto-relaunch |
454
483
  | `src/index.ts` | Library exports |
455
484
 
package/dist/cli.js CHANGED
@@ -38,6 +38,7 @@ import { homedir } from 'node:os';
38
38
  import { startAutoOAuthFlow, getStatus, refreshTokens, loadCredentials } from './oauth.js';
39
39
  import { startProxy, sanitizeError } from './proxy.js';
40
40
  import { listAccountAliases, loadAllAccounts, addAccountViaOAuth, removeAccount } from './accounts.js';
41
+ import { listBackends, saveBackend, removeBackend } from './openai-backend.js';
41
42
  const args = process.argv.slice(2);
42
43
  const command = args[0] ?? 'proxy';
43
44
  async function login() {
@@ -251,6 +252,96 @@ async function accounts() {
251
252
  console.error('Usage: dario accounts [list|add <alias>|remove <alias>]');
252
253
  process.exit(1);
253
254
  }
255
+ async function backend() {
256
+ const sub = args[1];
257
+ if (!sub || sub === 'list') {
258
+ const all = await listBackends();
259
+ console.log('');
260
+ console.log(' dario — Backends');
261
+ console.log(' ────────────────');
262
+ console.log('');
263
+ if (all.length === 0) {
264
+ console.log(' No secondary backends configured.');
265
+ console.log('');
266
+ console.log(' Dario\'s Claude subscription path runs unchanged. To add an');
267
+ console.log(' OpenAI-compat backend (OpenAI, OpenRouter, Groq, local LiteLLM,');
268
+ console.log(' etc.), run:');
269
+ console.log(' dario backend add openai --key=sk-...');
270
+ console.log(' dario backend add openai --key=sk-... --base-url=https://api.groq.com/openai/v1');
271
+ console.log('');
272
+ return;
273
+ }
274
+ console.log(` ${all.length} backend${all.length === 1 ? '' : 's'} configured`);
275
+ console.log('');
276
+ for (const b of all) {
277
+ const redacted = b.apiKey.length > 8
278
+ ? `${b.apiKey.slice(0, 3)}...${b.apiKey.slice(-4)}`
279
+ : '***';
280
+ console.log(` ${b.name.padEnd(16)} ${b.provider.padEnd(10)} ${b.baseUrl.padEnd(40)} ${redacted}`);
281
+ }
282
+ console.log('');
283
+ return;
284
+ }
285
+ if (sub === 'add') {
286
+ const name = args[2];
287
+ if (!name || name.startsWith('--')) {
288
+ console.error('');
289
+ console.error(' Usage: dario backend add <name> --key=<api-key> [--base-url=<url>]');
290
+ console.error('');
291
+ console.error(' Examples:');
292
+ console.error(' dario backend add openai --key=sk-proj-...');
293
+ console.error(' dario backend add groq --key=gsk_... --base-url=https://api.groq.com/openai/v1');
294
+ console.error(' dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1');
295
+ console.error('');
296
+ process.exit(1);
297
+ }
298
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
299
+ console.error('[dario] Invalid backend name. Use letters, numbers, dot, underscore, dash only.');
300
+ process.exit(1);
301
+ }
302
+ const keyArg = args.find(a => a.startsWith('--key='));
303
+ const baseUrlArg = args.find(a => a.startsWith('--base-url='));
304
+ const apiKey = keyArg ? keyArg.split('=').slice(1).join('=') : '';
305
+ const baseUrl = baseUrlArg ? baseUrlArg.split('=').slice(1).join('=') : 'https://api.openai.com/v1';
306
+ if (!apiKey) {
307
+ console.error('[dario] --key=<api-key> is required.');
308
+ process.exit(1);
309
+ }
310
+ const creds = {
311
+ provider: 'openai', // v3.6.0: only openai-compat backends are supported
312
+ name,
313
+ apiKey,
314
+ baseUrl,
315
+ };
316
+ await saveBackend(creds);
317
+ console.log('');
318
+ console.log(` Backend "${name}" added (openai-compat, ${baseUrl}).`);
319
+ console.log(' Restart \`dario proxy\` to pick up the new routing.');
320
+ console.log('');
321
+ return;
322
+ }
323
+ if (sub === 'remove' || sub === 'rm') {
324
+ const name = args[2];
325
+ if (!name) {
326
+ console.error('');
327
+ console.error(' Usage: dario backend remove <name>');
328
+ console.error('');
329
+ process.exit(1);
330
+ }
331
+ const ok = await removeBackend(name);
332
+ if (ok) {
333
+ console.log(`[dario] Backend "${name}" removed.`);
334
+ }
335
+ else {
336
+ console.error(`[dario] No backend "${name}" found.`);
337
+ process.exit(1);
338
+ }
339
+ return;
340
+ }
341
+ console.error(`[dario] Unknown backend subcommand: ${sub}`);
342
+ console.error('Usage: dario backend [list|add <name> --key=...|remove <name>]');
343
+ process.exit(1);
344
+ }
254
345
  async function help() {
255
346
  console.log(`
256
347
  dario — Use your Claude subscription as an API.
@@ -264,6 +355,10 @@ async function help() {
264
355
  dario accounts list List accounts in the multi-account pool
265
356
  dario accounts add NAME Add a new account to the pool (runs OAuth flow)
266
357
  dario accounts remove N Remove an account from the pool
358
+ dario backend list List configured OpenAI-compat backends
359
+ dario backend add NAME --key=sk-... [--base-url=...]
360
+ Add an OpenAI-compat backend (OpenAI, OpenRouter, Groq, etc.)
361
+ dario backend remove N Remove an OpenAI-compat backend
267
362
 
268
363
  Proxy options:
269
364
  --model=MODEL Force a model for all requests
@@ -319,6 +414,7 @@ const commands = {
319
414
  refresh,
320
415
  logout,
321
416
  accounts,
417
+ backend,
322
418
  help,
323
419
  version,
324
420
  '--help': help,
package/dist/index.d.ts CHANGED
@@ -13,3 +13,5 @@ export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAc
13
13
  export type { AccountCredentials } from './accounts.js';
14
14
  export { Analytics } from './analytics.js';
15
15
  export type { RequestRecord, AnalyticsSummary } from './analytics.js';
16
+ export { listBackends, saveBackend, removeBackend, getOpenAIBackend, isOpenAIModel, } from './openai-backend.js';
17
+ export type { BackendCredentials } from './openai-backend.js';
package/dist/index.js CHANGED
@@ -12,3 +12,8 @@ export { startProxy, sanitizeError } from './proxy.js';
12
12
  export { AccountPool, parseRateLimits } from './pool.js';
13
13
  export { listAccountAliases, loadAccount, loadAllAccounts, saveAccount, removeAccount, refreshAccountToken, addAccountViaOAuth, getAccountsDir, } from './accounts.js';
14
14
  export { Analytics } from './analytics.js';
15
+ // Multi-provider backends (v3.6.0+). Secondary OpenAI-compat providers
16
+ // (OpenAI, OpenRouter, Groq, local LiteLLM, etc.) configured via
17
+ // `dario backend add`. The Claude subscription path is unchanged — these
18
+ // are additional routes for non-Claude models.
19
+ export { listBackends, saveBackend, removeBackend, getOpenAIBackend, isOpenAIModel, } from './openai-backend.js';
@@ -0,0 +1,19 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ export interface BackendCredentials {
3
+ provider: string;
4
+ name: string;
5
+ apiKey: string;
6
+ baseUrl: string;
7
+ }
8
+ export declare function listBackends(): Promise<BackendCredentials[]>;
9
+ export declare function saveBackend(creds: BackendCredentials): Promise<void>;
10
+ export declare function removeBackend(name: string): Promise<boolean>;
11
+ /** Get the first openai-compat backend (v3.6.0 supports exactly one). */
12
+ export declare function getOpenAIBackend(): Promise<BackendCredentials | null>;
13
+ export declare function isOpenAIModel(model: string): boolean;
14
+ /**
15
+ * Forward a client request to the configured OpenAI-compat backend.
16
+ * Pass-through: the client is already speaking OpenAI format, we just swap
17
+ * the API key and the target URL. No template, no identity, no scrubbing.
18
+ */
19
+ export declare function forwardToOpenAI(req: IncomingMessage, res: ServerResponse, body: Buffer, backend: BackendCredentials, corsOrigin: string, securityHeaders: Record<string, string>, upstreamTimeoutMs: number, verbose: boolean): Promise<void>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * OpenAI-compatible backend.
3
+ *
4
+ * When `dario backend add openai --key=sk-...` has been run, requests to
5
+ * `/v1/chat/completions` with a GPT-style model name are forwarded to the
6
+ * configured OpenAI-compat endpoint instead of being routed through the
7
+ * Claude template path. The Claude backend is unchanged.
8
+ *
9
+ * The `--base-url` flag is accepted so the same command works for any
10
+ * OpenAI-compatible provider (OpenAI, OpenRouter, Groq, LiteLLM, a local
11
+ * Ollama exposing OpenAI compat, etc.). Only one openai-compat backend can
12
+ * be active at a time in v3.6.0; multi-backend-per-provider routing lands
13
+ * in a follow-up release.
14
+ */
15
+ import { readFile, writeFile, mkdir, unlink, readdir } from 'node:fs/promises';
16
+ import { join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ const DARIO_DIR = join(homedir(), '.dario');
19
+ const BACKENDS_DIR = join(DARIO_DIR, 'backends');
20
+ async function ensureDir() {
21
+ await mkdir(BACKENDS_DIR, { recursive: true, mode: 0o700 });
22
+ }
23
+ export async function listBackends() {
24
+ try {
25
+ await ensureDir();
26
+ const files = await readdir(BACKENDS_DIR);
27
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
28
+ const results = [];
29
+ for (const f of jsonFiles) {
30
+ try {
31
+ const raw = await readFile(join(BACKENDS_DIR, f), 'utf-8');
32
+ results.push(JSON.parse(raw));
33
+ }
34
+ catch { /* skip unreadable */ }
35
+ }
36
+ return results;
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
42
+ export async function saveBackend(creds) {
43
+ await ensureDir();
44
+ const path = join(BACKENDS_DIR, `${creds.name}.json`);
45
+ await writeFile(path, JSON.stringify(creds, null, 2), { mode: 0o600 });
46
+ }
47
+ export async function removeBackend(name) {
48
+ const path = join(BACKENDS_DIR, `${name}.json`);
49
+ try {
50
+ await unlink(path);
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /** Get the first openai-compat backend (v3.6.0 supports exactly one). */
58
+ export async function getOpenAIBackend() {
59
+ const all = await listBackends();
60
+ return all.find(b => b.provider === 'openai') ?? null;
61
+ }
62
+ // Model names that should route to the OpenAI backend when one is configured.
63
+ // Deliberately narrow — OpenAI and reasoning-series only. Custom GPT-shaped
64
+ // names from other providers (llama-*, mixtral-*) don't match by default;
65
+ // users pass them through as-is on the OpenAI-compat endpoint and they'll
66
+ // reach the configured baseUrl, which is correct for OpenRouter/Groq/etc.
67
+ const OPENAI_MODEL_PATTERNS = [
68
+ /^gpt-/i,
69
+ /^o1-/i,
70
+ /^o3-/i,
71
+ /^o4-/i,
72
+ /^chatgpt-/i,
73
+ /^text-davinci/i,
74
+ /^text-embedding-/i,
75
+ ];
76
+ export function isOpenAIModel(model) {
77
+ return OPENAI_MODEL_PATTERNS.some(p => p.test(model));
78
+ }
79
+ /**
80
+ * Forward a client request to the configured OpenAI-compat backend.
81
+ * Pass-through: the client is already speaking OpenAI format, we just swap
82
+ * the API key and the target URL. No template, no identity, no scrubbing.
83
+ */
84
+ export async function forwardToOpenAI(req, res, body, backend, corsOrigin, securityHeaders, upstreamTimeoutMs, verbose) {
85
+ const target = `${backend.baseUrl.replace(/\/$/, '')}/chat/completions`;
86
+ const clientBeta = req.headers['anthropic-beta'];
87
+ // Headers: drop anything Anthropic-specific, keep only the essentials
88
+ // OpenAI-compat endpoints care about. Streaming is driven by the body, not
89
+ // a header, so we don't need to parse it here.
90
+ const headers = {
91
+ 'Content-Type': 'application/json',
92
+ 'Authorization': `Bearer ${backend.apiKey}`,
93
+ 'Accept': req.headers.accept?.toString() ?? 'application/json',
94
+ };
95
+ // Some openai-compat providers (OpenRouter) want their own custom headers
96
+ // for attribution. If the client sent an x-title or http-referer, forward
97
+ // those through so the upstream provider sees them.
98
+ for (const h of ['x-title', 'http-referer', 'x-openrouter-app']) {
99
+ const v = req.headers[h];
100
+ if (typeof v === 'string')
101
+ headers[h] = v;
102
+ }
103
+ // Drop Anthropic-specific headers entirely
104
+ void clientBeta;
105
+ const abort = new AbortController();
106
+ const timeout = setTimeout(() => abort.abort(), upstreamTimeoutMs);
107
+ try {
108
+ if (verbose) {
109
+ console.log(`[dario] → openai backend: ${target}`);
110
+ }
111
+ const upstream = await fetch(target, {
112
+ method: 'POST',
113
+ headers,
114
+ body: body.length > 0 ? new Uint8Array(body) : undefined,
115
+ signal: abort.signal,
116
+ });
117
+ const respHeaders = {
118
+ 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
119
+ 'Access-Control-Allow-Origin': corsOrigin,
120
+ ...securityHeaders,
121
+ };
122
+ // Forward rate-limit + request-id headers from the upstream
123
+ for (const [key, value] of upstream.headers.entries()) {
124
+ if (key.startsWith('x-ratelimit') ||
125
+ key.startsWith('openai-') ||
126
+ key === 'request-id' ||
127
+ key === 'x-request-id') {
128
+ respHeaders[key] = value;
129
+ }
130
+ }
131
+ res.writeHead(upstream.status, respHeaders);
132
+ if (upstream.body) {
133
+ const reader = upstream.body.getReader();
134
+ try {
135
+ while (true) {
136
+ const { done, value } = await reader.read();
137
+ if (done)
138
+ break;
139
+ if (value)
140
+ res.write(Buffer.from(value));
141
+ }
142
+ }
143
+ finally {
144
+ reader.releaseLock();
145
+ }
146
+ }
147
+ res.end();
148
+ }
149
+ catch (err) {
150
+ clearTimeout(timeout);
151
+ if (!res.headersSent) {
152
+ res.writeHead(502, { 'Content-Type': 'application/json', ...securityHeaders });
153
+ res.end(JSON.stringify({
154
+ error: 'Upstream OpenAI-compat backend error',
155
+ message: err instanceof Error ? err.message : String(err),
156
+ backend: backend.name,
157
+ }));
158
+ }
159
+ else {
160
+ try {
161
+ res.end();
162
+ }
163
+ catch { /* already closed */ }
164
+ }
165
+ return;
166
+ }
167
+ finally {
168
+ clearTimeout(timeout);
169
+ }
170
+ }
package/dist/proxy.js CHANGED
@@ -10,6 +10,7 @@ import { buildCCRequest, reverseMapResponse } from './cc-template.js';
10
10
  import { AccountPool, parseRateLimits } from './pool.js';
11
11
  import { Analytics } from './analytics.js';
12
12
  import { loadAllAccounts, loadAccount, refreshAccountToken } from './accounts.js';
13
+ import { getOpenAIBackend, isOpenAIModel, forwardToOpenAI } from './openai-backend.js';
13
14
  const ANTHROPIC_API = 'https://api.anthropic.com';
14
15
  const DEFAULT_PORT = 3456;
15
16
  const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
@@ -324,6 +325,16 @@ export async function startProxy(opts = {}) {
324
325
  const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
325
326
  const verbose = opts.verbose ?? false;
326
327
  const passthrough = opts.passthrough ?? false;
328
+ // Multi-provider backends (v3.6.0+). Loaded once at startup; the CLI
329
+ // `dario backend add openai --key=…` writes to ~/.dario/backends/.
330
+ // Routing: a GPT-family model arriving on /v1/chat/completions is
331
+ // dispatched to the openai-compat backend when one is configured,
332
+ // otherwise it falls through to the existing Claude-side handling
333
+ // (which used to map gpt-* names to Claude equivalents).
334
+ let openaiBackend = await getOpenAIBackend();
335
+ if (openaiBackend) {
336
+ console.log(` OpenAI-compat backend: ${openaiBackend.name} → ${openaiBackend.baseUrl}`);
337
+ }
327
338
  // Multi-account pool — activated when ~/.dario/accounts/ has 2+ entries.
328
339
  // Single-account dario keeps its existing code path unchanged.
329
340
  const accountsList = await loadAllAccounts();
@@ -590,6 +601,27 @@ export async function startProxy(opts = {}) {
590
601
  clearTimeout(bodyTimeout);
591
602
  }
592
603
  const body = Buffer.concat(chunks);
604
+ // Multi-provider routing (v3.6.0+). When an OpenAI-compat backend is
605
+ // configured and the request is on /v1/chat/completions with a
606
+ // GPT-family model, forward it straight through to the backend
607
+ // instead of running it through the Claude template path. Requests
608
+ // on /v1/messages or with Claude-family models fall through to
609
+ // existing behavior.
610
+ if (openaiBackend && isOpenAI && body.length > 0) {
611
+ try {
612
+ const peek = JSON.parse(body.toString());
613
+ const rawModel = (peek.model || '').toString();
614
+ if (rawModel && isOpenAIModel(rawModel)) {
615
+ if (verbose) {
616
+ console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → openai backend`);
617
+ }
618
+ requestCount++;
619
+ await forwardToOpenAI(req, res, body, openaiBackend, corsOrigin, SECURITY_HEADERS, UPSTREAM_TIMEOUT_MS, verbose);
620
+ return;
621
+ }
622
+ }
623
+ catch { /* not JSON — fall through to existing path */ }
624
+ }
593
625
  // Parse body once, apply OpenAI translation, model override, and sanitization
594
626
  let finalBody = body.length > 0 ? body : undefined;
595
627
  let ccToolMap = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.5.0",
4
- "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
3
+ "version": "3.6.1",
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": {
7
7
  "dario": "./dist/cli.js"
@@ -31,16 +31,23 @@
31
31
  "fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
32
32
  },
33
33
  "keywords": [
34
+ "llm",
35
+ "llm-router",
36
+ "multi-provider",
37
+ "openai-compat",
38
+ "openai",
39
+ "openrouter",
40
+ "groq",
41
+ "litellm",
42
+ "ollama",
34
43
  "claude",
35
44
  "anthropic",
36
45
  "oauth",
37
46
  "proxy",
38
47
  "api",
39
- "bridge",
40
48
  "subscription",
41
49
  "claude-max",
42
50
  "claude-pro",
43
- "llm",
44
51
  "ai",
45
52
  "cli",
46
53
  "developer-tools"