@askalf/dario 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1
- 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 +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,9 @@ Install it once, log in once (using your existing Claude Code credentials if you
|
|
|
31
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
33
|
|
|
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
|
|
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.
|
|
35
|
+
|
|
36
|
+
**Multi-provider routing** (new in v3.6.0) lets dario speak to more than just Claude. Configure an OpenAI-compat backend (OpenAI, OpenRouter, Groq, local LiteLLM, Ollama's openai-compat mode) with `dario backend add openai --key=... [--base-url=...]`, and GPT-family model names at `/v1/chat/completions` route to that backend while Claude model names keep flowing through the Claude subscription path. Point your tool at `http://localhost:3456` once and use any model from any provider. See [Multi-Provider Routing](#multi-provider-routing) below.
|
|
35
37
|
|
|
36
38
|
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.
|
|
37
39
|
|
|
@@ -79,6 +81,8 @@ No separate API key. No Extra Usage charges. No rebuilding your workflow around
|
|
|
79
81
|
|
|
80
82
|
**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).
|
|
81
83
|
|
|
84
|
+
**Use dario multi-provider routing if** you want one local endpoint that speaks to Claude subscriptions *and* OpenAI / OpenRouter / Groq / a local LiteLLM / anything else OpenAI-compat. `dario backend add openai --key=...`, point your tool at `http://localhost:3456`, and every model from every provider flows through the same URL. See [Multi-Provider Routing](#multi-provider-routing).
|
|
85
|
+
|
|
82
86
|
**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.
|
|
83
87
|
|
|
84
88
|
**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.
|
|
@@ -203,6 +207,51 @@ Pool mode v3.5.0 ships **headroom-aware selection across requests**. It does not
|
|
|
203
207
|
|
|
204
208
|
---
|
|
205
209
|
|
|
210
|
+
## Multi-Provider Routing
|
|
211
|
+
|
|
212
|
+
*New in v3.6.0.* Dario is no longer Claude-only. Configure an OpenAI-compat backend once, and GPT-family model names at `/v1/chat/completions` route to that backend while Claude model names keep flowing through the Claude subscription path. Works with **any** OpenAI-compat provider — OpenAI, OpenRouter, Groq, a local LiteLLM instance, Ollama's openai-compat mode, whatever — via a configurable `--base-url`.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# OpenAI itself (default base URL)
|
|
216
|
+
dario backend add openai --key=sk-proj-...
|
|
217
|
+
|
|
218
|
+
# Groq
|
|
219
|
+
dario backend add groq --key=gsk_... --base-url=https://api.groq.com/openai/v1
|
|
220
|
+
|
|
221
|
+
# OpenRouter
|
|
222
|
+
dario backend add openrouter --key=sk-or-... --base-url=https://openrouter.ai/api/v1
|
|
223
|
+
|
|
224
|
+
# Local LiteLLM / Ollama / any openai-compat server
|
|
225
|
+
dario backend add local --key=anything --base-url=http://127.0.0.1:4000/v1
|
|
226
|
+
|
|
227
|
+
# Inspect
|
|
228
|
+
dario backend list
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### How it routes
|
|
232
|
+
|
|
233
|
+
After the proxy starts, every request at `/v1/chat/completions` is checked:
|
|
234
|
+
|
|
235
|
+
| Request model | Route |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `gpt-*`, `o1-*`, `o3-*`, `o4-*`, `chatgpt-*`, `text-davinci-*`, `text-embedding-*` | OpenAI-compat backend (if configured) |
|
|
238
|
+
| `claude-*` (or the shortcut `opus`/`sonnet`/`haiku`) | Claude subscription path |
|
|
239
|
+
| Anything else on `/v1/chat/completions` | Claude subscription path with existing OpenAI-compat translation |
|
|
240
|
+
|
|
241
|
+
Point any tool that speaks the OpenAI Chat Completions API at `http://localhost:3456/v1` once, and both GPT and Claude models work through the same base URL. Cursor, Continue, Aider, any OpenAI SDK — they don't need to know anything changed.
|
|
242
|
+
|
|
243
|
+
### Why this matters
|
|
244
|
+
|
|
245
|
+
Dario's earlier layers (template replay, framework scrubbing, pool routing) keep the Claude subscription path defensible against Anthropic's classifier. Multi-provider routing changes the *game board*: when dario also speaks OpenAI and OpenAI-compat, a squeeze on the Claude side stops being an existential issue — traffic for affected workloads shifts to another backend, and dario is still useful. The moment Anthropic ships their own "bring your subscription to the API" feature, dario's Claude backend simplifies and keeps working. Either way, dario is still the local router between every model and every tool on your machine.
|
|
246
|
+
|
|
247
|
+
### Not yet in this release
|
|
248
|
+
|
|
249
|
+
- **Cross-format translation** (Anthropic → OpenAI). Requests at `/v1/messages` with GPT-family model names fall through to the existing Claude-side handling (which maps them to Claude equivalents). Full Anthropic → OpenAI request translation, including tool_use format conversion, lands in a follow-up.
|
|
250
|
+
- **Per-model routing rules.** v3.6.0 supports one active openai-compat backend. Per-model selection (`llama-*` → Groq, `mixtral-*` → OpenRouter) ships next.
|
|
251
|
+
- **Fallback rules.** "If Claude 429s, use Gemini" is a follow-up goal. v3.6.0 ships the routing plumbing; fallback logic layers on top.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
206
255
|
## Dario and askalf
|
|
207
256
|
|
|
208
257
|
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.
|
|
@@ -238,6 +287,9 @@ Pool mode in dario covers the "I want multi-account routing on my own machine wi
|
|
|
238
287
|
| `dario accounts list` | List accounts in the multi-account pool |
|
|
239
288
|
| `dario accounts add <alias>` | Add a new account to the pool (runs OAuth flow) |
|
|
240
289
|
| `dario accounts remove <alias>` | Remove an account from the pool |
|
|
290
|
+
| `dario backend list` | List configured OpenAI-compat backends |
|
|
291
|
+
| `dario backend add <name> --key=<k> [--base-url=<u>]` | Add an OpenAI-compat backend |
|
|
292
|
+
| `dario backend remove <name>` | Remove an OpenAI-compat backend |
|
|
241
293
|
| `dario help` | Full command reference |
|
|
242
294
|
|
|
243
295
|
### Proxy options
|
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;
|