@askalf/dario 5.4.27 → 5.4.29
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/health-response.d.ts +12 -0
- package/dist/health-response.js +1 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +20 -14
- package/dist/stream-drain.d.ts +34 -0
- package/dist/stream-drain.js +42 -0
- package/package.json +1 -1
|
@@ -34,6 +34,18 @@ export interface HealthStatusLike {
|
|
|
34
34
|
mode: 'single';
|
|
35
35
|
active: number;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Request-queue snapshot (dario#905), surfaced to internal callers only.
|
|
39
|
+
* `active === maxConcurrent` with `queued > 0` for a sustained period is
|
|
40
|
+
* the slot-exhaustion signature — before this field existed, that state
|
|
41
|
+
* was invisible from outside the process while every request 504'd.
|
|
42
|
+
*/
|
|
43
|
+
queue?: {
|
|
44
|
+
active: number;
|
|
45
|
+
queued: number;
|
|
46
|
+
maxConcurrent: number;
|
|
47
|
+
maxQueued: number;
|
|
48
|
+
};
|
|
37
49
|
}
|
|
38
50
|
export interface HealthResponse {
|
|
39
51
|
httpStatus: number;
|
package/dist/health-response.js
CHANGED
|
@@ -82,6 +82,7 @@ export function buildHealthResponse(s, requestCount, includeInternal) {
|
|
|
82
82
|
expiresIn: s.expiresIn,
|
|
83
83
|
requests: requestCount,
|
|
84
84
|
...(s.sessions ? { sessions: s.sessions } : {}),
|
|
85
|
+
...(s.queue ? { queue: s.queue } : {}),
|
|
85
86
|
...(s.refreshFailures ? { refreshFailures: s.refreshFailures } : {}),
|
|
86
87
|
}
|
|
87
88
|
: liveness;
|
|
@@ -354,7 +354,7 @@ export declare function detectDrift(t: TemplateData, installedOverride?: string
|
|
|
354
354
|
*/
|
|
355
355
|
export declare const SUPPORTED_CC_RANGE: {
|
|
356
356
|
readonly min: "1.0.0";
|
|
357
|
-
readonly maxTested: "2.1.
|
|
357
|
+
readonly maxTested: "2.1.222";
|
|
358
358
|
};
|
|
359
359
|
/**
|
|
360
360
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -969,7 +969,7 @@ export function detectDrift(t, installedOverride) {
|
|
|
969
969
|
*/
|
|
970
970
|
export const SUPPORTED_CC_RANGE = {
|
|
971
971
|
min: '1.0.0',
|
|
972
|
-
maxTested: '2.1.
|
|
972
|
+
maxTested: '2.1.222',
|
|
973
973
|
};
|
|
974
974
|
/**
|
|
975
975
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/proxy.d.ts
CHANGED
|
@@ -345,6 +345,12 @@ interface ProxyOptions {
|
|
|
345
345
|
maxQueued?: number;
|
|
346
346
|
/** Max ms a queued request waits before it times out with 504. Default 60000. dario#80. */
|
|
347
347
|
queueTimeoutMs?: number;
|
|
348
|
+
/**
|
|
349
|
+
* Max ms before the upstream fetch is aborted. Default 300000 (5 min,
|
|
350
|
+
* matching the Anthropic SDK). Injectable so tests can exercise the
|
|
351
|
+
* timeout → slot-release path without waiting 5 minutes (dario#905).
|
|
352
|
+
*/
|
|
353
|
+
upstreamTimeoutMs?: number;
|
|
348
354
|
/**
|
|
349
355
|
* Override the outbound `output_config.effort` value on non-haiku
|
|
350
356
|
* requests. Default (undefined) pins `'high'`, matching CC 2.1.116's
|
package/dist/proxy.js
CHANGED
|
@@ -1426,6 +1426,7 @@ export async function startProxy(opts = {}) {
|
|
|
1426
1426
|
maxQueued: opts.maxQueued ?? DEFAULT_MAX_QUEUED,
|
|
1427
1427
|
queueTimeoutMs: opts.queueTimeoutMs ?? DEFAULT_QUEUE_TIMEOUT_MS,
|
|
1428
1428
|
});
|
|
1429
|
+
const upstreamTimeoutMs = opts.upstreamTimeoutMs ?? UPSTREAM_TIMEOUT_MS;
|
|
1429
1430
|
// Cache context-1m beta availability. Set false once per account after the
|
|
1430
1431
|
// first "long context" rejection, so we skip sending context-1m on every
|
|
1431
1432
|
// subsequent request instead of paying the round-trip + retry cost each time.
|
|
@@ -1528,7 +1529,7 @@ export async function startProxy(opts = {}) {
|
|
|
1528
1529
|
// disconnect no longer aborts the upstream fetch — we keep consuming
|
|
1529
1530
|
// the SSE so Anthropic sees a CC-shaped read-to-EOF pattern. See
|
|
1530
1531
|
// src/stream-drain.ts for the rationale + tradeoff.
|
|
1531
|
-
const { decideOnClientClose, resolveDrainOnClose } = await import('./stream-drain.js');
|
|
1532
|
+
const { decideOnClientClose, resolveDrainOnClose, waitForClientDrain } = await import('./stream-drain.js');
|
|
1532
1533
|
const drainOnClose = resolveDrainOnClose(opts.drainOnClose);
|
|
1533
1534
|
if (verbose) {
|
|
1534
1535
|
console.log(`[dario] drain-on-close: ${drainOnClose ? 'enabled' : 'disabled'}`);
|
|
@@ -1709,6 +1710,9 @@ export async function startProxy(opts = {}) {
|
|
|
1709
1710
|
sessions: pool.size === 0
|
|
1710
1711
|
? { mode: 'single', active: sessionRegistry.size() }
|
|
1711
1712
|
: { mode: 'pool', stickyBindings: pool.stickyCount() },
|
|
1713
|
+
// Concurrency-slot visibility (dario#905): active pinned at
|
|
1714
|
+
// maxConcurrent with queued > 0 is the slot-exhaustion signature.
|
|
1715
|
+
queue: queue.snapshot(),
|
|
1712
1716
|
}, requestCount, includeInternal);
|
|
1713
1717
|
res.writeHead(httpStatus, JSON_HEADERS);
|
|
1714
1718
|
res.end(JSON.stringify(body));
|
|
@@ -1856,7 +1860,10 @@ export async function startProxy(opts = {}) {
|
|
|
1856
1860
|
// Always-on as of v4 (pre-v4 this was gated to pool mode).
|
|
1857
1861
|
if (urlPath === '/analytics' && req.method === 'GET') {
|
|
1858
1862
|
res.writeHead(200, JSON_HEADERS);
|
|
1859
|
-
|
|
1863
|
+
// `queue` rides along the summary (dario#905): request-queue.ts always
|
|
1864
|
+
// documented snapshot() as "exposed for /analytics", but it was never
|
|
1865
|
+
// actually wired in, so slot exhaustion was invisible from outside.
|
|
1866
|
+
res.end(JSON.stringify({ ...analytics.summary(), queue: queue.snapshot() }));
|
|
1860
1867
|
return;
|
|
1861
1868
|
}
|
|
1862
1869
|
// Analytics live stream — SSE of new RequestRecord JSON, one event
|
|
@@ -2278,7 +2285,7 @@ export async function startProxy(opts = {}) {
|
|
|
2278
2285
|
console.log(`[dario] #${requestCount} ${req.method} ${urlPath} (model: ${rawModel}) → openai backend`);
|
|
2279
2286
|
}
|
|
2280
2287
|
requestCount++;
|
|
2281
|
-
await forwardToOpenAI(req, res, body, openaiBackend, corsOrigin, SECURITY_HEADERS,
|
|
2288
|
+
await forwardToOpenAI(req, res, body, openaiBackend, corsOrigin, SECURITY_HEADERS, upstreamTimeoutMs, verbose);
|
|
2282
2289
|
return;
|
|
2283
2290
|
}
|
|
2284
2291
|
}
|
|
@@ -2303,7 +2310,7 @@ export async function startProxy(opts = {}) {
|
|
|
2303
2310
|
}
|
|
2304
2311
|
console.log(`[dario] #${requestCount} pool exhausted — /v1/chat/completions → ${openaiBackend.name} as ${poolFallbackModel}`);
|
|
2305
2312
|
requestCount++;
|
|
2306
|
-
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel },
|
|
2313
|
+
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel }, upstreamTimeoutMs, verbose);
|
|
2307
2314
|
return;
|
|
2308
2315
|
}
|
|
2309
2316
|
// Parse body once, apply OpenAI translation, model override, and sanitization
|
|
@@ -2750,7 +2757,7 @@ export async function startProxy(opts = {}) {
|
|
|
2750
2757
|
upstreamAbortReason = 'timeout';
|
|
2751
2758
|
upstreamAbort.abort();
|
|
2752
2759
|
}
|
|
2753
|
-
},
|
|
2760
|
+
}, upstreamTimeoutMs);
|
|
2754
2761
|
onClientClose = () => {
|
|
2755
2762
|
const action = decideOnClientClose(res.writableEnded, upstreamAbort.signal.aborted, drainOnClose);
|
|
2756
2763
|
if (action === 'abort') {
|
|
@@ -3177,7 +3184,7 @@ export async function startProxy(opts = {}) {
|
|
|
3177
3184
|
if (fallbackBody) {
|
|
3178
3185
|
console.log(`[dario] #${requestCount} pool exhausted mid-flight (429, no peer) — /v1/chat/completions → ${openaiBackend.name} as ${poolFallbackModel}`);
|
|
3179
3186
|
requestCount++;
|
|
3180
|
-
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel },
|
|
3187
|
+
await forwardToOpenAI(req, res, fallbackBody, openaiBackend, corsOrigin, { ...SECURITY_HEADERS, 'x-dario-pool-fallback': poolFallbackModel }, upstreamTimeoutMs, verbose);
|
|
3181
3188
|
return;
|
|
3182
3189
|
}
|
|
3183
3190
|
}
|
|
@@ -3320,12 +3327,11 @@ export async function startProxy(opts = {}) {
|
|
|
3320
3327
|
if (res.write(chunk) === false)
|
|
3321
3328
|
needsDrain = true;
|
|
3322
3329
|
};
|
|
3323
|
-
// Resolves on 'close'
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
});
|
|
3330
|
+
// Resolves on 'close' (vanished client) AND on upstream abort. The
|
|
3331
|
+
// abort arm is what keeps a connected-but-not-reading client from
|
|
3332
|
+
// parking this handler forever and leaking its queue slot — the
|
|
3333
|
+
// dario#905 wedge. See waitForClientDrain in src/stream-drain.ts.
|
|
3334
|
+
const waitForDrain = () => waitForClientDrain(res, upstreamAbort.signal);
|
|
3329
3335
|
try {
|
|
3330
3336
|
let buffer = '';
|
|
3331
3337
|
const MAX_LINE_LENGTH = 1_000_000; // 1MB max per SSE line
|
|
@@ -3567,10 +3573,10 @@ export async function startProxy(opts = {}) {
|
|
|
3567
3573
|
writeLogLine(logFileStream, { ...errLogBase, reject: 'client-closed' });
|
|
3568
3574
|
}
|
|
3569
3575
|
else if (upstreamAbortReason === 'timeout') {
|
|
3570
|
-
console.error(`[dario] #${requestCount} upstream timeout after ${
|
|
3576
|
+
console.error(`[dario] #${requestCount} upstream timeout after ${upstreamTimeoutMs / 1000}s`);
|
|
3571
3577
|
if (!res.headersSent) {
|
|
3572
3578
|
res.writeHead(504, JSON_HEADERS);
|
|
3573
|
-
res.end(JSON.stringify({ error: 'Upstream timeout', message: `Anthropic did not respond within ${
|
|
3579
|
+
res.end(JSON.stringify({ error: 'Upstream timeout', message: `Anthropic did not respond within ${upstreamTimeoutMs / 1000}s` }));
|
|
3574
3580
|
}
|
|
3575
3581
|
else if (!res.writableEnded) {
|
|
3576
3582
|
res.end();
|
package/dist/stream-drain.d.ts
CHANGED
|
@@ -58,3 +58,37 @@ export declare function decideOnClientClose(writableEnded: boolean, upstreamAbor
|
|
|
58
58
|
* Explicit `true`/`false` on the options object always wins.
|
|
59
59
|
*/
|
|
60
60
|
export declare function resolveDrainOnClose(explicit: boolean | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Minimal event surface of `http.ServerResponse` that the drain wait needs —
|
|
63
|
+
* structural so tests can pass a plain EventEmitter.
|
|
64
|
+
*/
|
|
65
|
+
export interface DrainWaitable {
|
|
66
|
+
once(event: 'drain' | 'close', listener: () => void): unknown;
|
|
67
|
+
off(event: 'drain' | 'close', listener: () => void): unknown;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Wait out client-socket backpressure after `res.write()` returned false
|
|
71
|
+
* (dario#905). Settles on the first of:
|
|
72
|
+
*
|
|
73
|
+
* - `'drain'` on `res` — the client consumed its buffer; resume streaming.
|
|
74
|
+
* - `'close'` on `res` — the client connection is gone; the loop resumes
|
|
75
|
+
* and the gated writer / next read handles teardown.
|
|
76
|
+
* - `signal` aborts — the upstream AbortController fired (upstream
|
|
77
|
+
* timeout, client-close abort, SSE overflow).
|
|
78
|
+
*
|
|
79
|
+
* The abort arm is the load-bearing one. Without it, a client that stays
|
|
80
|
+
* connected but stops reading (dead peer behind an open TCP window, wedged
|
|
81
|
+
* consumer) parks the request handler on this promise forever: the upstream
|
|
82
|
+
* timeout still fires, but it only aborts the upstream fetch — which nothing
|
|
83
|
+
* is awaiting — so the handler's `finally` never runs and its concurrency
|
|
84
|
+
* slot leaks. Leaked slots accumulate until `active === maxConcurrent`, at
|
|
85
|
+
* which point every request 504s with `queue-timeout` while `/health` stays
|
|
86
|
+
* green (dario#905). With the abort arm, the wait resolves, the next
|
|
87
|
+
* `reader.read()` rejects on the aborted body, and normal teardown releases
|
|
88
|
+
* the slot.
|
|
89
|
+
*
|
|
90
|
+
* Resolves immediately when the signal is already aborted, and removes all
|
|
91
|
+
* three listeners on settle so repeated waits within one response never
|
|
92
|
+
* accumulate listeners on the shared signal.
|
|
93
|
+
*/
|
|
94
|
+
export declare function waitForClientDrain(res: DrainWaitable, signal: AbortSignal): Promise<void>;
|
package/dist/stream-drain.js
CHANGED
|
@@ -66,3 +66,45 @@ export function resolveDrainOnClose(explicit, env = process.env) {
|
|
|
66
66
|
const v = (env.DARIO_DRAIN_ON_CLOSE ?? '').toLowerCase();
|
|
67
67
|
return v === '1' || v === 'true' || v === 'yes';
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Wait out client-socket backpressure after `res.write()` returned false
|
|
71
|
+
* (dario#905). Settles on the first of:
|
|
72
|
+
*
|
|
73
|
+
* - `'drain'` on `res` — the client consumed its buffer; resume streaming.
|
|
74
|
+
* - `'close'` on `res` — the client connection is gone; the loop resumes
|
|
75
|
+
* and the gated writer / next read handles teardown.
|
|
76
|
+
* - `signal` aborts — the upstream AbortController fired (upstream
|
|
77
|
+
* timeout, client-close abort, SSE overflow).
|
|
78
|
+
*
|
|
79
|
+
* The abort arm is the load-bearing one. Without it, a client that stays
|
|
80
|
+
* connected but stops reading (dead peer behind an open TCP window, wedged
|
|
81
|
+
* consumer) parks the request handler on this promise forever: the upstream
|
|
82
|
+
* timeout still fires, but it only aborts the upstream fetch — which nothing
|
|
83
|
+
* is awaiting — so the handler's `finally` never runs and its concurrency
|
|
84
|
+
* slot leaks. Leaked slots accumulate until `active === maxConcurrent`, at
|
|
85
|
+
* which point every request 504s with `queue-timeout` while `/health` stays
|
|
86
|
+
* green (dario#905). With the abort arm, the wait resolves, the next
|
|
87
|
+
* `reader.read()` rejects on the aborted body, and normal teardown releases
|
|
88
|
+
* the slot.
|
|
89
|
+
*
|
|
90
|
+
* Resolves immediately when the signal is already aborted, and removes all
|
|
91
|
+
* three listeners on settle so repeated waits within one response never
|
|
92
|
+
* accumulate listeners on the shared signal.
|
|
93
|
+
*/
|
|
94
|
+
export function waitForClientDrain(res, signal) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
if (signal.aborted) {
|
|
97
|
+
resolve();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const done = () => {
|
|
101
|
+
res.off('drain', done);
|
|
102
|
+
res.off('close', done);
|
|
103
|
+
signal.removeEventListener('abort', done);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
res.once('drain', done);
|
|
107
|
+
res.once('close', done);
|
|
108
|
+
signal.addEventListener('abort', done);
|
|
109
|
+
});
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.29",
|
|
4
4
|
"description": "Use your Claude Pro/Max subscription in any tool — Cursor, Cline, Aider, the Agent SDK, your scripts — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|