@askalf/dario 3.3.0 → 3.4.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 CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomUUID, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
3
- import { execSync, spawn } from 'node:child_process';
4
- import { readFileSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs';
3
+ import { execSync } from 'node:child_process';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { homedir, tmpdir } from 'node:os';
6
+ import { homedir } from 'node:os';
7
7
  import { arch, platform } from 'node:process';
8
8
  import { getAccessToken, getStatus } from './oauth.js';
9
9
  import { buildCCRequest, reverseMapResponse } from './cc-template.js';
@@ -36,63 +36,29 @@ class Semaphore {
36
36
  next();
37
37
  }
38
38
  }
39
- // Billing tag hash seed — extracted from Claude Code binary (constant XGA)
39
+ // Billing tag hash seed — matches Claude Code's value
40
40
  const BILLING_SEED = '59cf53e54c78';
41
- // Compute per-request build tag matching Claude Code's Oz$ algorithm:
41
+ // Compute per-request build tag:
42
42
  // SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)
43
43
  function computeBuildTag(userMessage, version) {
44
44
  const chars = [4, 7, 20].map(i => userMessage[i] || '0').join('');
45
45
  return createHash('sha256').update(`${BILLING_SEED}${chars}${version}`).digest('hex').slice(0, 3);
46
46
  }
47
- // Per-request cch: real Claude Code generates a random 5-char hex value each request.
48
- // Confirmed via MITM: 10 identical requests → 10 unique cch values, no deterministic pattern.
47
+ // Per-request cch: random 5-char hex value each request (Claude Code does the same).
49
48
  function computeCch() {
50
49
  return randomBytes(3).toString('hex').slice(0, 5);
51
50
  }
