@askalf/dario 2.2.4 → 2.3.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 +3 -3
- package/dist/proxy.js +57 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,21 +44,21 @@ Opus, Sonnet, Haiku — all models, streaming, tool use. Works with Cursor, Cont
|
|
|
44
44
|
|
|
45
45
|
**Grok** (xAI)
|
|
46
46
|
|
|
47
|
-
*"
|
|
47
|
+
*"Dario works great and is safe. Fully functional with OpenClaw / Hermes. Gives you Opus 4.6, Sonnet & Haiku using your existing Claude Max/Pro sub. No extra API key or billing needed. Streaming + tools work perfectly. 100% open-source (~1100 lines TS), runs locally only, proper OAuth (PKCE), no telemetry. Highly recommended if you want a clean local proxy."*
|
|
48
48
|
|
|
49
49
|
</td>
|
|
50
50
|
<td width="33%" valign="top">
|
|
51
51
|
|
|
52
52
|
**Copilot** (Microsoft)
|
|
53
53
|
|
|
54
|
-
*"Safe for local use — well-implemented with strong security practices. The main risk vector is operator error rather than code defects."*
|
|
54
|
+
*"Verdict: Safe for local use — well-implemented with strong security practices. Minimal attack surface: 1 production dependency, PKCE OAuth, localhost-only binding, timing-safe auth, zero telemetry. The main risk vector is operator error rather than code defects."*
|
|
55
55
|
|
|
56
56
|
</td>
|
|
57
57
|
<td width="33%" valign="top">
|
|
58
58
|
|
|
59
59
|
**Gemini** (Google)
|
|
60
60
|
|
|
61
|
-
*"Highly recommended. Solves a massive pain point for developers. Modular & lean, modern PKCE auth, mature CI/CD pipeline."*
|
|
61
|
+
*"Highly recommended for personal, local development. Solves a massive pain point for developers by bridging Claude Max/Pro subscriptions with developer IDEs, saving substantial API costs. Modular & lean (~1100 lines), modern PKCE auth, SSRF protection, mature CI/CD pipeline with CodeQL and npm provenance attestations."*
|
|
62
62
|
|
|
63
63
|
</td>
|
|
64
64
|
</tr>
|
package/dist/proxy.js
CHANGED
|
@@ -8,7 +8,30 @@ const DEFAULT_PORT = 3456;
|
|
|
8
8
|
const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts, prevents abuse
|
|
9
9
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
10
10
|
const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
|
|
11
|
+
const MAX_CONCURRENT = 10; // Max concurrent upstream requests
|
|
11
12
|
const LOCALHOST = '127.0.0.1';
|
|
13
|
+
// Simple semaphore for concurrency control
|
|
14
|
+
class Semaphore {
|
|
15
|
+
max;
|
|
16
|
+
queue = [];
|
|
17
|
+
active = 0;
|
|
18
|
+
constructor(max) {
|
|
19
|
+
this.max = max;
|
|
20
|
+
}
|
|
21
|
+
async acquire() {
|
|
22
|
+
if (this.active < this.max) {
|
|
23
|
+
this.active++;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
return new Promise(resolve => { this.queue.push(() => { this.active++; resolve(); }); });
|
|
27
|
+
}
|
|
28
|
+
release() {
|
|
29
|
+
this.active--;
|
|
30
|
+
const next = this.queue.shift();
|
|
31
|
+
if (next)
|
|
32
|
+
next();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
12
35
|
// Detect installed Claude Code version at startup
|
|
13
36
|
function detectClaudeVersion() {
|
|
14
37
|
try {
|
|
@@ -76,6 +99,9 @@ function anthropicToOpenai(body) {
|
|
|
76
99
|
};
|
|
77
100
|
}
|
|
78
101
|
/** Translate Anthropic SSE → OpenAI SSE. */
|
|
102
|
+
// Track tool call state across stream chunks
|
|
103
|
+
let _streamToolIndex = 0;
|
|
104
|
+
let _streamToolId = '';
|
|
79
105
|
function translateStreamChunk(line) {
|
|
80
106
|
if (!line.startsWith('data: '))
|
|
81
107
|
return null;
|
|
@@ -84,13 +110,33 @@ function translateStreamChunk(line) {
|
|
|
84
110
|
return 'data: [DONE]\n\n';
|
|
85
111
|
try {
|
|
86
112
|
const e = JSON.parse(json);
|
|
113
|
+
const ts = Math.floor(Date.now() / 1000);
|
|
114
|
+
if (e.type === 'content_block_start') {
|
|
115
|
+
const block = e.content_block;
|
|
116
|
+
if (block?.type === 'tool_use' && block.name) {
|
|
117
|
+
_streamToolId = block.id ?? `call_${_streamToolIndex}`;
|
|
118
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { tool_calls: [{ index: _streamToolIndex, id: _streamToolId, type: 'function', function: { name: block.name, arguments: '' } }] }, finish_reason: null }] })}\n\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
87
121
|
if (e.type === 'content_block_delta') {
|
|
88
122
|
const d = e.delta;
|
|
89
123
|
if (d?.type === 'text_delta' && d.text)
|
|
90
|
-
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created:
|
|
124
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { content: d.text }, finish_reason: null }] })}\n\n`;
|
|
125
|
+
if (d?.type === 'input_json_delta' && d.partial_json)
|
|
126
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { tool_calls: [{ index: _streamToolIndex, function: { arguments: d.partial_json } }] }, finish_reason: null }] })}\n\n`;
|
|
127
|
+
}
|
|
128
|
+
if (e.type === 'content_block_stop') {
|
|
129
|
+
if (_streamToolId) {
|
|
130
|
+
_streamToolIndex++;
|
|
131
|
+
_streamToolId = '';
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
if (e.type === 'message_stop') {
|
|
136
|
+
_streamToolIndex = 0;
|
|
137
|
+
_streamToolId = '';
|
|
138
|
+
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`;
|
|
91
139
|
}
|
|
92
|
-
if (e.type === 'message_stop')
|
|
93
|
-
return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'claude', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] })}\n\ndata: [DONE]\n\n`;
|
|
94
140
|
}
|
|
95
141
|
catch { }
|
|
96
142
|
return null;
|
|
@@ -229,6 +275,7 @@ export async function startProxy(opts = {}) {
|
|
|
229
275
|
};
|
|
230
276
|
const useCli = opts.cliBackend ?? false;
|
|
231
277
|
let requestCount = 0;
|
|
278
|
+
const semaphore = new Semaphore(MAX_CONCURRENT);
|
|
232
279
|
// Optional proxy authentication — pre-encode key buffer for performance
|
|
233
280
|
const apiKey = process.env.DARIO_API_KEY;
|
|
234
281
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
@@ -320,7 +367,8 @@ export async function startProxy(opts = {}) {
|
|
|
320
367
|
res.end(ERR_METHOD);
|
|
321
368
|
return;
|
|
322
369
|
}
|
|
323
|
-
// Proxy to Anthropic
|
|
370
|
+
// Proxy to Anthropic (with concurrency control)
|
|
371
|
+
await semaphore.acquire();
|
|
324
372
|
try {
|
|
325
373
|
const accessToken = await getAccessToken();
|
|
326
374
|
// Read request body with size limit and timeout (prevents slow-loris)
|
|
@@ -385,7 +433,7 @@ export async function startProxy(opts = {}) {
|
|
|
385
433
|
}
|
|
386
434
|
if (verbose) {
|
|
387
435
|
const modelInfo = modelOverride ? ` (model: ${modelOverride})` : '';
|
|
388
|
-
console.log(`[dario] #${requestCount} ${req.method} ${
|
|
436
|
+
console.log(`[dario] #${requestCount} ${req.method} ${urlPath}${modelInfo}`);
|
|
389
437
|
}
|
|
390
438
|
// Merge client beta flags with defaults
|
|
391
439
|
const clientBeta = req.headers['anthropic-beta'];
|
|
@@ -491,6 +539,9 @@ export async function startProxy(opts = {}) {
|
|
|
491
539
|
res.writeHead(502, JSON_HEADERS);
|
|
492
540
|
res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
|
|
493
541
|
}
|
|
542
|
+
finally {
|
|
543
|
+
semaphore.release();
|
|
544
|
+
}
|
|
494
545
|
});
|
|
495
546
|
server.on('error', (err) => {
|
|
496
547
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -548,7 +599,7 @@ export async function startProxy(opts = {}) {
|
|
|
548
599
|
const refreshInterval = setInterval(async () => {
|
|
549
600
|
try {
|
|
550
601
|
const s = await getStatus();
|
|
551
|
-
if (s.status === 'expiring') {
|
|
602
|
+
if (s.status === 'expiring' || s.status === 'expired') {
|
|
552
603
|
console.log('[dario] Token expiring, refreshing...');
|
|
553
604
|
await getAccessToken(); // triggers refresh
|
|
554
605
|
}
|