@askalf/dario 2.2.5 → 2.3.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/dist/proxy.js +62 -8
- package/package.json +1 -1
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,11 +433,14 @@ 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
|
-
// Merge client beta flags with defaults
|
|
438
|
+
// Merge client beta flags with required defaults.
|
|
439
|
+
// Only include betas that are essential for OAuth + standard features.
|
|
440
|
+
// Avoid betas that may require Extra Usage (context-management, prompt-caching-scope).
|
|
441
|
+
// Client-provided betas pass through — the client controls its own feature set.
|
|
391
442
|
const clientBeta = req.headers['anthropic-beta'];
|
|
392
|
-
let beta = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,
|
|
443
|
+
let beta = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219';
|
|
393
444
|
if (clientBeta)
|
|
394
445
|
beta += ',' + clientBeta.split(',').map(f => f.trim()).filter(f => f.length > 0 && f.length < 100).join(',');
|
|
395
446
|
const headers = {
|
|
@@ -491,6 +542,9 @@ export async function startProxy(opts = {}) {
|
|
|
491
542
|
res.writeHead(502, JSON_HEADERS);
|
|
492
543
|
res.end(JSON.stringify({ error: 'Proxy error', message: 'Failed to reach upstream API' }));
|
|
493
544
|
}
|
|
545
|
+
finally {
|
|
546
|
+
semaphore.release();
|
|
547
|
+
}
|
|
494
548
|
});
|
|
495
549
|
server.on('error', (err) => {
|
|
496
550
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -548,7 +602,7 @@ export async function startProxy(opts = {}) {
|
|
|
548
602
|
const refreshInterval = setInterval(async () => {
|
|
549
603
|
try {
|
|
550
604
|
const s = await getStatus();
|
|
551
|
-
if (s.status === 'expiring') {
|
|
605
|
+
if (s.status === 'expiring' || s.status === 'expired') {
|
|
552
606
|
console.log('[dario] Token expiring, refreshing...');
|
|
553
607
|
await getAccessToken(); // triggers refresh
|
|
554
608
|
}
|