@askalf/dario 6.8.16 → 6.8.17

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
@@ -479,6 +479,11 @@ async function proxy() {
479
479
  // user waits at the cap while everyone else keeps flowing. 0 = off.
480
480
  const maxConcurrentPerConsumer = parsePositiveIntFlag('--max-concurrent-per-consumer=')
481
481
  ?? parsePositiveIntEnv(process.env['DARIO_MAX_CONCURRENT_PER_CONSUMER']);
482
+ // --shutdown-grace=MS — how long SIGTERM waits for in-flight requests to
483
+ // finish before the process exits (default 90000). A container recreate
484
+ // used to sever every streaming response after five seconds. dario#1370.
485
+ const shutdownGraceMs = parsePositiveIntFlag('--shutdown-grace=')
486
+ ?? parsePositiveIntEnv(process.env['DARIO_SHUTDOWN_GRACE_MS']);
482
487
  // --pool-strategy=headroom|fill-first — where UNBOUND (new) conversations
483
488
  // land. `headroom` (default) spreads them to the seat with the most slack;
484
489
  // `fill-first` concentrates them on the alphabetically-first eligible seat
@@ -704,7 +709,7 @@ async function proxy() {
704
709
  console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
705
710
  process.exit(1);
706
711
  }
707
- await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, analyticsToken, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, poolStrategy, poolHeadroomFloor, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger, keys, keysPath });
712
+ await startProxy({ port, host, verbose, verboseBodies, model, fastModel, noClaudeAuth, analyticsToken, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, thinkTimeBaseMs, thinkTimePerTokenMs, thinkTimeJitterMs, thinkTimeMaxMs, sessionStartMinMs, sessionStartJitterMs, stealth, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, maxConcurrentPerConsumer, shutdownGraceMs, poolStrategy, poolHeadroomFloor, poolSharedState, poolSharedStateIntervalMs, effort, maxTokens, poolFallbackModel, modelAliases, logFile, passthroughBetas, skipFields, systemPrompt, overageGuardEnabled, overageGuardBehavior, overageGuardCooldownMs, overageGuardNotifyOs, honorClientThinking, preserveOutputFormat, midstreamContinue, ledger, keys, keysPath });
708
713
  }
709
714
  /**
710
715
  * `dario keys` — named keys for a shared dario (v6.8, dario#1318). One
@@ -2074,6 +2079,12 @@ async function help() {
2074
2079
  as --strict-tls: make the unsafe state
2075
2080
  require intent. Env: DARIO_STRICT_TEMPLATE=1.
2076
2081
  (v3.30.8, dario#77)
2082
+ --shutdown-grace=MS How long SIGTERM waits for in-flight requests
2083
+ to finish before exiting (default: 90000).
2084
+ The listener closes at once. Set the
2085
+ container's stop grace above it, or Docker
2086
+ SIGKILLs the drain. Env:
2087
+ DARIO_SHUTDOWN_GRACE_MS. (dario#1370)
2077
2088
  --max-concurrent=N Max in-flight requests across the WHOLE proxy,
2078
2089
  not per seat (default: 10; a pool defaults to
2079
2090
  10 per seat). Past it, requests wait in dario.
package/dist/proxy.d.ts CHANGED
@@ -395,6 +395,12 @@ interface ProxyOptions {
395
395
  poolSharedState?: boolean;
396
396
  /** How often to pull peers' readings, ms. Default 2000. */
397
397
  poolSharedStateIntervalMs?: number;
398
+ /**
399
+ * How long a SIGTERM waits for in-flight requests to finish before the
400
+ * process exits. Default 90s. The listener closes at once, so nothing new
401
+ * is accepted while the wait runs. dario#1370.
402
+ */
403
+ shutdownGraceMs?: number;
398
404
  /** Max concurrent in-flight requests. Default 10. dario#80. */
399
405
  maxConcurrent?: number;
400
406
  /** Max requests buffered waiting for a concurrency slot. Default 128. dario#80. */
