@askalf/dario 3.23.0 → 3.24.0

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/cli.js CHANGED
@@ -208,7 +208,25 @@ async function proxy() {
208
208
  const strictTls = args.includes('--strict-tls');
209
209
  const modelArg = args.find(a => a.startsWith('--model='));
210
210
  const model = modelArg ? modelArg.split('=')[1] : undefined;
211
- await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, noAutoDetect, strictTls });
211
+ // --pace-min=MS / --pace-jitter=MS (v3.24, direction #6 behavioral
212
+ // smoothing). Inter-request gap floor + optional uniform-random jitter.
213
+ // Defaults preserve v3.23 behavior (500ms floor, no jitter). The pure
214
+ // calc lives in src/pacing.ts; the flags just feed it.
215
+ const pacingMinMs = parsePositiveIntFlag('--pace-min=');
216
+ const pacingJitterMs = parsePositiveIntFlag('--pace-jitter=');
217
+ await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs });
218
+ }
219
+ function parsePositiveIntFlag(prefix) {
220
+ const found = args.find(a => a.startsWith(prefix));
221
+ if (!found)
222
+ return undefined;
223
+ const raw = found.slice(prefix.length);
224
+ const n = parseInt(raw, 10);
225
+ if (!Number.isFinite(n) || n < 0) {
226
+ console.error(`[dario] Invalid ${prefix.replace(/=$/, '')} value: ${JSON.stringify(raw)}. Must be a non-negative integer (ms).`);
227
+ process.exit(1);
228
+ }
229
+ return n;
212
230
  }
