@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/README.md +158 -137
- package/dist/cc-oauth-detect.d.ts +47 -0
- package/dist/cc-oauth-detect.js +232 -0
- package/dist/cc-template.d.ts +4 -4
- package/dist/cc-template.js +5 -5
- package/dist/cli.js +3 -5
- package/dist/oauth.js +21 -14
- package/dist/proxy.d.ts +0 -1
- package/dist/proxy.js +73 -296
- package/package.json +1 -1
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';
|
|
@@ -36,63 +36,29 @@ class Semaphore {
|
|
|
36
36
|
next();
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
// Billing tag hash seed —
|
|
39
|
+
// Billing tag hash seed — matches Claude Code's value
|
|
40
40
|
const BILLING_SEED = '59cf53e54c78';
|
|
41
|
-
// Compute per-request build tag
|
|
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:
|
|
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
|
|
53
|
-
|
|
54
|
-
function
|
|
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
|
-
|
|
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.
|
|
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
|
-
'
|
|
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 =
|
|
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
|
|
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
|
|
538
|
-
//
|
|
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
|
|
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 —
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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 —
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 —
|
|
999
|
-
//
|
|
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;
|