52
- // Detect installed Claude Code binary at startup (single exec for both version + availability)
53
- let cliAvailable = false;
54
- function detectCli() {
51
+ // Detect installed Claude Code version for the build-tag computation.
52
+ // Falls back to a known-good version if claude isn't on PATH.
53
+ function detectCliVersion() {
55
54
  try {
56
55
  const out = execSync('claude --version', { timeout: 5000, stdio: 'pipe' }).toString().trim();
57
- cliAvailable = true;
58
- // Capture major version (e.g., 2.1.100) — build tag is computed per-request
59
56
  return out.match(/^([\d]+\.[\d]+\.[\d]+)/)?.[1] ?? '2.1.100';
60
57
  }
61
58
  catch {
62
- cliAvailable = false;
63
59
  return '2.1.100';
64
60
  }
65
61
  }
66
- /** Convert a non-streaming Messages API response to SSE event stream. */
67
- function jsonToSse(jsonBody) {
68
- try {
69
- const msg = JSON.parse(jsonBody);
70
- const events = [];
71
- // message_start
72
- events.push(`event: message_start\ndata: ${JSON.stringify({ type: 'message_start', message: { ...msg, content: [], stop_reason: null } })}\n\n`);
73
- // content blocks
74
- const content = msg.content;
75
- if (content) {
76
- for (let i = 0; i < content.length; i++) {
77
- const block = content[i];
78
- events.push(`event: content_block_start\ndata: ${JSON.stringify({ type: 'content_block_start', index: i, content_block: { type: block.type, ...(block.type === 'text' ? { text: '' } : { thinking: '' }) } })}\n\n`);
79
- if (block.type === 'text' && block.text) {
80
- events.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: i, delta: { type: 'text_delta', text: block.text } })}\n\n`);
81
- }
82
- else if (block.type === 'thinking' && block.thinking) {
83
- events.push(`event: content_block_delta\ndata: ${JSON.stringify({ type: 'content_block_delta', index: i, delta: { type: 'thinking_delta', thinking: block.thinking } })}\n\n`);
84
- }
85
- events.push(`event: content_block_stop\ndata: ${JSON.stringify({ type: 'content_block_stop', index: i })}\n\n`);
86
- }
87
- }
88
- // message_stop
89
- events.push(`event: message_stop\ndata: ${JSON.stringify({ type: 'message_stop' })}\n\n`);
90
- return events.join('');
91
- }
92
- catch {
93
- return '';
94
- }
95
- }
96
62
  /** Extract first user message text from a request body for billing tag computation. */
97
63
  function extractFirstUserMessage(body) {
98
64
  const messages = body.messages;
@@ -109,42 +75,8 @@ function extractFirstUserMessage(body) {
109
75
  }
110
76
  return '';
111
77
  }
112
- /** Convert CLI JSON response to OpenAI SSE format. */
113
- function jsonToOpenaiSse(jsonBody) {
114
- try {
115
- const parsed = JSON.parse(jsonBody);
116
- const text = parsed.content?.find(c => c.type === 'text')?.text ?? '';
117
- const ts = Math.floor(Date.now() / 1000);
118
- return `data: ${JSON.stringify({ id: 'chatcmpl-dario', object: 'chat.completion.chunk', created: ts, model: 'claude', choices: [{ index: 0, delta: { content: text }, finish_reason: null }] })}\n\n` +
119
- `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`;
120
- }
121
- catch {
122
- return '';
123
- }
124
- }
125
- /** Send a CLI result to the client, handling streaming/format translation. */
126
- function sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, securityHeaders) {
127
- const headers = { 'Access-Control-Allow-Origin': corsOrigin, ...securityHeaders };
128
- const ok = cliResult.status >= 200 && cliResult.status < 300;
129
- if (ok && clientWantsStream) {
130
- const sseData = isOpenAI ? jsonToOpenaiSse(cliResult.body) : jsonToSse(cliResult.body);
131
- if (sseData) {
132
- res.writeHead(200, { 'Content-Type': 'text/event-stream', ...headers });
133
- res.end(sseData);
134
- return;
135
- }
136
- }
137
- if (ok && isOpenAI) {
138
- try {
139
- cliResult.body = JSON.stringify(anthropicToOpenai(JSON.parse(cliResult.body)));
140
- }
141
- catch { }
142
- }
143
- res.writeHead(cliResult.status, { 'Content-Type': cliResult.contentType, ...headers });
144
- res.end(cliResult.body);
145
- }
146
- // Session ID rotates per request — each CC --print invocation creates a new session.
147
- // A persistent session ID across many requests is a detection signal.
78
+ // Session ID rotates per request fresh UUID per invocation.
79
+ // A persistent session ID across many requests is a behavioral fingerprint.
148
80
  let SESSION_ID = randomUUID();
149
81
  const OS_NAME = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'MacOS' : 'Linux';
150
82
  // Claude Code device identity — required for Max plan billing classification.
@@ -205,8 +137,7 @@ function filterBillableBetas(betas) {
205
137
  const ORCHESTRATION_TAG_NAMES = [
206
138
  'system-reminder', 'env', 'system_information', 'current_working_directory',
207
139
  'operating_system', 'default_shell', 'home_directory', 'task_metadata',
208
- 'tool_exec', 'tool_output', 'skill_content', 'skill_files',
209
- 'directories', 'available_skills', 'thinking',
140
+ 'directories', 'thinking',
210
141
  ];
211
142
  const ORCHESTRATION_PATTERNS = ORCHESTRATION_TAG_NAMES.flatMap(tag => [
212
143
  new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'),
@@ -375,123 +306,6 @@ function enrich429(body, headers) {
375
306
  return body;
376
307
  }
377
308
  }
378
- /**
379
- * CLI Backend: route requests through `claude --print` instead of direct API.
380
- * This bypasses rate limiting because Claude Code's binary has priority routing.
381
- */
382
- async function handleViaCli(body, model, verbose) {
383
- try {
384
- const parsed = JSON.parse(body.toString());
385
- // Extract the last user message as the prompt
386
- const messages = parsed.messages ?? [];
387
- const lastUser = [...messages].reverse().find(m => m.role === 'user');
388
- if (!lastUser) {
389
- return { status: 400, body: JSON.stringify({ error: 'No user message' }), contentType: 'application/json' };
390
- }
391
- const rawModel = model ?? parsed.model ?? 'claude-opus-4-6';
392
- // Validate model name — only allow alphanumeric, hyphens, dots, underscores
393
- const effectiveModel = /^[a-zA-Z0-9._-]+$/.test(rawModel) ? rawModel : 'claude-opus-4-6';
394
- const prompt = typeof lastUser.content === 'string'
395
- ? lastUser.content
396
- : JSON.stringify(lastUser.content);
397
- // Build claude --print command
398
- const args = ['--print', '--model', effectiveModel];
399
- // Flatten system prompt — API accepts string or array of content blocks,
400
- // but claude --print only accepts a string
401
- let systemPrompt = '';
402
- if (typeof parsed.system === 'string') {
403
- systemPrompt = parsed.system;
404
- }
405
- else if (Array.isArray(parsed.system)) {
406
- systemPrompt = parsed.system
407
- .filter(b => b.text)
408
- .map(b => b.text)
409
- .join('\n\n');
410
- }
411
- // Include conversation history as context
412
- const history = messages.slice(0, -1);
413
- if (history.length > 0) {
414
- const historyText = history.map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n');
415
- systemPrompt = systemPrompt ? `${systemPrompt}\n\nConversation history:\n${historyText}` : `Conversation history:\n${historyText}`;
416
- }
417
- // Write system prompt to temp file instead of passing as arg to avoid E2BIG
418
- // on large conversation contexts (OS arg size limit ~2MB)
419
- let systemPromptFile = null;
420
- if (systemPrompt) {
421
- systemPromptFile = join(tmpdir(), `dario-sysprompt-${randomUUID()}.txt`);
422
- writeFileSync(systemPromptFile, systemPrompt, { mode: 0o600 });
423
- args.push('--append-system-prompt-file', systemPromptFile);
424
- }
425
- if (verbose) {
426
- console.log(`[dario:cli] model=${effectiveModel} prompt=${prompt.substring(0, 60)}...`);
427
- }
428
- // Spawn claude --print
429
- return new Promise((resolve) => {
430
- // Cleanup temp file when done
431
- const cleanup = () => { if (systemPromptFile)
432
- try {
433
- unlinkSync(systemPromptFile);
434
- }
435
- catch { } };
436
- const child = spawn('claude', args, {
437
- stdio: ['pipe', 'pipe', 'pipe'],
438
- timeout: 300_000,
439
- });
440
- let stdout = '';
441
- let stderr = '';
442
- const MAX_CLI_OUTPUT = 5_000_000; // 5MB cap per stream — prevents OOM from runaway CLI
443
- child.stdout.on('data', (d) => { if (stdout.length < MAX_CLI_OUTPUT)
444
- stdout += d.toString(); });
445
- child.stderr.on('data', (d) => { if (stderr.length < MAX_CLI_OUTPUT)
446
- stderr += d.toString(); });
447
- child.stdin.write(prompt);
448
- child.stdin.end();
449
- child.on('close', (code) => {
450
- cleanup();
451
- if (code !== 0 || !stdout.trim()) {
452
- resolve({
453
- status: 502,
454
- body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: sanitizeError(stderr.substring(0, 200)) || 'CLI backend failed' } }),
455
- contentType: 'application/json',
456
- });
457
- return;
458
- }
459
- // Build a proper Messages API response
460
- const text = stdout.trim();
461
- const estimatedTokens = Math.ceil(text.length / 4);
462
- const response = {
463
- id: `msg_${randomUUID().replace(/-/g, '').substring(0, 24)}`,
464
- type: 'message',
465
- role: 'assistant',
466
- model: effectiveModel,
467
- content: [{ type: 'text', text }],
468
- stop_reason: 'end_turn',
469
- stop_sequence: null,
470
- usage: {
471
- input_tokens: Math.ceil(prompt.length / 4),
472
- output_tokens: estimatedTokens,
473
- },
474
- };
475
- resolve({ status: 200, body: JSON.stringify(response), contentType: 'application/json' });
476
- });
477
- child.on('error', (err) => {
478
- cleanup();
479
- resolve({
480
- status: 502,
481
- body: JSON.stringify({ type: 'error', error: { type: 'api_error', message: 'Claude CLI not found. Install Claude Code first.' } }),
482
- contentType: 'application/json',
483
- });
484
- });
485
- });
486
- }
487
- catch (err) {
488
- return {
489
- status: 400,
490
- body: JSON.stringify({ type: 'error', error: { type: 'invalid_request_error', message: 'Invalid request body' } }),
491
- contentType: 'application/json',
492
- };
493
- }
494
- }
495
309
  export async function startProxy(opts = {}) {
496
310
  const port = opts.port ?? DEFAULT_PORT;
497
311
  const verbose = opts.verbose ?? false;
@@ -502,7 +316,7 @@ export async function startProxy(opts = {}) {
502
316
  console.error('[dario] Not authenticated. Run `dario login` first.');
503
317
  process.exit(1);
504
318
  }
505
- const cliVersion = detectCli();
319
+ const cliVersion = detectCliVersion();
506
320
  const modelOverride = opts.model ? (MODEL_ALIASES[opts.model] ?? opts.model) : null;
507
321
  const identity = loadClaudeIdentity();
508
322
  if (identity.deviceId) {
@@ -512,7 +326,7 @@ export async function startProxy(opts = {}) {
512
326
  console.warn('[dario] WARNING: No Claude Code device identity found. Requests may be billed as Extra Usage.');
513
327
  console.warn('[dario] Run Claude Code at least once to generate ~/.claude/.claude.json');
514
328
  }
515
- // Pre-build static headers (matches real Claude Code captured via MITM)
329
+ // Pre-build static headers matches the set a real Claude Code client sends.
516
330
  const staticHeaders = passthrough ? {
517
331
  'accept': 'application/json',
518
332
  'Content-Type': 'application/json',
@@ -531,12 +345,10 @@ export async function startProxy(opts = {}) {
531
345
  // Claude Code runs on Bun which reports v24.3.0 as Node compat version
532
346
  'x-stainless-runtime-version': 'v24.3.0',
533
347
  };
534
- const useCli = opts.cliBackend ?? false;
535
348
  let requestCount = 0;
536
349
  const semaphore = new Semaphore(MAX_CONCURRENT);
537
- // Rate governor: CC --print takes ~2-3s per invocation.
538
- // Rapid-fire requests from one "session" is an inhuman pattern.
539
- // Minimum 500ms between requests — fast enough for agents, slow enough to not flag.
350
+ // Rate governor minimum 500ms between requests. Fast enough for agents,
351
+ // slow enough to not look like a scripted flood of identical traffic.
540
352
  let lastRequestTime = 0;
541
353
  const MIN_REQUEST_INTERVAL_MS = parseInt(process.env.DARIO_MIN_INTERVAL_MS || '500', 10);
542
354
  // Optional proxy authentication — pre-encode key buffer for performance
@@ -656,31 +468,6 @@ export async function startProxy(opts = {}) {
656
468
  clearTimeout(bodyTimeout);
657
469
  }
658
470
  const body = Buffer.concat(chunks);
659
- // CLI backend mode: route through claude --print (works for both Anthropic and OpenAI endpoints)
660
- if (useCli && req.method === 'POST' && body.length > 0) {
661
- let cliBody = body;
662
- let clientWantsStream = false;
663
- // Translate OpenAI format before passing to CLI
664
- if (isOpenAI) {
665
- try {
666
- const parsed = JSON.parse(body.toString());
667
- clientWantsStream = !!parsed.stream;
668
- cliBody = Buffer.from(JSON.stringify(openaiToAnthropic(parsed, modelOverride)));
669
- }
670
- catch { /* send as-is */ }
671
- }
672
- else {
673
- try {
674
- const parsed = JSON.parse(body.toString());
675
- clientWantsStream = !!parsed.stream;
676
- }
677
- catch { }
678
- }
679
- const cliResult = await handleViaCli(cliBody, modelOverride, verbose);
680
- requestCount++;
681
- sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, SECURITY_HEADERS);
682
- return;
683
- }
684
471
  // Parse body once, apply OpenAI translation, model override, and sanitization
685
472
  let finalBody = body.length > 0 ? body : undefined;
686
473
  let ccToolMap = null;
@@ -731,7 +518,7 @@ export async function startProxy(opts = {}) {
731
518
  beta += ',' + clientBeta;
732
519
  }
733
520
  else {
734
- // CC v2.1.104 beta set (exact 8 from MITM capture, exact order).
521
+ // CC v2.1.104 beta set 8 flags in the order Claude Code sends them.
735
522
  // context-1m requires Extra Usage — if it 400s, we auto-retry without it.
736
523
  beta = 'claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,effort-2025-11-24';
737
524
  if (clientBeta) {
@@ -749,7 +536,7 @@ export async function startProxy(opts = {}) {
749
536
  await new Promise(r => setTimeout(r, MIN_REQUEST_INTERVAL_MS - elapsed));
750
537
  }
751
538
  lastRequestTime = Date.now();
752
- // Rotate session ID per request — CC --print creates a new session each invocation
539
+ // Rotate session ID per request — fresh UUID avoids persistent-session fingerprinting
753
540
  SESSION_ID = randomUUID();
754
541
  const headers = {
755
542
  ...staticHeaders,
@@ -767,12 +554,22 @@ export async function startProxy(opts = {}) {
767
554
  body: finalBody ? new Uint8Array(finalBody) : undefined,
768
555
  signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
769
556
  });
770
- // Auto-retry without context-1m if it triggers a long-context billing error
771
- if (upstream.status === 429 && !passthrough) {
772
- const peekBody = await upstream.text().catch(() => '');
773
- if (peekBody.includes('long context') || peekBody.includes('Extra usage is required')) {
557
+ // Auto-retry without context-1m if it triggers a long-context billing error.
558
+ // Anthropic returns this as either 400 ("long context beta is not yet available
559
+ // for this subscription") or 429 ("Extra usage is required for long context
560
+ // requests") depending on the endpoint we handle both.
561
+ //
562
+ // Note: `upstream.text()` consumes the body, so once we peek we MUST
563
+ // handle the response here (can't fall through to the normal forwarder).
564
+ let peekedBody = null;
565
+ if ((upstream.status === 400 || upstream.status === 429) && !passthrough) {
566
+ peekedBody = await upstream.text().catch(() => '');
567
+ const isLongContextError = peekedBody.includes('long context')
568
+ || peekedBody.includes('Extra usage is required')
569
+ || peekedBody.includes('long_context');
570
+ if (isLongContextError) {
774
571
  if (verbose)
775
- console.log(`[dario] #${requestCount} context-1m rejected — retrying without it`);
572
+ console.log(`[dario] #${requestCount} context-1m rejected (${upstream.status}) — retrying without it`);
776
573
  const reducedBeta = beta.replace(',context-1m-2025-08-07', '').replace('context-1m-2025-08-07,', '');
777
574
  const retryHeaders = { ...headers, 'anthropic-beta': reducedBeta };
778
575
  const retry = await fetch(targetBase, {
@@ -781,34 +578,48 @@ export async function startProxy(opts = {}) {
781
578
  body: finalBody ? new Uint8Array(finalBody) : undefined,
782
579
  signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
783
580
  });
784
- // Use the retry response from here on
581
+ // Use the retry response from here on — peeked body is now stale
785
582
  upstream = retry;
583
+ peekedBody = null;
786
584
  }
787
- else {
788
- // Not a context-1m issue — handle as normal 429 below
789
- // Re-wrap the already-consumed body for downstream handling
790
- const enriched = enrich429(peekBody, upstream.headers);
791
- if (!(cliAvailable && !useCli)) {
792
- const responseHeaders = {
793
- 'Content-Type': 'application/json',
794
- 'Access-Control-Allow-Origin': corsOrigin,
795
- ...SECURITY_HEADERS,
796
- };
797
- for (const [key, value] of upstream.headers.entries()) {
798
- if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
799
- responseHeaders[key] = value;
800
- }
585
+ else if (upstream.status === 429) {
586
+ // Not a context-1m issue — return enriched 429 directly
587
+ const enriched = enrich429(peekedBody, upstream.headers);
588
+ const responseHeaders = {
589
+ 'Content-Type': 'application/json',
590
+ 'Access-Control-Allow-Origin': corsOrigin,
591
+ ...SECURITY_HEADERS,
592
+ };
593
+ for (const [key, value] of upstream.headers.entries()) {
594
+ if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
595
+ responseHeaders[key] = value;
801
596
  }
802
- requestCount++;
803
- res.writeHead(429, responseHeaders);
804
- res.end(enriched);
805
- return;
806
597
  }
807
- // Fall through to CLI fallback below
598
+ requestCount++;
599
+ res.writeHead(429, responseHeaders);
600
+ res.end(enriched);
601
+ return;
602
+ }
603
+ else if (upstream.status === 400) {
604
+ // Non-long-context 400 — forward upstream error directly.
605
+ // The body is already consumed, so we write it straight out.
606
+ const responseHeaders = {
607
+ 'Content-Type': upstream.headers.get('content-type') ?? 'application/json',
608
+ 'Access-Control-Allow-Origin': corsOrigin,
609
+ ...SECURITY_HEADERS,
610
+ };
611
+ for (const [key, value] of upstream.headers.entries()) {
612
+ if (key === 'request-id')
613
+ responseHeaders[key] = value;
614
+ }
615
+ requestCount++;
616
+ res.writeHead(400, responseHeaders);
617
+ res.end(peekedBody);
618
+ return;
808
619
  }
809
620
  }
810
621
  // Enrich 429 errors with rate limit details from headers (Anthropic only returns "Error")
811
- if (upstream.status === 429 && !(cliAvailable && !useCli)) {
622
+ if (upstream.status === 429) {
812
623
  const errBody = await upstream.text().catch(() => '');
813
624
  const enriched = enrich429(errBody, upstream.headers);
814
625
  const responseHeaders = {
@@ -826,42 +637,6 @@ export async function startProxy(opts = {}) {
826
637
  res.end(enriched);
827
638
  return;
828
639
  }
829
- // Auto-fallback: if API returns 429 and CLI is available, retry through CLI binary
830
- if (upstream.status === 429 && cliAvailable && !useCli) {
831
- const errBody429 = await upstream.text().catch(() => '');
832
- if (verbose)
833
- console.log(`[dario] #${requestCount} 429 from API — falling back to CLI`);
834
- let clientWantsStream = false;
835
- try {
836
- clientWantsStream = !!JSON.parse(body.toString()).stream;
837
- }
838
- catch { }
839
- const cliResult = await handleViaCli(body, modelOverride, verbose);
840
- // If CLI fallback also failed, return the original 429 with enriched details
841
- // instead of a cryptic 502 from CLI failure
842
- if (cliResult.status >= 500) {
843
- if (verbose)
844
- console.log(`[dario] #${requestCount} CLI fallback failed (${cliResult.status}) — returning original 429`);
845
- const enriched = enrich429(errBody429, upstream.headers);
846
- const responseHeaders = {
847
- 'Content-Type': 'application/json',
848
- 'Access-Control-Allow-Origin': corsOrigin,
849
- ...SECURITY_HEADERS,
850
- };
851
- for (const [key, value] of upstream.headers.entries()) {
852
- if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
853
- responseHeaders[key] = value;
854
- }
855
- }
856
- requestCount++;
857
- res.writeHead(429, responseHeaders);
858
- res.end(enriched);
859
- return;
860
- }
861
- requestCount++;
862
- sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, SECURITY_HEADERS);
863
- return;
864
- }
865
640
  // Detect streaming from content-type (reliable) or body (fallback)
866
641
  const contentType = upstream.headers.get('content-type') ?? '';
867
642
  const isStream = contentType.includes('text/event-stream');
@@ -980,7 +755,9 @@ export async function startProxy(opts = {}) {
980
755
  process.exit(1);
981
756
  });
982
757
  server.listen(port, LOCALHOST, () => {
983
- const modeLine = passthrough ? 'Mode: passthrough (OAuth swap only, no injection)' : useCli ? 'Backend: Claude CLI (bypasses rate limits)' : `OAuth: ${status.status} (expires in ${status.expiresIn})`;
758
+ const modeLine = passthrough
759
+ ? 'Mode: passthrough (OAuth swap only, no injection)'
760
+ : `OAuth: ${status.status} (expires in ${status.expiresIn})`;
984
761
  const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
985
762
  console.log('');
986
763
  console.log(` dario — http://localhost:${port}`);
@@ -995,8 +772,8 @@ export async function startProxy(opts = {}) {
995
772
  console.log(` ${modelLine}`);
996
773
  console.log('');
997
774
  });
998
- // Session presence heartbeat — registers this proxy as an active Claude Code session
999
- // Claude Code sends this every 5 seconds; the server uses it for priority routing
775
+ // Session presence heartbeat — keeps the OAuth session marked active
776
+ // (matches the ~5s cadence of a real Claude Code session).
1000
777
  const clientId = randomUUID();
1001
778
  const connectedAt = new Date().toISOString();
1002
779
  let lastPresencePulse = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
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": {