213
231
  async function accounts() {
214
232
  const sub = args[1];
@@ -459,6 +477,15 @@ async function help() {
459
477
  from a stock CC request. Install Bun
460
478
  (https://bun.sh) so dario auto-relaunches
461
479
  under it, or use shim mode. (v3.23)
480
+ --pace-min=MS Minimum ms between upstream requests
481
+ (default: 500). Prevents request floods
482
+ that are distinguishable from human-paced
483
+ CC traffic.
484
+ --pace-jitter=MS Max additional uniform-random jitter (ms)
485
+ added on top of --pace-min per request.
486
+ Default: 0 (off). Set to e.g. 300 to hide
487
+ the floor from long-run inter-arrival
488
+ statistics. (v3.24)
462
489
  --port=PORT Port to listen on (default: 3456)
463
490
  --host=ADDRESS Address to bind to (default: 127.0.0.1)
464
491
  Use 0.0.0.0 for LAN; see README for DARIO_API_KEY
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Inter-request pacing (v3.24, direction #6 — behavioral smoothing).
3
+ *
4
+ * Real CC traffic has human-paced gaps between requests — sub-second when
5
+ * the model is streaming tool-loop output, multi-second when the user is
6
+ * typing the next message. A proxy that fires requests at machine speed
7
+ * with perfectly uniform spacing stands out against that rhythm.
8
+ *
9
+ * This module supplies the pure gap-calculation function the proxy's
10
+ * rate governor calls before every outbound fetch. Two knobs:
11
+ *
12
+ * minGapMs — lower bound on the wall-clock distance between requests.
13
+ * Was a hardcoded 500ms through v3.23; keep 500 as default
14
+ * so back-compat is exact when both knobs stay at defaults.
15
+ *
16
+ * jitterMs — uniform random addition on top of minGap. The *effective*
17
+ * gap for a given request is minGap + U(0, jitter). Adds
18
+ * non-uniformity so an observer can't infer the floor from
19
+ * the long-run minimum of inter-arrival times.
20
+ *
21
+ * Pure over (now, lastRequestTime, minGap, jitter, rng) so the tests can
22
+ * exercise every edge without spawning timers. The proxy passes
23
+ * `Math.random` as the rng at runtime; tests pass a deterministic stub.
24
+ *
25
+ * The first request in a session (lastRequestTime === 0) is never paced —
26
+ * the purpose is smoothing the *gap between* requests, not delaying the
27
+ * first one from whenever the consumer happens to connect.
28
+ */
29
+ export interface PacingConfig {
30
+ /** Minimum wall-clock milliseconds between the completion of one request and the start of the next. */
31
+ minGapMs: number;
32
+ /** Max additional uniform-random jitter (ms) added on top of minGap. Pass 0 to disable. */
33
+ jitterMs: number;
34
+ }
35
+ /**
36
+ * How many milliseconds to sleep before the next upstream fetch.
37
+ *
38
+ * Returns 0 when no delay is required — either because this is the first
39
+ * request of the session, or enough wall-clock time has already elapsed
40
+ * since `lastRequestTime`.
41
+ *
42
+ * `rng` defaults to Math.random; tests inject a deterministic stub.
43
+ * Negative configuration values are clamped to 0 (lenient, not an error).
44
+ */
45
+ export declare function computePacingDelay(now: number, lastRequestTime: number, cfg: PacingConfig, rng?: () => number): number;
46
+ /**
47
+ * Resolve a PacingConfig from explicit options, env vars, and defaults.
48
+ *
49
+ * Precedence (highest first):
50
+ * 1. Explicit argument (typically from CLI flag)
51
+ * 2. DARIO_PACE_MIN_MS / DARIO_PACE_JITTER_MS env vars
52
+ * 3. Legacy DARIO_MIN_INTERVAL_MS env var (minGap only — matches v3.23
53
+ * behavior so existing setups don't regress silently)
54
+ * 4. Defaults: minGap=500, jitter=0
55
+ *
56
+ * Invalid strings (non-numeric, negative) are ignored and fall through to
57
+ * the next source — a typoed env var shouldn't fail-loud at startup.
58
+ */
59
+ export declare function resolvePacingConfig(explicit?: {
60
+ minGapMs?: number;
61
+ jitterMs?: number;
62
+ }, env?: NodeJS.ProcessEnv): PacingConfig;
package/dist/pacing.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Inter-request pacing (v3.24, direction #6 — behavioral smoothing).
3
+ *
4
+ * Real CC traffic has human-paced gaps between requests — sub-second when
5
+ * the model is streaming tool-loop output, multi-second when the user is
6
+ * typing the next message. A proxy that fires requests at machine speed
7
+ * with perfectly uniform spacing stands out against that rhythm.
8
+ *
9
+ * This module supplies the pure gap-calculation function the proxy's
10
+ * rate governor calls before every outbound fetch. Two knobs:
11
+ *
12
+ * minGapMs — lower bound on the wall-clock distance between requests.
13
+ * Was a hardcoded 500ms through v3.23; keep 500 as default
14
+ * so back-compat is exact when both knobs stay at defaults.
15
+ *
16
+ * jitterMs — uniform random addition on top of minGap. The *effective*
17
+ * gap for a given request is minGap + U(0, jitter). Adds
18
+ * non-uniformity so an observer can't infer the floor from
19
+ * the long-run minimum of inter-arrival times.
20
+ *
21
+ * Pure over (now, lastRequestTime, minGap, jitter, rng) so the tests can
22
+ * exercise every edge without spawning timers. The proxy passes
23
+ * `Math.random` as the rng at runtime; tests pass a deterministic stub.
24
+ *
25
+ * The first request in a session (lastRequestTime === 0) is never paced —
26
+ * the purpose is smoothing the *gap between* requests, not delaying the
27
+ * first one from whenever the consumer happens to connect.
28
+ */
29
+ /**
30
+ * How many milliseconds to sleep before the next upstream fetch.
31
+ *
32
+ * Returns 0 when no delay is required — either because this is the first
33
+ * request of the session, or enough wall-clock time has already elapsed
34
+ * since `lastRequestTime`.
35
+ *
36
+ * `rng` defaults to Math.random; tests inject a deterministic stub.
37
+ * Negative configuration values are clamped to 0 (lenient, not an error).
38
+ */
39
+ export function computePacingDelay(now, lastRequestTime, cfg, rng = Math.random) {
40
+ if (lastRequestTime <= 0)
41
+ return 0;
42
+ const minGap = Math.max(0, cfg.minGapMs);
43
+ const jitter = Math.max(0, cfg.jitterMs);
44
+ const jitterAdd = jitter > 0 ? Math.floor(rng() * jitter) : 0;
45
+ const effectiveGap = minGap + jitterAdd;
46
+ const elapsed = now - lastRequestTime;
47
+ if (elapsed >= effectiveGap)
48
+ return 0;
49
+ return effectiveGap - elapsed;
50
+ }
51
+ /**
52
+ * Resolve a PacingConfig from explicit options, env vars, and defaults.
53
+ *
54
+ * Precedence (highest first):
55
+ * 1. Explicit argument (typically from CLI flag)
56
+ * 2. DARIO_PACE_MIN_MS / DARIO_PACE_JITTER_MS env vars
57
+ * 3. Legacy DARIO_MIN_INTERVAL_MS env var (minGap only — matches v3.23
58
+ * behavior so existing setups don't regress silently)
59
+ * 4. Defaults: minGap=500, jitter=0
60
+ *
61
+ * Invalid strings (non-numeric, negative) are ignored and fall through to
62
+ * the next source — a typoed env var shouldn't fail-loud at startup.
63
+ */
64
+ export function resolvePacingConfig(explicit = {}, env = process.env) {
65
+ const minGap = pickNonNegativeInt(explicit.minGapMs, env.DARIO_PACE_MIN_MS, env.DARIO_MIN_INTERVAL_MS) ?? 500;
66
+ const jitter = pickNonNegativeInt(explicit.jitterMs, env.DARIO_PACE_JITTER_MS) ?? 0;
67
+ return { minGapMs: minGap, jitterMs: jitter };
68
+ }
69
+ function pickNonNegativeInt(...candidates) {
70
+ for (const c of candidates) {
71
+ if (c === undefined || c === null || c === '')
72
+ continue;
73
+ const n = typeof c === 'number' ? c : parseInt(c, 10);
74
+ if (Number.isFinite(n) && n >= 0)
75
+ return Math.floor(n);
76
+ }
77
+ return undefined;
78
+ }
package/dist/proxy.d.ts CHANGED
@@ -13,6 +13,8 @@ interface ProxyOptions {
13
13
  hybridTools?: boolean;
14
14
  noAutoDetect?: boolean;
15
15
  strictTls?: boolean;
16
+ pacingMinMs?: number;
17
+ pacingJitterMs?: number;
16
18
  }
17
19
  export declare function sanitizeError(err: unknown): string;
18
20
  export declare function startProxy(opts?: ProxyOptions): Promise<void>;
package/dist/proxy.js CHANGED
@@ -571,10 +571,19 @@ export async function startProxy(opts = {}) {
571
571
  betaBase = betaBase ? `${betaBase},oauth-2025-04-20` : 'oauth-2025-04-20';
572
572
  }
573
573
  const betaWithoutContext1m = betaBase.split(',').filter((t) => t !== 'context-1m-2025-08-07').join(',');
574
- // Rate governor — minimum 500ms between requests. Fast enough for agents,
575
- // slow enough to not look like a scripted flood of identical traffic.
574
+ // Rate governor — floor + optional jitter between requests. A hardcoded
575
+ // 500ms floor keeps the default behavior identical to v3.23; `--pace-min`
576
+ // and `--pace-jitter` let callers tune the distribution. Pure calc lives
577
+ // in src/pacing.ts so the edge cases are unit-tested without timers.
578
+ const { computePacingDelay, resolvePacingConfig } = await import('./pacing.js');
576
579
  let lastRequestTime = 0;
577
- const MIN_REQUEST_INTERVAL_MS = parseInt(process.env.DARIO_MIN_INTERVAL_MS || '500', 10);
580
+ const pacingCfg = resolvePacingConfig({
581
+ minGapMs: opts.pacingMinMs,
582
+ jitterMs: opts.pacingJitterMs,
583
+ });
584
+ if (verbose) {
585
+ console.log(`[dario] pacing: min=${pacingCfg.minGapMs}ms jitter=${pacingCfg.jitterMs}ms`);
586
+ }
578
587
  // Optional proxy authentication — pre-encode key buffer for performance
579
588
  const apiKey = process.env.DARIO_API_KEY;
580
589
  const apiKeyBuf = apiKey ? Buffer.from(apiKey) : null;
@@ -1076,11 +1085,11 @@ export async function startProxy(opts = {}) {
1076
1085
  beta = beta.split(',').filter((t) => t.length > 0 && !rejectedSet.has(t)).join(',');
1077
1086
  }
1078
1087
  }
1079
- // Rate governor — prevent inhuman request cadence
1080
- const now = Date.now();
1081
- const elapsed = now - lastRequestTime;
1082
- if (elapsed < MIN_REQUEST_INTERVAL_MS && lastRequestTime > 0) {
1083
- await new Promise(r => setTimeout(r, MIN_REQUEST_INTERVAL_MS - elapsed));
1088
+ // Rate governor — prevent inhuman request cadence. See src/pacing.ts
1089
+ // for the pure delay calculator (floor + uniform jitter).
1090
+ const pacingDelay = computePacingDelay(Date.now(), lastRequestTime, pacingCfg);
1091
+ if (pacingDelay > 0) {
1092
+ await new Promise(r => setTimeout(r, pacingDelay));
1084
1093
  }
1085
1094
  lastRequestTime = Date.now();
1086
1095
  // Session ID: pool mode uses the per-account identity.sessionId (stable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.23.0",
3
+ "version": "3.24.0",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",