@askalf/dario 4.8.121 → 4.8.123
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/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +11 -2
- package/dist/pool.d.ts +1 -0
- package/dist/pool.js +46 -16
- package/dist/proxy.js +27 -2
- package/package.json +1 -1
package/dist/model-catalog.d.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* — the same ordering normalizeUpstreamIds() produces for live data.
|
|
33
33
|
*/
|
|
34
34
|
export declare const BAKED_BASE_MODELS: readonly string[];
|
|
35
|
-
/** The set of suspended families, resolved from env
|
|
35
|
+
/** The set of suspended families, resolved from env (memoized by raw value). */
|
|
36
36
|
export declare function suspendedFamilies(): Set<string>;
|
|
37
37
|
/** True when `model` (any spelling) belongs to a suspended family. */
|
|
38
38
|
export declare function isSuspendedModel(model: string): boolean;
|
package/dist/model-catalog.js
CHANGED
|
@@ -67,14 +67,23 @@ export const BAKED_BASE_MODELS = [
|
|
|
67
67
|
* to re-suspend if access is ever pulled again.
|
|
68
68
|
*/
|
|
69
69
|
const DEFAULT_SUSPENDED_MODELS = '';
|
|
70
|
-
|
|
70
|
+
// Memoized by the raw env value (#642-audit): isSuspendedModel runs per request,
|
|
71
|
+
// and this rebuilt a Set from process.env every call. Keyed on the raw string so
|
|
72
|
+
// a runtime change to DARIO_SUSPENDED_MODELS still re-parses; the common (empty)
|
|
73
|
+
// value allocates the Set once.
|
|
74
|
+
let _suspendedCache = null;
|
|
75
|
+
/** The set of suspended families, resolved from env (memoized by raw value). */
|
|
71
76
|
export function suspendedFamilies() {
|
|
72
77
|
const raw = process.env.DARIO_SUSPENDED_MODELS ?? DEFAULT_SUSPENDED_MODELS;
|
|
73
|
-
|
|
78
|
+
if (_suspendedCache && _suspendedCache.raw === raw)
|
|
79
|
+
return _suspendedCache.set;
|
|
80
|
+
const set = new Set(raw
|
|
74
81
|
.split(',')
|
|
75
82
|
.map((s) => s.trim())
|
|
76
83
|
.filter((s) => s.length > 0)
|
|
77
84
|
.map((s) => modelFamily(s) ?? s.toLowerCase()));
|
|
85
|
+
_suspendedCache = { raw, set };
|
|
86
|
+
return set;
|
|
78
87
|
}
|
|
79
88
|
/** True when `model` (any spelling) belongs to a suspended family. */
|
|
80
89
|
export function isSuspendedModel(model) {
|
package/dist/pool.d.ts
CHANGED
package/dist/pool.js
CHANGED
|
@@ -142,6 +142,7 @@ export function computeHeadroom(snapshot, family) {
|
|
|
142
142
|
}
|
|
143
143
|
const STICKY_TTL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
144
144
|
const STICKY_MAX_ENTRIES = 2_000; // lazy cleanup cap
|
|
145
|
+
const STICKY_CLEANUP_INTERVAL_MS = 30_000; // amortize the O(n) TTL/orphan sweep
|
|
145
146
|
/**
|
|
146
147
|
* Headroom floor under which an account is treated as "effectively exhausted"
|
|
147
148
|
* for routing decisions. A sticky binding whose account drops below this
|
|
@@ -150,6 +151,21 @@ const STICKY_MAX_ENTRIES = 2_000; // lazy cleanup cap
|
|
|
150
151
|
* loop stops once every candidate is below it. 0.02 == 2%.
|
|
151
152
|
*/
|
|
152
153
|
const POOL_HEADROOM_FLOOR = 0.02;
|
|
154
|
+
// Pick the account with the most headroom in a single pass. The prior
|
|
155
|
+
// `.reduce()` form recomputed the incumbent's headroom every iteration
|
|
156
|
+
// (~2n computeHeadroom calls); this computes each once (#642-audit).
|
|
157
|
+
function pickMaxHeadroom(accounts, family) {
|
|
158
|
+
let best = accounts[0];
|
|
159
|
+
let bestHeadroom = computeHeadroom(best.rateLimit, family);
|
|
160
|
+
for (let i = 1; i < accounts.length; i++) {
|
|
161
|
+
const h = computeHeadroom(accounts[i].rateLimit, family);
|
|
162
|
+
if (h > bestHeadroom) {
|
|
163
|
+
best = accounts[i];
|
|
164
|
+
bestHeadroom = h;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return best;
|
|
168
|
+
}
|
|
153
169
|
export class AccountPool {
|
|
154
170
|
accounts = new Map();
|
|
155
171
|
queue = [];
|
|
@@ -157,6 +173,8 @@ export class AccountPool {
|
|
|
157
173
|
queueTimeoutMs = 60_000;
|
|
158
174
|
drainTimer = null;
|
|
159
175
|
sticky = new Map();
|
|
176
|
+
// Amortize the O(n) sticky TTL/orphan sweep — timestamp of the last run.
|
|
177
|
+
lastStickyCleanup = 0;
|
|
160
178
|
add(alias, opts) {
|
|
161
179
|
const existing = this.accounts.get(alias);
|
|
162
180
|
this.accounts.set(alias, {
|
|
@@ -194,8 +212,17 @@ export class AccountPool {
|
|
|
194
212
|
const account = this.accounts.get(alias);
|
|
195
213
|
if (!account)
|
|
196
214
|
return;
|
|
197
|
-
|
|
198
|
-
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
// Escalate the exponential cool-down only for a genuinely fresh failure.
|
|
217
|
+
// A burst of concurrent in-flight requests that all 401 on the same account
|
|
218
|
+
// (before the first cool-down takes hold) would otherwise bump the counter
|
|
219
|
+
// k times and jump the window to authCooldownMs(k) instead of 60s
|
|
220
|
+
// (#642-audit). isInAuthCooldown reflects state BEFORE this failure, so the
|
|
221
|
+
// burst escalates once; always refresh the timestamp to hold the window.
|
|
222
|
+
if (!isInAuthCooldown(account, now)) {
|
|
223
|
+
account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
|
|
224
|
+
}
|
|
225
|
+
account.lastAuthFailureAt = now;
|
|
199
226
|
}
|
|
200
227
|
/**
|
|
201
228
|
* Clear an account's auth-failure cool-down. Called by the proxy after a
|
|
@@ -230,11 +257,7 @@ export class AccountPool {
|
|
|
230
257
|
a.expiresAt > now + 30_000 &&
|
|
231
258
|
!isInAuthCooldown(a, now));
|
|
232
259
|
if (eligible.length > 0) {
|
|
233
|
-
return eligible
|
|
234
|
-
const bestHeadroom = computeHeadroom(best.rateLimit, family);
|
|
235
|
-
const currHeadroom = computeHeadroom(curr.rateLimit, family);
|
|
236
|
-
return currHeadroom > bestHeadroom ? curr : best;
|
|
237
|
-
});
|
|
260
|
+
return pickMaxHeadroom(eligible, family);
|
|
238
261
|
}
|
|
239
262
|
// All accounts exhausted — return the one with the earliest reset.
|
|
240
263
|
// Auth-cooldown'd accounts are excluded from this fallback too: we
|
|
@@ -310,14 +333,25 @@ export class AccountPool {
|
|
|
310
333
|
*/
|
|
311
334
|
cleanupSticky() {
|
|
312
335
|
const now = Date.now();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
336
|
+
// TTL/orphan sweep is O(n); amortize it — run at most once per
|
|
337
|
+
// STICKY_CLEANUP_INTERVAL_MS instead of on every selectSticky (#642-audit).
|
|
338
|
+
// Stale bindings are never wrongly USED meanwhile: selectSticky re-validates
|
|
339
|
+
// a binding's expiry/rejection/headroom before returning it.
|
|
340
|
+
if (now - this.lastStickyCleanup >= STICKY_CLEANUP_INTERVAL_MS) {
|
|
341
|
+
this.lastStickyCleanup = now;
|
|
342
|
+
for (const [key, b] of this.sticky) {
|
|
343
|
+
if (!this.accounts.has(b.alias) || now - b.boundAt > STICKY_TTL_MS) {
|
|
344
|
+
this.sticky.delete(key);
|
|
345
|
+
}
|
|
316
346
|
}
|
|
317
347
|
}
|
|
348
|
+
// Hard size cap always enforced (bounds memory). Batch-evict down to 80% so
|
|
349
|
+
// the O(n log n) sort amortizes over many inserts rather than firing on every
|
|
350
|
+
// new conversation at the cap (#642-audit).
|
|
318
351
|
if (this.sticky.size > STICKY_MAX_ENTRIES) {
|
|
352
|
+
const target = Math.floor(STICKY_MAX_ENTRIES * 0.8);
|
|
319
353
|
const sorted = [...this.sticky.entries()].sort((a, b) => a[1].boundAt - b[1].boundAt);
|
|
320
|
-
const toDrop = sorted.slice(0, this.sticky.size -
|
|
354
|
+
const toDrop = sorted.slice(0, this.sticky.size - target);
|
|
321
355
|
for (const [key] of toDrop)
|
|
322
356
|
this.sticky.delete(key);
|
|
323
357
|
}
|
|
@@ -340,11 +374,7 @@ export class AccountPool {
|
|
|
340
374
|
a.expiresAt > now + 30_000 &&
|
|
341
375
|
!isInAuthCooldown(a, now));
|
|
342
376
|
if (eligible.length > 0) {
|
|
343
|
-
return eligible
|
|
344
|
-
const bestHeadroom = computeHeadroom(best.rateLimit, family);
|
|
345
|
-
const currHeadroom = computeHeadroom(curr.rateLimit, family);
|
|
346
|
-
return currHeadroom > bestHeadroom ? curr : best;
|
|
347
|
-
});
|
|
377
|
+
return pickMaxHeadroom(eligible, family);
|
|
348
378
|
}
|
|
349
379
|
if (candidates.length > 0) {
|
|
350
380
|
return candidates.reduce((a, b) => a.requestCount < b.requestCount ? a : b);
|
package/dist/proxy.js
CHANGED
|
@@ -455,6 +455,21 @@ function sanitizeContent(text, patterns) {
|
|
|
455
455
|
}
|
|
456
456
|
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
457
457
|
}
|
|
458
|
+
// Memoize the compiled preserve-tag pattern set by Set reference (#642-audit):
|
|
459
|
+
// opts.preserveOrchestrationTags is a single Set instance passed on every
|
|
460
|
+
// request, so recompile the ~28 patterns once instead of per request. The
|
|
461
|
+
// default (no-preserve) path already uses the precompiled constant.
|
|
462
|
+
let _orchPreserveKey;
|
|
463
|
+
let _orchPatterns;
|
|
464
|
+
function orchestrationPatternsFor(preserveTags) {
|
|
465
|
+
if (preserveTags === undefined)
|
|
466
|
+
return ORCHESTRATION_PATTERNS_DEFAULT;
|
|
467
|
+
if (preserveTags !== _orchPreserveKey) {
|
|
468
|
+
_orchPreserveKey = preserveTags;
|
|
469
|
+
_orchPatterns = buildOrchestrationPatterns(preserveTags);
|
|
470
|
+
}
|
|
471
|
+
return _orchPatterns;
|
|
472
|
+
}
|
|
458
473
|
/**
|
|
459
474
|
* Strip orchestration tags from all messages in a request body.
|
|
460
475
|
*
|
|
@@ -465,7 +480,7 @@ export function sanitizeMessages(body, preserveTags) {
|
|
|
465
480
|
const messages = body.messages;
|
|
466
481
|
if (!messages)
|
|
467
482
|
return;
|
|
468
|
-
const patterns =
|
|
483
|
+
const patterns = orchestrationPatternsFor(preserveTags);
|
|
469
484
|
for (const msg of messages) {
|
|
470
485
|
if (typeof msg.content === 'string') {
|
|
471
486
|
msg.content = sanitizeContent(msg.content, patterns);
|
|
@@ -1758,9 +1773,15 @@ export async function startProxy(opts = {}) {
|
|
|
1758
1773
|
// recognizes through its own Anthropic gateway, bypassing localhost).
|
|
1759
1774
|
let forcedProvider = cliProviderOverride;
|
|
1760
1775
|
let requestEffort; // dario#419 — per-request effort parsed from a model-name suffix (model:high / model-high)
|
|
1776
|
+
// Parsed body, shared between the provider-prefix detection below and the
|
|
1777
|
+
// template-build block further down so the same bytes are not JSON.parsed
|
|
1778
|
+
// twice per request (#642-audit). Mutations in the prefix block re-serialize
|
|
1779
|
+
// `body` FROM this object, so it always represents the current body.
|
|
1780
|
+
let parsedBody = null;
|
|
1761
1781
|
if (body.length > 0) {
|
|
1762
1782
|
try {
|
|
1763
1783
|
const parsed = JSON.parse(body.toString());
|
|
1784
|
+
parsedBody = parsed;
|
|
1764
1785
|
const rawModel = parsed.model ?? '';
|
|
1765
1786
|
const prefix = parseProviderPrefix(rawModel);
|
|
1766
1787
|
if (prefix) {
|
|
@@ -1866,7 +1887,11 @@ export async function startProxy(opts = {}) {
|
|
|
1866
1887
|
} : undefined;
|
|
1867
1888
|
if (body.length > 0) {
|
|
1868
1889
|
try {
|
|
1869
|
-
|
|
1890
|
+
// Reuse the object parsed for provider-prefix detection above — the same
|
|
1891
|
+
// bytes, so re-parsing is wasted work on large bodies (#642-audit). Falls
|
|
1892
|
+
// back to a fresh parse when the earlier block didn't run (e.g. the body
|
|
1893
|
+
// was not JSON, or an OpenAI-backend reroute skipped it).
|
|
1894
|
+
const parsed = parsedBody ?? JSON.parse(body.toString());
|
|
1870
1895
|
// Strip orchestration tags from messages (Aider, Cursor, etc.)
|
|
1871
1896
|
sanitizeMessages(parsed, opts.preserveOrchestrationTags);
|
|
1872
1897
|
const result = isOpenAI ? openaiToAnthropic(parsed, modelOverride) : (modelOverride ? { ...parsed, model: modelOverride } : parsed);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askalf/dario",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.123",
|
|
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": {
|