@askalf/dario
Advanced tools
@@ -83,4 +83,30 @@ /** | ||
| }; | ||
| pool?: { | ||
| /** | ||
| * `headroom` (default) spreads new conversations to the seat with the | ||
| * most headroom; `fill-first` concentrates them on the alphabetically- | ||
| * first eligible seat until it drains to the 2% floor, then spills to | ||
| * the next — primary/backup semantics, alias order is the knob. | ||
| */ | ||
| strategy?: 'headroom' | 'fill-first'; | ||
| }; | ||
| effort?: string | null; | ||
| maxTokens?: number | 'client' | null; | ||
| /** | ||
| * Pool-exhausted fallback. When `model` is a non-empty string and an | ||
| * openai-compat backend is configured, OpenAI-shape requests that the | ||
| * Claude pool can't serve are forwarded to that backend as `model` | ||
| * (response marked `x-dario-pool-fallback`) instead of surfacing the | ||
| * 429/503. Null/absent = off. | ||
| */ | ||
| poolFallback?: { | ||
| model?: string | null; | ||
| }; | ||
| /** | ||
| * User-defined model aliases: client-visible name → target model. | ||
| * Resolved at request time before provider-prefix parsing, so a target | ||
| * may carry a prefix (`"my-fast": "openai:gpt-4o-mini"`). Names are | ||
| * matched case-insensitively; one step, never recursive. | ||
| */ | ||
| modelAliases?: Record<string, string>; | ||
| passthroughBetas?: string[]; | ||
@@ -87,0 +113,0 @@ systemPrompt?: string | null; |
+28
-0
@@ -68,4 +68,7 @@ /** | ||
| queue: { maxConcurrent: null, maxQueued: null, timeoutMs: null }, | ||
| pool: { strategy: 'headroom' }, | ||
| effort: null, | ||
| maxTokens: null, | ||
| poolFallback: { model: null }, | ||
| modelAliases: {}, | ||
| passthroughBetas: [], | ||
@@ -307,2 +310,14 @@ systemPrompt: null, | ||
| } | ||
| if (isPlainObject(parsed.pool)) { | ||
| out.pool = {}; | ||
| if (parsed.pool.strategy === 'headroom' || parsed.pool.strategy === 'fill-first') { | ||
| out.pool.strategy = parsed.pool.strategy; | ||
| } | ||
| } | ||
| if (isPlainObject(parsed.poolFallback)) { | ||
| out.poolFallback = {}; | ||
| if (parsed.poolFallback.model === null || typeof parsed.poolFallback.model === 'string') { | ||
| out.poolFallback.model = parsed.poolFallback.model; | ||
| } | ||
| } | ||
| const effort = pickStringOrNull('effort'); | ||
@@ -321,2 +336,15 @@ if (effort !== undefined) | ||
| } | ||
| if (isPlainObject(parsed.modelAliases)) { | ||
| const aliases = {}; | ||
| for (const [k, v] of Object.entries(parsed.modelAliases)) { | ||
| if (typeof v !== 'string') | ||
| continue; | ||
| const name = k.trim().toLowerCase(); | ||
| const target = v.trim(); | ||
| if (!name || !target) | ||
| continue; | ||
| aliases[name] = target; | ||
| } | ||
| out.modelAliases = aliases; | ||
| } | ||
| if (Array.isArray(parsed.passthroughBetas)) { | ||
@@ -323,0 +351,0 @@ out.passthroughBetas = parsed.passthroughBetas |
@@ -285,3 +285,3 @@ /** | ||
| readonly min: "1.0.0"; | ||
| readonly maxTested: "2.1.211"; | ||
| readonly maxTested: "2.1.212"; | ||
| }; | ||
@@ -288,0 +288,0 @@ /** |
@@ -809,3 +809,3 @@ /** | ||
| min: '1.0.0', | ||
| maxTested: '2.1.211', | ||
| maxTested: '2.1.212', | ||
| }; | ||
@@ -812,0 +812,0 @@ /** |
+28
-0
@@ -78,2 +78,28 @@ /** | ||
| } | ||
| /** | ||
| * Pool routing strategy. | ||
| * | ||
| * `headroom` (default) — every selection picks the account with the most | ||
| * headroom, spreading new conversations across all seats. | ||
| * | ||
| * `fill-first` — concentrate new conversations on the lexicographically- | ||
| * first eligible account (by alias) until its headroom drops to the 2% | ||
| * floor, then spill to the next. Two things headroom spreading can't give | ||
| * you: primary/backup semantics (a `z-backup` seat stays untouched until | ||
| * `a-main` is actually drained), and cache concentration (every fresh | ||
| * conversation lands where the prompt-cache pressure already is, keeping | ||
| * the spill seat's windows fully fresh for when they're needed). Alias | ||
| * order is the operator's knob — name seats `1-main` / `2-overflow` to | ||
| * pick the fill order. Sticky bindings behave identically in both modes; | ||
| * strategy only decides where UNBOUND (new) conversations land. | ||
| */ | ||
| export type PoolStrategy = 'headroom' | 'fill-first'; | ||
| /** | ||
| * Resolve the pool strategy from an explicit value (CLI flag / config file, | ||
| * already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as | ||
| * the env fallback. Unrecognized values fall through — a typo behaves like | ||
| * the default rather than crashing startup, matching the other resolvers | ||
| * in this codebase (see resolveSessionRotationConfig). | ||
| */ | ||
| export declare function resolvePoolStrategy(explicit?: string | null, env?: NodeJS.ProcessEnv): PoolStrategy; | ||
| /** Parse an Anthropic response's rate-limit headers into a snapshot. */ | ||
@@ -109,2 +135,3 @@ export declare function parseRateLimits(headers: Headers): RateLimitSnapshot; | ||
| export declare class AccountPool { | ||
| private readonly strategy; | ||
| private accounts; | ||
@@ -117,2 +144,3 @@ private queue; | ||
| private lastStickyCleanup; | ||
| constructor(strategy?: PoolStrategy); | ||
| add(alias: string, opts: { | ||
@@ -119,0 +147,0 @@ accessToken: string; |
+53
-0
@@ -58,2 +58,19 @@ /** | ||
| /** | ||
| * Resolve the pool strategy from an explicit value (CLI flag / config file, | ||
| * already precedence-merged by the caller) with `DARIO_POOL_STRATEGY` as | ||
| * the env fallback. Unrecognized values fall through — a typo behaves like | ||
| * the default rather than crashing startup, matching the other resolvers | ||
| * in this codebase (see resolveSessionRotationConfig). | ||
| */ | ||
| export function resolvePoolStrategy(explicit, env = process.env) { | ||
| for (const c of [explicit, env.DARIO_POOL_STRATEGY]) { | ||
| if (typeof c !== 'string') | ||
| continue; | ||
| const s = c.trim().toLowerCase(); | ||
| if (s === 'headroom' || s === 'fill-first') | ||
| return s; | ||
| } | ||
| return 'headroom'; | ||
| } | ||
| /** | ||
| * Match `anthropic-ratelimit-unified-7d_<family>-utilization`. Generic on | ||
@@ -170,3 +187,19 @@ * `<family>` so a future `7d_opus` / `7d_haiku` (or anything Anthropic | ||
| } | ||
| // Fill-first pick: lexicographically-first eligible account still above the | ||
| // headroom floor. Alias order (not insertion order) — accounts load from a | ||
| // readdir whose order the OS doesn't guarantee, and the operator can control | ||
| // alias names but not readdir. Returns null when every candidate is at/below | ||
| // the floor so the caller can fall back to max-headroom. | ||
| function pickFillFirst(accounts, family) { | ||
| let best = null; | ||
| for (const a of accounts) { | ||
| if (best !== null && a.alias >= best.alias) | ||
| continue; | ||
| if (computeHeadroom(a.rateLimit, family) > POOL_HEADROOM_FLOOR) | ||
| best = a; | ||
| } | ||
| return best; | ||
| } | ||
| export class AccountPool { | ||
| strategy; | ||
| accounts = new Map(); | ||
@@ -180,2 +213,5 @@ queue = []; | ||
| lastStickyCleanup = 0; | ||
| constructor(strategy = 'headroom') { | ||
| this.strategy = strategy; | ||
| } | ||
| add(alias, opts) { | ||
@@ -263,2 +299,10 @@ const existing = this.accounts.get(alias); | ||
| if (eligible.length > 0) { | ||
| if (this.strategy === 'fill-first') { | ||
| const first = pickFillFirst(eligible, family); | ||
| if (first) | ||
| return first; | ||
| // Every eligible account is at/below the floor — the terminal state | ||
| // both strategies share. Fall through to max-headroom so the caller | ||
| // still gets the least-drained account instead of null. | ||
| } | ||
| return pickMaxHeadroom(eligible, family); | ||
@@ -381,2 +425,11 @@ } | ||
| if (eligible.length > 0) { | ||
| // Fill-first failover keeps the fill order: the next account tried | ||
| // after a 429 is the next alias in line, not the max-headroom seat — | ||
| // otherwise a single failover would defeat the concentration the | ||
| // strategy exists to provide. | ||
| if (this.strategy === 'fill-first') { | ||
| const first = pickFillFirst(eligible, family); | ||
| if (first) | ||
| return first; | ||
| } | ||
| return pickMaxHeadroom(eligible, family); | ||
@@ -383,0 +436,0 @@ } |
+56
-0
@@ -41,2 +41,20 @@ import { type IncomingMessage } from 'node:http'; | ||
| /** | ||
| * User-defined model aliases: client-visible name → target model. | ||
| * | ||
| * Complements the built-in family shorthands above — those track the model | ||
| * catalog; these are operator-declared (config `modelAliases`, repeatable | ||
| * `--model-alias=name=target`, `DARIO_MODEL_ALIASES=name=target,…`) and | ||
| * resolve FIRST at request time, before provider-prefix parsing, so a | ||
| * target may carry a prefix (`my-fast` → `openai:gpt-4o-mini`) and | ||
| * retarget the backend. One step, never recursive: a target that names | ||
| * another alias is forwarded as-is. Alias names match case-insensitively; | ||
| * targets forward verbatim. An alias may shadow a real model id or a | ||
| * built-in shorthand — deliberate, that's how you downgrade every `opus` | ||
| * call from a client whose picker you don't control. | ||
| */ | ||
| export declare function parseModelAliasSpecs(specs: readonly string[]): Record<string, string>; | ||
| /** Resolve `model` through user aliases. Null = no alias applies (also on | ||
| * self-mapping, so a misconfigured `opus=opus` can't loop the caller). */ | ||
| export declare function applyModelAlias(model: string | null | undefined, aliases: Record<string, string> | undefined): string | null; | ||
| /** | ||
| * Pick the per-request model override under a forced `--model`. | ||
@@ -56,2 +74,9 @@ * | ||
| export declare function selectModelOverride(incomingModel: string, modelOverride: string | null, fastModelOverride: string | null): string | null; | ||
| /** | ||
| * Rebuild an OpenAI-shape request body with the model swapped to the pool- | ||
| * fallback target. Null when the body isn't a JSON object — the caller | ||
| * surfaces the original error instead of forwarding garbage. Exported for | ||
| * tests; the two call sites are the pool-exhausted dispatch paths. | ||
| */ | ||
| export declare function buildPoolFallbackBody(body: Buffer, fallbackModel: string): Buffer | null; | ||
| export declare function parseProviderPrefix(model: string): { | ||
@@ -293,2 +318,12 @@ provider: 'openai' | 'claude'; | ||
| strictTemplate?: boolean; | ||
| /** | ||
| * Pool routing strategy. `headroom` (default) spreads new conversations | ||
| * to the seat with the most headroom; `fill-first` concentrates them on | ||
| * the alphabetically-first eligible seat until it drains to the 2% | ||
| * floor, then spills to the next — primary/backup semantics where a | ||
| * `z-backup` seat stays untouched until `a-main` is actually drained. | ||
| * Sticky bindings behave identically in both modes. Sourced from | ||
| * `--pool-strategy` / `DARIO_POOL_STRATEGY` / config `pool.strategy`. | ||
| */ | ||
| poolStrategy?: string; | ||
| /** Max concurrent in-flight requests. Default 10. dario#80. */ | ||
@@ -319,2 +354,23 @@ maxConcurrent?: number; | ||
| /** | ||
| * Pool-exhausted fallback model (strictly opt-in; off when unset/empty). | ||
| * When the Claude pool can't serve — selection finds every seat drained | ||
| * or cooling, or a mid-flight 429 has no peer left — OpenAI-shape | ||
| * requests (/v1/chat/completions) are forwarded to the configured | ||
| * openai-compat backend with the model swapped to this value, instead | ||
| * of surfacing the 429/503. Responses carry `x-dario-pool-fallback`. | ||
| * Anthropic-shape requests keep the error: dario has no OpenAI→Anthropic | ||
| * response translation. Inert without a configured backend. Sourced from | ||
| * `--pool-fallback` / `DARIO_POOL_FALLBACK` / config `poolFallback.model`. | ||
| */ | ||
| poolFallbackModel?: string; | ||
| /** | ||
| * User-defined model aliases, client-visible name (lowercase) → target. | ||
| * Resolved per request BEFORE provider-prefix parsing (a target may | ||
| * carry a prefix and retarget the backend) and advertised on | ||
| * /v1/models. One step, never recursive. See parseModelAliasSpecs. | ||
| * Sourced from config `modelAliases` < `DARIO_MODEL_ALIASES` < | ||
| * repeatable `--model-alias=name=target`, merged per-key by the CLI. | ||
| */ | ||
| modelAliases?: Record<string, string>; | ||
| /** | ||
| * Append-only request log file. One JSON line per completed request, | ||
@@ -321,0 +377,0 @@ * with secrets scrubbed via redactSecrets. Useful for backgrounded |
@@ -41,2 +41,3 @@ /** | ||
| { path: 'sessionStart.jitterMs', label: 'Session-start jitter', type: 'number' }, | ||
| { path: 'pool.strategy', label: 'Pool strategy', type: 'string', hint: '"headroom" (default) or "fill-first"' }, | ||
| // ── Overage-guard (v4.1, dario#288) ───────────────────────── | ||
@@ -259,2 +260,3 @@ { path: 'overageGuard.enabled', label: 'Overage-guard', type: 'bool', hint: 'halt proxy on any representative-claim=overage' }, | ||
| 'overageGuard.behavior': ['halt', 'warn'], | ||
| 'pool.strategy': ['headroom', 'fill-first'], | ||
| }; | ||
@@ -261,0 +263,0 @@ function doSave(state) { |
+1
-1
| { | ||
| "name": "@askalf/dario", | ||
| "version": "5.2.5", | ||
| "version": "5.2.6", | ||
| "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,0 +5,0 @@ "type": "module", |
+1
-0
@@ -13,2 +13,3 @@ <div align="center"> | ||
| <a href="https://scorecard.dev/viewer/?uri=github.com/askalf/dario"><img src="https://img.shields.io/ossf-scorecard/github.com/askalf/dario?label=OpenSSF%20Scorecard&color=6f42c1" alt="OpenSSF Scorecard"></a> | ||
| <a href="https://www.bestpractices.dev/projects/13638"><img src="https://www.bestpractices.dev/projects/13638/badge" alt="OpenSSF Best Practices"></a> | ||
| <a href="https://github.com/askalf/dario/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/@askalf/dario?color=6f42c1" alt="License"></a> | ||
@@ -15,0 +16,0 @@ <a href="https://www.npmjs.com/package/@askalf/dario"><img src="https://img.shields.io/npm/dm/@askalf/dario?color=6f42c1" alt="Downloads"></a> |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential malware
Supply chain riskAI has identified this package as malware. This is a strong signal that the package may be malicious.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential malware
Supply chain riskAI has identified this package as malware. This is a strong signal that the package may be malicious.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1402341
1.68%27117
1.54%358
0.28%121
3.42%