@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 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 for the details.
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "Use your Claude subscription as an API. No API key needed. Local proxy for Claude Max/Pro subscriptions.",
5
5
  "type": "module",
6
6
  "bin": {