@askalf/dario 2.0.0 → 2.1.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 +13 -27
- package/dist/cli.js +5 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +52 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
```bash
|
|
28
28
|
npx @askalf/dario login # detects Claude Code credentials, starts proxy
|
|
29
29
|
|
|
30
|
-
# now use it from anywhere
|
|
31
|
-
export ANTHROPIC_BASE_URL=http://localhost:3456
|
|
32
|
-
export ANTHROPIC_API_KEY=dario
|
|
30
|
+
# now use it from anywhere — Anthropic or OpenAI SDK
|
|
31
|
+
export ANTHROPIC_BASE_URL=http://localhost:3456 # or OPENAI_BASE_URL=http://localhost:3456/v1
|
|
32
|
+
export ANTHROPIC_API_KEY=dario # or OPENAI_API_KEY=dario
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Opus, Sonnet, Haiku — all models, streaming, tool use. Works with OpenClaw, Cursor, Continue, Aider, Hermes, or any tool that speaks the Anthropic or OpenAI API. When rate limited, `--cli` routes through Claude Code for uninterrupted Opus access.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -88,6 +88,7 @@ Usage:
|
|
|
88
88
|
ANTHROPIC_BASE_URL=http://localhost:3456
|
|
89
89
|
ANTHROPIC_API_KEY=dario
|
|
90
90
|
|
|
91
|
+
Auth: open (no DARIO_API_KEY set)
|
|
91
92
|
OAuth: healthy (expires in 11h 42m)
|
|
92
93
|
Model: passthrough (client decides)
|
|
93
94
|
```
|
|
@@ -159,34 +160,18 @@ dario proxy --cli --model=opus
|
|
|
159
160
|
|
|
160
161
|
## OpenAI Compatibility
|
|
161
162
|
|
|
162
|
-
Dario
|
|
163
|
+
Dario implements `/v1/chat/completions` — any tool built for the OpenAI API works with your Claude subscription. No code changes needed.
|
|
163
164
|
|
|
164
165
|
```bash
|
|
165
|
-
|
|
166
|
+
dario proxy --model=opus
|
|
167
|
+
|
|
166
168
|
export OPENAI_BASE_URL=http://localhost:3456/v1
|
|
167
169
|
export OPENAI_API_KEY=dario
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
```python
|
|
171
|
-
from openai import OpenAI
|
|
172
170
|
|
|
173
|
-
|
|
174
|
-
response = client.chat.completions.create(
|
|
175
|
-
model="claude-opus-4-6", # or use "gpt-4" — auto-maps to Opus
|
|
176
|
-
messages=[{"role": "user", "content": "Hello!"}]
|
|
177
|
-
)
|
|
171
|
+
# Cursor, Continue, LiteLLM, any OpenAI SDK — all work
|
|
178
172
|
```
|
|
179
173
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
| OpenAI model | Maps to |
|
|
183
|
-
|---|---|
|
|
184
|
-
| `gpt-4`, `gpt-4o`, `o1`, `o3` | `claude-opus-4-6` |
|
|
185
|
-
| `o1-mini`, `o3-mini` | `claude-sonnet-4-6` |
|
|
186
|
-
| `gpt-3.5-turbo`, `gpt-4o-mini` | `claude-haiku-4-5` |
|
|
187
|
-
| Any `claude-*` model | Passed through directly |
|
|
188
|
-
|
|
189
|
-
Streaming, system prompts, temperature, and stop sequences all translate automatically.
|
|
174
|
+
Use `--model=opus` to force the model regardless of what the client sends. Or pass `claude-opus-4-6` as the model name directly — Claude model names work as-is.
|
|
190
175
|
|
|
191
176
|
## Usage Examples
|
|
192
177
|
|
|
@@ -310,12 +295,13 @@ ANTHROPIC_BASE_URL=http://localhost:3456 ANTHROPIC_API_KEY=dario your-tool-here
|
|
|
310
295
|
|
|
311
296
|
### Proxy Options
|
|
312
297
|
|
|
313
|
-
| Flag | Description | Default |
|
|
314
|
-
|
|
298
|
+
| Flag/Env | Description | Default |
|
|
299
|
+
|----------|-------------|---------|
|
|
315
300
|
| `--cli` | Use Claude CLI as backend (bypasses rate limits) | off |
|
|
316
301
|
| `--model=MODEL` | Force a model (`opus`, `sonnet`, `haiku`, or full ID) | passthrough |
|
|
317
302
|
| `--port=PORT` | Port to listen on | `3456` |
|
|
318
303
|
| `--verbose` / `-v` | Log every request | off |
|
|
304
|
+
| `DARIO_API_KEY` | If set, all endpoints (except `/health`) require matching `x-api-key` header or `Authorization: Bearer` header | unset (open) |
|
|
319
305
|
|
|
320
306
|
## Supported Features
|
|
321
307
|
|
package/dist/cli.js
CHANGED
|
@@ -13,9 +13,9 @@ import { readFile, unlink } from 'node:fs/promises';
|
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { startAutoOAuthFlow, getStatus, refreshTokens } from './oauth.js';
|
|
16
|
-
import { startProxy } from './proxy.js';
|
|
16
|
+
import { startProxy, sanitizeError } from './proxy.js';
|
|
17
17
|
const args = process.argv.slice(2);
|
|
18
|
-
const command = args[0] ??
|
|
18
|
+
const command = args[0] ?? 'proxy';
|
|
19
19
|
async function login() {
|
|
20
20
|
console.log('');
|
|
21
21
|
console.log(' dario — Claude Login');
|
|
@@ -50,7 +50,7 @@ async function login() {
|
|
|
50
50
|
}
|
|
51
51
|
catch (err) {
|
|
52
52
|
console.error('');
|
|
53
|
-
console.error(` Login failed: ${err
|
|
53
|
+
console.error(` Login failed: ${sanitizeError(err)}`);
|
|
54
54
|
console.error(' Try again with `dario login`.');
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
@@ -89,7 +89,7 @@ async function refresh() {
|
|
|
89
89
|
console.log(`[dario] Token refreshed. Expires in ${expiresIn} minutes.`);
|
|
90
90
|
}
|
|
91
91
|
catch (err) {
|
|
92
|
-
console.error(`[dario] Refresh failed: ${err
|
|
92
|
+
console.error(`[dario] Refresh failed: ${sanitizeError(err)}`);
|
|
93
93
|
process.exit(1);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -172,7 +172,6 @@ if (!handler) {
|
|
|
172
172
|
process.exit(1);
|
|
173
173
|
}
|
|
174
174
|
handler().catch(err => {
|
|
175
|
-
|
|
176
|
-
console.error('Fatal error:', msg.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]'));
|
|
175
|
+
console.error('Fatal error:', sanitizeError(err));
|
|
177
176
|
process.exit(1);
|
|
178
177
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { startAutoOAuthFlow, refreshTokens, getAccessToken, getStatus, loadCredentials } from './oauth.js';
|
|
8
8
|
export type { OAuthTokens, CredentialsFile } from './oauth.js';
|
|
9
|
-
export { startProxy } from './proxy.js';
|
|
9
|
+
export { startProxy, sanitizeError } from './proxy.js';
|
package/dist/index.js
CHANGED
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* No API key needed — your Claude subscription pays for it.
|
|
9
9
|
*/
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
12
12
|
import { execSync, spawn } from 'node:child_process';
|
|
13
13
|
import { arch, platform, version as nodeVersion } from 'node:process';
|
|
14
14
|
import { getAccessToken, getStatus } from './oauth.js';
|
|
@@ -17,7 +17,6 @@ const DEFAULT_PORT = 3456;
|
|
|
17
17
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
|
|
18
18
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
19
19
|
const LOCALHOST = '127.0.0.1';
|
|
20
|
-
const CORS_ORIGIN = 'http://localhost';
|
|
21
20
|
// Detect installed Claude Code version at startup
|
|
22
21
|
function detectClaudeVersion() {
|
|
23
22
|
try {
|
|
@@ -57,16 +56,20 @@ const MODEL_ALIASES = {
|
|
|
57
56
|
};
|
|
58
57
|
// OpenAI model name → Anthropic model name
|
|
59
58
|
const OPENAI_MODEL_MAP = {
|
|
60
|
-
'gpt-4': 'claude-opus-4-6',
|
|
59
|
+
'gpt-4.1': 'claude-opus-4-6',
|
|
60
|
+
'gpt-4.1-mini': 'claude-sonnet-4-6',
|
|
61
|
+
'gpt-4.1-nano': 'claude-haiku-4-5',
|
|
61
62
|
'gpt-4o': 'claude-opus-4-6',
|
|
62
|
-
'gpt-4-turbo': 'claude-opus-4-6',
|
|
63
63
|
'gpt-4o-mini': 'claude-haiku-4-5',
|
|
64
|
+
'gpt-4-turbo': 'claude-opus-4-6',
|
|
65
|
+
'gpt-4': 'claude-opus-4-6',
|
|
64
66
|
'gpt-3.5-turbo': 'claude-haiku-4-5',
|
|
65
|
-
'o1': 'claude-opus-4-6',
|
|
66
|
-
'o1-mini': 'claude-sonnet-4-6',
|
|
67
|
-
'o1-preview': 'claude-opus-4-6',
|
|
68
67
|
'o3': 'claude-opus-4-6',
|
|
69
68
|
'o3-mini': 'claude-sonnet-4-6',
|
|
69
|
+
'o4-mini': 'claude-sonnet-4-6',
|
|
70
|
+
'o1': 'claude-opus-4-6',
|
|
71
|
+
'o1-mini': 'claude-sonnet-4-6',
|
|
72
|
+
'o1-pro': 'claude-opus-4-6',
|
|
70
73
|
};
|
|
71
74
|
/**
|
|
72
75
|
* Translate OpenAI chat completion request → Anthropic Messages request.
|
|
@@ -177,10 +180,13 @@ function openaiModelsList() {
|
|
|
177
180
|
})),
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
|
-
function sanitizeError(err) {
|
|
183
|
+
export function sanitizeError(err) {
|
|
181
184
|
const msg = err instanceof Error ? err.message : String(err);
|
|
182
|
-
// Never leak tokens in error messages
|
|
183
|
-
return msg
|
|
185
|
+
// Never leak tokens, JWTs, or bearer values in error messages
|
|
186
|
+
return msg
|
|
187
|
+
.replace(/sk-ant-[a-zA-Z0-9_-]+/g, '[REDACTED]')
|
|
188
|
+
.replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED_JWT]')
|
|
189
|
+
.replace(/Bearer\s+[a-zA-Z0-9_-]+/gi, 'Bearer [REDACTED]');
|
|
184
190
|
}
|
|
185
191
|
/**
|
|
186
192
|
* CLI Backend: route requests through `claude --print` instead of direct API.
|
|
@@ -195,7 +201,9 @@ async function handleViaCli(body, model, verbose) {
|
|
|
195
201
|
if (!lastUser) {
|
|
196
202
|
return { status: 400, body: JSON.stringify({ error: 'No user message' }), contentType: 'application/json' };
|
|
197
203
|
}
|
|
198
|
-
const
|
|
204
|
+
const rawModel = model ?? parsed.model ?? 'claude-opus-4-6';
|
|
205
|
+
// Validate model name — only allow alphanumeric, hyphens, dots, underscores
|
|
206
|
+
const effectiveModel = /^[a-zA-Z0-9._-]+$/.test(rawModel) ? rawModel : 'claude-opus-4-6';
|
|
199
207
|
const prompt = typeof lastUser.content === 'string'
|
|
200
208
|
? lastUser.content
|
|
201
209
|
: JSON.stringify(lastUser.content);
|
|
@@ -231,7 +239,7 @@ async function handleViaCli(body, model, verbose) {
|
|
|
231
239
|
if (code !== 0 || !stdout.trim()) {
|
|
232
240
|
resolve({
|
|
233
241
|
status: 502,
|
|
234
|
-
body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: stderr.substring(0, 200) || 'CLI backend failed' } }),
|
|
242
|
+
body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: sanitizeError(stderr.substring(0, 200)) || 'CLI backend failed' } }),
|
|
235
243
|
contentType: 'application/json',
|
|
236
244
|
});
|
|
237
245
|
return;
|
|
@@ -286,11 +294,28 @@ export async function startProxy(opts = {}) {
|
|
|
286
294
|
const useCli = opts.cliBackend ?? false;
|
|
287
295
|
let requestCount = 0;
|
|
288
296
|
let tokenCostEstimate = 0;
|
|
297
|
+
// Optional proxy authentication
|
|
298
|
+
const apiKey = process.env.DARIO_API_KEY;
|
|
299
|
+
const corsOrigin = `http://localhost:${port}`;
|
|
300
|
+
function checkAuth(req) {
|
|
301
|
+
if (!apiKey)
|
|
302
|
+
return true; // no key set = open access
|
|
303
|
+
const provided = req.headers['x-api-key']
|
|
304
|
+
|| req.headers.authorization?.replace(/^Bearer\s+/i, '');
|
|
305
|
+
if (!provided)
|
|
306
|
+
return false;
|
|
307
|
+
try {
|
|
308
|
+
return timingSafeEqual(Buffer.from(provided), Buffer.from(apiKey));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
289
314
|
const server = createServer(async (req, res) => {
|
|
290
315
|
// CORS preflight
|
|
291
316
|
if (req.method === 'OPTIONS') {
|
|
292
317
|
res.writeHead(204, {
|
|
293
|
-
'Access-Control-Allow-Origin':
|
|
318
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
294
319
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
295
320
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta',
|
|
296
321
|
'Access-Control-Max-Age': '86400',
|
|
@@ -312,6 +337,12 @@ export async function startProxy(opts = {}) {
|
|
|
312
337
|
}));
|
|
313
338
|
return;
|
|
314
339
|
}
|
|
340
|
+
// Auth gate — everything below health requires auth when DARIO_API_KEY is set
|
|
341
|
+
if (!checkAuth(req)) {
|
|
342
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
343
|
+
res.end(JSON.stringify({ error: 'Unauthorized', message: 'Invalid or missing API key' }));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
315
346
|
// Status endpoint
|
|
316
347
|
if (urlPath === '/status') {
|
|
317
348
|
const s = await getStatus();
|
|
@@ -322,7 +353,7 @@ export async function startProxy(opts = {}) {
|
|
|
322
353
|
// OpenAI-compatible models list
|
|
323
354
|
if (urlPath === '/v1/models' && req.method === 'GET') {
|
|
324
355
|
requestCount++;
|
|
325
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin':
|
|
356
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': corsOrigin });
|
|
326
357
|
res.end(JSON.stringify(openaiModelsList()));
|
|
327
358
|
return;
|
|
328
359
|
}
|
|
@@ -368,7 +399,7 @@ export async function startProxy(opts = {}) {
|
|
|
368
399
|
requestCount++;
|
|
369
400
|
res.writeHead(cliResult.status, {
|
|
370
401
|
'Content-Type': cliResult.contentType,
|
|
371
|
-
'Access-Control-Allow-Origin':
|
|
402
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
372
403
|
});
|
|
373
404
|
res.end(cliResult.body);
|
|
374
405
|
return;
|
|
@@ -447,7 +478,7 @@ export async function startProxy(opts = {}) {
|
|
|
447
478
|
// Forward response headers
|
|
448
479
|
const responseHeaders = {
|
|
449
480
|
'Content-Type': contentType || 'application/json',
|
|
450
|
-
'Access-Control-Allow-Origin':
|
|
481
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
451
482
|
};
|
|
452
483
|
// Forward rate limit headers (including unified subscription headers)
|
|
453
484
|
for (const [key, value] of upstream.headers.entries()) {
|
|
@@ -463,6 +494,7 @@ export async function startProxy(opts = {}) {
|
|
|
463
494
|
const decoder = new TextDecoder();
|
|
464
495
|
try {
|
|
465
496
|
let buffer = '';
|
|
497
|
+
const MAX_LINE_LENGTH = 1_000_000; // 1MB max per SSE line
|
|
466
498
|
while (true) {
|
|
467
499
|
const { done, value } = await reader.read();
|
|
468
500
|
if (done)
|
|
@@ -470,6 +502,10 @@ export async function startProxy(opts = {}) {
|
|
|
470
502
|
if (isOpenAI) {
|
|
471
503
|
// Translate Anthropic SSE → OpenAI SSE
|
|
472
504
|
buffer += decoder.decode(value, { stream: true });
|
|
505
|
+
// Guard against unbounded buffer growth
|
|
506
|
+
if (buffer.length > MAX_LINE_LENGTH) {
|
|
507
|
+
buffer = buffer.slice(-MAX_LINE_LENGTH);
|
|
508
|
+
}
|
|
473
509
|
const lines = buffer.split('\n');
|
|
474
510
|
buffer = lines.pop() ?? '';
|
|
475
511
|
for (const line of lines) {
|