package/dist/proxy.js CHANGED
@@ -51,6 +51,7 @@ import { listCodexAccountAliases, loadAllCodexAccounts, codexAccountNeedsRefresh
51
51
  import { route as routeProvider } from './provider-adapter.js';
52
52
  import { selectPoolFallbackModels } from './pool-fallback-tier.js';
53
53
  import { RequestQueue, QueueFullError, QueueTimeoutError, DEFAULT_MAX_CONCURRENT, DEFAULT_MAX_QUEUED, DEFAULT_QUEUE_TIMEOUT_MS, resolveMaxConcurrent } from './request-queue.js';
54
+ import { waitForIdle, DEFAULT_SHUTDOWN_GRACE_MS } from './shutdown-drain.js';
54
55
  import { redactSecrets } from './redact.js';
55
56
  import { BAKED_BASE_MODELS, withLongContextVariants, buildOpenAIModelsList, getModelCatalog, getCachedBases, resolveAliasAgainst, prewarmModelCatalog, retryModelCatalogNow, isSuspendedModel } from './model-catalog.js';
56
57
  import { classifyUpstreamRejection, diagnosticSnippet, POOL_PARKED } from './upstream-rejection.js';
@@ -5761,15 +5762,17 @@ export async function startProxy(opts = {}) {
5761
5762
  poolSync?.stop();
5762
5763
  if (logFileStream)
5763
5764
  logFileStream.end();
5764
- // Flush tokens first (best-effort, bounded), then close the server. The
5765
- // flush is fire-and-forget under the same 5s force-exit guard below so a
5766
- // hung fsync can't wedge shutdown.
5767
5765
  keyStore?.close();
5768
- void Promise.all([flushPoolTokens(), ledger?.close()]).finally(() => {
5769
- server.close(() => process.exit(0));
5770
- });
5771
- // Force exit after 5s if connections (or the flush) don't complete.
5772
- setTimeout(() => process.exit(0), 5000).unref();
5766
+ // Stop accepting connections now; responses already streaming keep going.
5767
+ // Then flush tokens (best-effort), drain what is in flight up to the
5768
+ // grace, and exit. The force-exit guard sits past the grace so a stream
5769
+ // that never ends cannot wedge shutdown, and a hung fsync cannot either.
5770
+ server.close();
5771
+ const graceMs = opts.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS;
5772
+ void Promise.all([flushPoolTokens(), ledger?.close()])
5773
+ .then(() => waitForIdle(() => queue.snapshot().active, { graceMs }))
5774
+ .finally(() => process.exit(0));
5775
+ setTimeout(() => process.exit(0), graceMs + 5000).unref();
5773
5776
  };
5774
5777
  process.on('SIGINT', shutdown);
5775
5778
  process.on('SIGTERM', shutdown);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shutdown drain (dario#1370).
3
+ *
4
+ * A SIGTERM used to give in-flight requests five seconds and then exit. The
5
+ * fleet's agent runs are one long streamed response each, so a container
6
+ * recreate — every within-minor autodeploy of a bot release — severed every
7
+ * run that was in flight: three dropped executions in one night, each
8
+ * re-armed a quarter of an hour later with its spend gone.
9
+ *
10
+ * `waitForIdle` is the wait `shutdown` now does between closing the listener
11
+ * and exiting: poll the in-flight count until it reaches zero or the grace
12
+ * runs out. It is pure over its inputs — the count, the clock and the sleep
13
+ * are injected — so the policy is tested without a server or real timers.
14
+ */
15
+ /** How long a SIGTERM waits for in-flight requests by default. */
16
+ export declare const DEFAULT_SHUTDOWN_GRACE_MS = 90000;
17
+ /** How often the drain re-reads the in-flight count. */
18
+ export declare const SHUTDOWN_POLL_MS = 250;
19
+ export interface DrainOptions {
20
+ /** Longest the drain waits before giving up on the remaining requests. */
21
+ graceMs: number;
22
+ /** Poll interval; defaults to SHUTDOWN_POLL_MS. */
23
+ pollMs?: number;
24
+ /** Clock and sleep, injectable for tests. */
25
+ now?: () => number;
26
+ sleep?: (ms: number) => Promise<void>;
27
+ /** Where the drain narrates; defaults to console.log. */
28
+ log?: (line: string) => void;
29
+ }
30
+ export interface DrainResult {
31
+ /** True when the in-flight count reached zero within the grace. */
32
+ drained: boolean;
33
+ /** How long the drain waited. */
34
+ waitedMs: number;
35
+ /** In-flight requests left when the drain returned. */
36
+ remaining: number;
37
+ }
38
+ /**
39
+ * Wait until `getActive()` reports no in-flight request, or `graceMs` has
40
+ * passed. Logs once when it starts waiting and once when it stops, never per
41
+ * poll, so a long drain is two lines rather than a scroll.
42
+ */
43
+ export declare function waitForIdle(getActive: () => number, opts: DrainOptions): Promise<DrainResult>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shutdown drain (dario#1370).
3
+ *
4
+ * A SIGTERM used to give in-flight requests five seconds and then exit. The
5
+ * fleet's agent runs are one long streamed response each, so a container
6
+ * recreate — every within-minor autodeploy of a bot release — severed every
7
+ * run that was in flight: three dropped executions in one night, each
8
+ * re-armed a quarter of an hour later with its spend gone.
9
+ *
10
+ * `waitForIdle` is the wait `shutdown` now does between closing the listener
11
+ * and exiting: poll the in-flight count until it reaches zero or the grace
12
+ * runs out. It is pure over its inputs — the count, the clock and the sleep
13
+ * are injected — so the policy is tested without a server or real timers.
14
+ */
15
+ /** How long a SIGTERM waits for in-flight requests by default. */
16
+ export const DEFAULT_SHUTDOWN_GRACE_MS = 90_000;
17
+ /** How often the drain re-reads the in-flight count. */
18
+ export const SHUTDOWN_POLL_MS = 250;
19
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
20
+ /**
21
+ * Wait until `getActive()` reports no in-flight request, or `graceMs` has
22
+ * passed. Logs once when it starts waiting and once when it stops, never per
23
+ * poll, so a long drain is two lines rather than a scroll.
24
+ */
25
+ export async function waitForIdle(getActive, opts) {
26
+ const pollMs = Math.max(1, opts.pollMs ?? SHUTDOWN_POLL_MS);
27
+ const now = opts.now ?? Date.now;
28
+ const sleep = opts.sleep ?? defaultSleep;
29
+ const log = opts.log ?? ((line) => console.log(line));
30
+ let active = getActive();
31
+ if (active <= 0)
32
+ return { drained: true, waitedMs: 0, remaining: 0 };
33
+ const started = now();
34
+ log(`[dario] draining ${active} in-flight request(s) before exit (up to ${Math.round(opts.graceMs / 1000)}s)`);
35
+ for (;;) {
36
+ await sleep(pollMs);
37
+ active = getActive();
38
+ const waitedMs = now() - started;
39
+ if (active <= 0) {
40
+ log(`[dario] drained after ${(waitedMs / 1000).toFixed(1)}s`);
41
+ return { drained: true, waitedMs, remaining: 0 };
42
+ }
43
+ if (waitedMs >= opts.graceMs) {
44
+ log(`[dario] still ${active} in flight after ${Math.round(waitedMs / 1000)}s; exiting`);
45
+ return { drained: false, waitedMs, remaining: active };
46
+ }
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "6.8.16",
3
+ "version": "6.8.17",
4
4
  "description": "Use your Claude and ChatGPT subscriptions in Cursor, Cline, Aider, Claude Code and the Agent SDK — at subscription pricing, not per-token API bills. One local Anthropic + OpenAI-compatible endpoint: either plan answers either wire shape, with automatic failover when one hits its limit.",
5
5
  "type": "module",
6
6
  "bin": {