@askalf/dario 2.2.5 → 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.
Files changed (2) hide show
  1. package/dist/proxy.js +57 -6
  2. 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: Math.floor(Date.now() / 1000), model: 'claude', choices: [{ index: 0, delta: { content: d.text }, finish_reason: null }] })}\n\n`;
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} ${req.url}${modelInfo}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "2.2.5",
3
+ "version": "2.3.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": {