@askalf/dario 3.4.0 → 3.4.3
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 +158 -139
- package/dist/cc-oauth-detect.d.ts +30 -14
- package/dist/cc-oauth-detect.js +62 -72
- package/dist/cc-template-data.json +1 -1
- package/dist/cc-template.d.ts +4 -4
- package/dist/cc-template.js +66 -31
- package/dist/cli.js +19 -5
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.js +72 -292
- package/package.json +4 -2
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
|
|
4
|
-
import { readFileSync, readdirSync
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { homedir
|
|
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';
|
|
@@ -13,7 +13,16 @@ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB — generous for large prompts
|
|
|
13
13
|
const UPSTREAM_TIMEOUT_MS = 300_000; // 5 min — matches Anthropic SDK default
|
|
14
14
|
const BODY_READ_TIMEOUT_MS = 30_000; // 30s — prevents slow-loris on body reads
|
|
15
15
|
const MAX_CONCURRENT = 10; // Max concurrent upstream requests
|
|
16
|
-
const
|
|
16
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
17
|
+
// A host is "loopback" if it's one of the well-known localhost literals.
|
|
18
|
+
// Used to decide whether to warn at startup about binding to a reachable
|
|
19
|
+
// interface — binding anywhere else means other machines can reach the
|
|
20
|
+
// proxy and should only be done with DARIO_API_KEY set.
|
|
21
|
+
function isLoopbackHost(host) {
|
|
22
|
+
if (host === '127.0.0.1' || host === '::1' || host === 'localhost')
|
|
23
|
+
return true;
|
|
24
|
+
return host.startsWith('127.');
|
|
25
|
+
}
|
|
17
26
|
// Simple semaphore for concurrency control
|
|
18
27
|
class Semaphore {
|
|
19
28
|
max;
|
|
@@ -36,63 +45,29 @@ class Semaphore {
|
|
|
36
45
|
next();
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
// Billing tag hash seed —
|
|
48
|
+
// Billing tag hash seed — matches Claude Code's value
|
|
40
49
|
const BILLING_SEED = '59cf53e54c78';
|
|
41
|
-
// Compute per-request build tag
|
|
50
|
+
// Compute per-request build tag:
|
|
42
51
|
// SHA-256(seed + chars[4,7,20] of user message + version).slice(0,3)
|
|
43
52
|
function computeBuildTag(userMessage, version) {
|
|
44
53
|
const chars = [4, 7, 20].map(i => userMessage[i] || '0').join('');
|
|
45
54
|
return createHash('sha256').update(`${BILLING_SEED}${chars}${version}`).digest('hex').slice(0, 3);
|
|
46
55
|
}
|
|
47
|
-
// Per-request cch:
|
|
48
|
-
// Confirmed via MITM: 10 identical requests → 10 unique cch values, no deterministic pattern.
|
|
56
|
+
// Per-request cch: random 5-char hex value each request (Claude Code does the same).
|
|
49
57
|
function computeCch() {
|
|
50
58
|
return randomBytes(3).toString('hex').slice(0, 5);
|
|
51
59
|
}
|
|
52
|
-
// Detect installed Claude Code
|
|
53
|
-
|
|
54
|
-
function
|
|
60
|
+
// Detect installed Claude Code version for the build-tag computation.
|
|
61
|
+
// Falls back to a known-good version if claude isn't on PATH.
|
|
62
|
+
function detectCliVersion() {
|
|
55
63
|
try {
|
|
56
64
|
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
65
|
return out.match(/^([\d]+\.[\d]+\.[\d]+)/)?.[1] ?? '2.1.100';
|
|
60
66
|
}
|
|
61
67
|
catch {
|
|
62
|
-
cliAvailable = false;
|
|
63
68
|
return '2.1.100';
|
|
64
69
|
}
|
|
65
70
|
}
|
|
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
71
|
/** Extract first user message text from a request body for billing tag computation. */
|
|
97
72
|
function extractFirstUserMessage(body) {
|
|
98
73
|
const messages = body.messages;
|
|
@@ -109,42 +84,8 @@ function extractFirstUserMessage(body) {
|
|
|
109
84
|
}
|
|
110
85
|
return '';
|
|
111
86
|
}
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
87
|
+
// Session ID rotates per request — fresh UUID per invocation.
|
|
88
|
+
// A persistent session ID across many requests is a behavioral fingerprint.
|
|
148
89
|
let SESSION_ID = randomUUID();
|
|
149
90
|
const OS_NAME = platform === 'win32' ? 'Windows' : platform === 'darwin' ? 'MacOS' : 'Linux';
|
|
150
91
|
// Claude Code device identity — required for Max plan billing classification.
|
|
@@ -205,8 +146,7 @@ function filterBillableBetas(betas) {
|
|
|
205
146
|
const ORCHESTRATION_TAG_NAMES = [
|
|
206
147
|
'system-reminder', 'env', 'system_information', 'current_working_directory',
|
|
207
148
|
'operating_system', 'default_shell', 'home_directory', 'task_metadata',
|
|
208
|
-
'
|
|
209
|
-
'directories', 'available_skills', 'thinking',
|
|
149
|
+
'directories', 'thinking',
|
|
210
150
|
];
|
|
211
151
|
const ORCHESTRATION_PATTERNS = ORCHESTRATION_TAG_NAMES.flatMap(tag => [
|
|
212
152
|
new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}>`, 'gi'),
|
|
@@ -375,125 +315,9 @@ function enrich429(body, headers) {
|
|
|
375
315
|
return body;
|
|
376
316
|
}
|
|
377
317
|
}
|
|
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
318
|
export async function startProxy(opts = {}) {
|
|
496
319
|
const port = opts.port ?? DEFAULT_PORT;
|
|
320
|
+
const host = opts.host ?? process.env.DARIO_HOST ?? DEFAULT_HOST;
|
|
497
321
|
const verbose = opts.verbose ?? false;
|
|
498
322
|
const passthrough = opts.passthrough ?? false;
|
|
499
323
|
// Verify auth before starting
|
|
@@ -502,7 +326,7 @@ export async function startProxy(opts = {}) {
|
|
|
502
326
|
console.error('[dario] Not authenticated. Run `dario login` first.');
|
|
503
327
|
process.exit(1);
|
|
504
328
|
}
|
|
505
|
-
const cliVersion =
|
|
329
|
+
const cliVersion = detectCliVersion();
|
|
506
330
|
const modelOverride = opts.model ? (MODEL_ALIASES[opts.model] ?? opts.model) : null;
|
|
507
331
|
const identity = loadClaudeIdentity();
|
|
508
332
|
if (identity.deviceId) {
|
|
@@ -512,7 +336,7 @@ export async function startProxy(opts = {}) {
|
|
|
512
336
|
console.warn('[dario] WARNING: No Claude Code device identity found. Requests may be billed as Extra Usage.');
|
|
513
337
|
console.warn('[dario] Run Claude Code at least once to generate ~/.claude/.claude.json');
|
|
514
338
|
}
|
|
515
|
-
// Pre-build static headers
|
|
339
|
+
// Pre-build static headers — matches the set a real Claude Code client sends.
|
|
516
340
|
const staticHeaders = passthrough ? {
|
|
517
341
|
'accept': 'application/json',
|
|
518
342
|
'Content-Type': 'application/json',
|
|
@@ -531,18 +355,20 @@ export async function startProxy(opts = {}) {
|
|
|
531
355
|
// Claude Code runs on Bun which reports v24.3.0 as Node compat version
|
|
532
356
|
'x-stainless-runtime-version': 'v24.3.0',
|
|
533
357
|
};
|
|
534
|
-
const useCli = opts.cliBackend ?? false;
|
|
535
358
|
let requestCount = 0;
|
|
536
359
|
const semaphore = new Semaphore(MAX_CONCURRENT);
|
|
537
|
-
// Rate governor
|
|
538
|
-
//
|
|
539
|
-
// Minimum 500ms between requests — fast enough for agents, slow enough to not flag.
|
|
360
|
+
// Rate governor — minimum 500ms between requests. Fast enough for agents,
|
|
361
|
+
// slow enough to not look like a scripted flood of identical traffic.
|
|
540
362
|
let lastRequestTime = 0;
|
|
541
363
|
const MIN_REQUEST_INTERVAL_MS = parseInt(process.env.DARIO_MIN_INTERVAL_MS || '500', 10);
|
|
542
364
|
// Optional proxy authentication — pre-encode key buffer for performance
|
|
543
365
|
const apiKey = process.env.DARIO_API_KEY;
|
|
544
366
|
const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
|
|
545
|
-
|
|
367
|
+
// CORS origin defaults to the localhost URL the proxy is served at. Users
|
|
368
|
+
// binding to a non-loopback address (e.g. a Tailscale interface) can
|
|
369
|
+
// override via DARIO_CORS_ORIGIN — otherwise browser-based clients hitting
|
|
370
|
+
// dario over the mesh will be blocked by their browser's CORS check.
|
|
371
|
+
const corsOrigin = process.env.DARIO_CORS_ORIGIN || `http://localhost:${port}`;
|
|
546
372
|
// Security headers for all responses
|
|
547
373
|
const SECURITY_HEADERS = {
|
|
548
374
|
'X-Content-Type-Options': 'nosniff',
|
|
@@ -656,31 +482,6 @@ export async function startProxy(opts = {}) {
|
|
|
656
482
|
clearTimeout(bodyTimeout);
|
|
657
483
|
}
|
|
658
484
|
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
485
|
// Parse body once, apply OpenAI translation, model override, and sanitization
|
|
685
486
|
let finalBody = body.length > 0 ? body : undefined;
|
|
686
487
|
let ccToolMap = null;
|
|
@@ -731,7 +532,7 @@ export async function startProxy(opts = {}) {
|
|
|
731
532
|
beta += ',' + clientBeta;
|
|
732
533
|
}
|
|
733
534
|
else {
|
|
734
|
-
// CC v2.1.104 beta set
|
|
535
|
+
// CC v2.1.104 beta set — 8 flags in the order Claude Code sends them.
|
|
735
536
|
// context-1m requires Extra Usage — if it 400s, we auto-retry without it.
|
|
736
537
|
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
538
|
if (clientBeta) {
|
|
@@ -749,7 +550,7 @@ export async function startProxy(opts = {}) {
|
|
|
749
550
|
await new Promise(r => setTimeout(r, MIN_REQUEST_INTERVAL_MS - elapsed));
|
|
750
551
|
}
|
|
751
552
|
lastRequestTime = Date.now();
|
|
752
|
-
// Rotate session ID per request —
|
|
553
|
+
// Rotate session ID per request — fresh UUID avoids persistent-session fingerprinting
|
|
753
554
|
SESSION_ID = randomUUID();
|
|
754
555
|
const headers = {
|
|
755
556
|
...staticHeaders,
|
|
@@ -798,24 +599,20 @@ export async function startProxy(opts = {}) {
|
|
|
798
599
|
else if (upstream.status === 429) {
|
|
799
600
|
// Not a context-1m issue — return enriched 429 directly
|
|
800
601
|
const enriched = enrich429(peekedBody, upstream.headers);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
responseHeaders[key] = value;
|
|
810
|
-
}
|
|
602
|
+
const responseHeaders = {
|
|
603
|
+
'Content-Type': 'application/json',
|
|
604
|
+
'Access-Control-Allow-Origin': corsOrigin,
|
|
605
|
+
...SECURITY_HEADERS,
|
|
606
|
+
};
|
|
607
|
+
for (const [key, value] of upstream.headers.entries()) {
|
|
608
|
+
if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
|
|
609
|
+
responseHeaders[key] = value;
|
|
811
610
|
}
|
|
812
|
-
requestCount++;
|
|
813
|
-
res.writeHead(429, responseHeaders);
|
|
814
|
-
res.end(enriched);
|
|
815
|
-
return;
|
|
816
611
|
}
|
|
817
|
-
|
|
818
|
-
|
|
612
|
+
requestCount++;
|
|
613
|
+
res.writeHead(429, responseHeaders);
|
|
614
|
+
res.end(enriched);
|
|
615
|
+
return;
|
|
819
616
|
}
|
|
820
617
|
else if (upstream.status === 400) {
|
|
821
618
|
// Non-long-context 400 — forward upstream error directly.
|
|
@@ -836,7 +633,7 @@ export async function startProxy(opts = {}) {
|
|
|
836
633
|
}
|
|
837
634
|
}
|
|
838
635
|
// Enrich 429 errors with rate limit details from headers (Anthropic only returns "Error")
|
|
839
|
-
if (upstream.status === 429
|
|
636
|
+
if (upstream.status === 429) {
|
|
840
637
|
const errBody = await upstream.text().catch(() => '');
|
|
841
638
|
const enriched = enrich429(errBody, upstream.headers);
|
|
842
639
|
const responseHeaders = {
|
|
@@ -854,42 +651,6 @@ export async function startProxy(opts = {}) {
|
|
|
854
651
|
res.end(enriched);
|
|
855
652
|
return;
|
|
856
653
|
}
|
|
857
|
-
// Auto-fallback: if API returns 429 and CLI is available, retry through CLI binary
|
|
858
|
-
if (upstream.status === 429 && cliAvailable && !useCli) {
|
|
859
|
-
const errBody429 = await upstream.text().catch(() => '');
|
|
860
|
-
if (verbose)
|
|
861
|
-
console.log(`[dario] #${requestCount} 429 from API — falling back to CLI`);
|
|
862
|
-
let clientWantsStream = false;
|
|
863
|
-
try {
|
|
864
|
-
clientWantsStream = !!JSON.parse(body.toString()).stream;
|
|
865
|
-
}
|
|
866
|
-
catch { }
|
|
867
|
-
const cliResult = await handleViaCli(body, modelOverride, verbose);
|
|
868
|
-
// If CLI fallback also failed, return the original 429 with enriched details
|
|
869
|
-
// instead of a cryptic 502 from CLI failure
|
|
870
|
-
if (cliResult.status >= 500) {
|
|
871
|
-
if (verbose)
|
|
872
|
-
console.log(`[dario] #${requestCount} CLI fallback failed (${cliResult.status}) — returning original 429`);
|
|
873
|
-
const enriched = enrich429(errBody429, upstream.headers);
|
|
874
|
-
const responseHeaders = {
|
|
875
|
-
'Content-Type': 'application/json',
|
|
876
|
-
'Access-Control-Allow-Origin': corsOrigin,
|
|
877
|
-
...SECURITY_HEADERS,
|
|
878
|
-
};
|
|
879
|
-
for (const [key, value] of upstream.headers.entries()) {
|
|
880
|
-
if (key.startsWith('x-ratelimit') || key.startsWith('anthropic-ratelimit') || key === 'request-id') {
|
|
881
|
-
responseHeaders[key] = value;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
requestCount++;
|
|
885
|
-
res.writeHead(429, responseHeaders);
|
|
886
|
-
res.end(enriched);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
requestCount++;
|
|
890
|
-
sendCliResponse(res, cliResult, clientWantsStream, isOpenAI, corsOrigin, SECURITY_HEADERS);
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
654
|
// Detect streaming from content-type (reliable) or body (fallback)
|
|
894
655
|
const contentType = upstream.headers.get('content-type') ?? '';
|
|
895
656
|
const isStream = contentType.includes('text/event-stream');
|
|
@@ -1007,24 +768,43 @@ export async function startProxy(opts = {}) {
|
|
|
1007
768
|
}
|
|
1008
769
|
process.exit(1);
|
|
1009
770
|
});
|
|
1010
|
-
server.listen(port,
|
|
1011
|
-
const modeLine = passthrough
|
|
771
|
+
server.listen(port, host, () => {
|
|
772
|
+
const modeLine = passthrough
|
|
773
|
+
? 'Mode: passthrough (OAuth swap only, no injection)'
|
|
774
|
+
: `OAuth: ${status.status} (expires in ${status.expiresIn})`;
|
|
1012
775
|
const modelLine = modelOverride ? `Model: ${modelOverride} (all requests)` : 'Model: passthrough (client decides)';
|
|
776
|
+
// Display URL uses `localhost` for loopback binds and the literal host
|
|
777
|
+
// for exposed binds, so the printed URL is the one a client would
|
|
778
|
+
// actually use to reach the proxy.
|
|
779
|
+
const displayHost = isLoopbackHost(host) ? 'localhost' : host;
|
|
1013
780
|
console.log('');
|
|
1014
|
-
console.log(` dario — http
|
|
781
|
+
console.log(` dario — http://${displayHost}:${port}`);
|
|
1015
782
|
console.log('');
|
|
1016
783
|
console.log(' Your Claude subscription is now an API.');
|
|
1017
784
|
console.log('');
|
|
1018
785
|
console.log(' Usage:');
|
|
1019
|
-
console.log(` ANTHROPIC_BASE_URL=http
|
|
786
|
+
console.log(` ANTHROPIC_BASE_URL=http://${displayHost}:${port}`);
|
|
1020
787
|
console.log(' ANTHROPIC_API_KEY=dario');
|
|
1021
788
|
console.log('');
|
|
1022
789
|
console.log(` ${modeLine}`);
|
|
1023
790
|
console.log(` ${modelLine}`);
|
|
791
|
+
if (!isLoopbackHost(host)) {
|
|
792
|
+
console.log('');
|
|
793
|
+
console.log(` ⚠ Bound to ${host} — reachable from other machines on the network.`);
|
|
794
|
+
if (!apiKey) {
|
|
795
|
+
console.log(' DARIO_API_KEY is not set. Any host that can reach this port can');
|
|
796
|
+
console.log(' proxy requests through your OAuth subscription. Set DARIO_API_KEY');
|
|
797
|
+
console.log(' before exposing dario beyond loopback.');
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
console.log(' DARIO_API_KEY is set — clients must send x-api-key or Authorization');
|
|
801
|
+
console.log(' to be accepted.');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
1024
804
|
console.log('');
|
|
1025
805
|
});
|
|
1026
|
-
// Session presence heartbeat —
|
|
1027
|
-
//
|
|
806
|
+
// Session presence heartbeat — keeps the OAuth session marked active
|
|
807
|
+
// (matches the ~5s cadence of a real Claude Code session).
|
|
1028
808
|
const clientId = randomUUID();
|
|
1029
809
|
const connectedAt = new Date().toISOString();
|
|
1030
810
|
let lastPresencePulse = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
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": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"start": "node dist/cli.js",
|
|
27
27
|
"dev": "tsx src/cli.ts",
|
|
28
28
|
"e2e": "node test/e2e.mjs",
|
|
29
|
-
"compat": "node test/compat.mjs"
|
|
29
|
+
"compat": "node test/compat.mjs",
|
|
30
|
+
"lint:pkg": "node scripts/check-package-json.mjs",
|
|
31
|
+
"fix:pkg": "node -e \"const fs=require('fs');fs.writeFileSync('package.json',JSON.stringify(JSON.parse(fs.readFileSync('package.json','utf-8')),null,2)+'\\n')\""
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
32
34
|
"claude",
|