@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 +195 -166
- package/dist/cli.js +96 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/openai-backend.d.ts +19 -0
- package/dist/openai-backend.js +170 -0
- package/dist/proxy.js +32 -0
- package/package.json +11 -4
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>
|
|
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> •
|
|
16
16
|
<a href="#who-this-is-for">Who it's for</a> •
|
|
17
|
+
<a href="#backends">Backends</a> •
|
|
17
18
|
<a href="#why-switch">Why switch</a> •
|
|
18
|
-
<a href="#how-it-works">How it works</a> •
|
|
19
|
-
<a href="#from-standalone-to-askalf">Standalone → askalf</a> •
|
|
20
19
|
<a href="#trust--transparency">Trust</a> •
|
|
21
20
|
<a href="#faq">FAQ</a>
|
|
22
21
|
</p>
|
|
@@ -25,63 +24,83 @@
|
|
|
25
24
|
|
|
26
25
|
## What it is
|
|
27
26
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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
|
-
|
|
49
|
-
- You need
|
|
50
|
-
- You
|
|
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
|
-
>
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
##
|
|
134
|
+
## Backends
|
|
114
135
|
|
|
115
|
-
Dario
|
|
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
|
-
###
|
|
138
|
+
### 1. Claude subscription backend (built in)
|
|
118
139
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
194
|
-
curl http://localhost:3456/
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
|
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)
|
|
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
|
|
374
|
-
| **SSRF protection** |
|
|
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
|
|
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
|
|
414
|
-
Dario auto-
|
|
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).
|
|
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
|
-
|
|
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,
|
|
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,
|
|
449
|
-
| `src/cc-template.ts` |
|
|
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` |
|
|
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.
|
|
4
|
-
"description": "
|
|
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"
|