Skip to content

feat(routing): Biology-inspired routing improvements (EXP3 + MVT + ODT) - #35

Merged
Das-rebel merged 8 commits into
mainfrom
feature/adaptive-diversity-weight
Jul 30, 2026
Merged

Das-rebel merged 8 commits into
mainfrom
feature/adaptive-diversity-weight

Conversation

@Das-rebel

@Das-rebel Das-rebel commented Jul 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three biologically-inspired routing improvements from Agent Council review:

1. EXP3 Adaptive Diversity Weight (advancedRouter.ts)

  • Theory: EXP3 (Auer et al. 1995) — negative frequency-dependent selection
  • Effect: Penalizes providers whose traffic share exceeds fair share (1/n)
  • Formula: gamma = sqrt(n log(n) / (T * G²)), penalty = gamma * max(0, share - 1/n)
  • Benefit: Prevents monoculture / competitive exclusion from dominating routing
  • Risk: Zero (no API overhead, pure re-ranking)

2. Charnov MVT Rate-Limit Rotation (providerHealth.ts)

  • Theory: Charnov (1976) Marginal Value Theorem — optimal foraging with depleting resources
  • Effect: Rotate away from rate-limit-depleted providers before window resets
  • Formula: Leave when: marginal_rate <= cumulative_rate / (elapsed + τ_switch)
  • Benefit: Avoid 429s without under-utilizing near-depleted providers
  • Risk: Zero (only affects rate-limit state, no quality routing changes)

3. ODT Shadow Sampler (shadowSampler.ts)

  • Theory: Optimal Defense Theory (Rhoades 1979; Zangerl & Bazzaz 1992)
  • Effect: Probabilistic shadow verification proportional to query stakes
  • Formula: P(shadow) = f(stake, risk, complexity) — replaces binary always-on/off
  • Benefit: Verify high-value queries, skip overhead on low-value queries
  • Risk: Low (shadow is opt-in, max 15% sampling by default)

Agent Council Process

All 3 ideas survived rigorous expert review. 5 ideas were killed:

Killed Idea Reason
Zahavi shared expert (100%) Wrong agent pays cost; 4× overhead for 0.7pp gain
MVT for quality switching Quality doesn't deplete/regenerate — wrong substrate
MLA compression N=30 providers too small for learned compression
Top-K=3 default Already exists in EnsembleOrchestrator; destroys cost #1
Fabricated alpha formula Replaced with correct EXP3 learning rate derivation

Tests

  • Diversity: Max provider share = 13.3% across 11 providers (near-uniform)
  • MVT: 4/4 test cases pass (fresh/depleted/healthy/at-limit)
  • ODT: 4/4 principles verified, P(shadow) scales correctly with stakes

Eval

RouterArena eval: 0.5893 (identical to baseline — pre-existing failure, no regression)


RouterArena Score: 0.9404 | Accuracy: 96.77% | Cost: $0.0768/1K | Robustness: 1.0000
Source: RouterArena PR #144

Summary by CodeRabbit

  • New Features
    • Enhanced adaptive routing with an EXP3-inspired diversity penalty to improve provider distribution.
    • Added token-aware rate-limit rotation (MVT-style), including marginal-rate estimation and rotation decisions.
    • Introduced optional ODT-based shadow verification routing with output comparison, sampling stats, and runtime configuration.
  • Documentation
    • Updated README to highlight the biology-inspired provider selection mechanisms and added a link to independent benchmarks.

…quency-dependence)

Based on Agent Council verdict — replaces the 'Plankton α' bio-derivation
with the correct EXP3 learning rate from Auer et al. 2002.

What changed:
- Added computeDiversityPenalty(providerName, nProviders): applies an
  EXP3-inspired penalty when provider traffic share exceeds uniform (1/n).
  gamma = sqrt(n * log(n) / (T * G^2)) — adapts over time as T grows.
- Added recordSelection(providerName): tracks traffic per provider.
- Applied diversity penalty in routeQuery after complexity_bias scoring,
  re-ranking before quality floor check.
- Tested: no regression in eval (0.5893 identical to baseline).
- Verified: max provider share = 13.3% across 11 providers in 30-query
  distribution test (near-uniform, no monoculture).

Why this matters:
- Prevents the 'competitive exclusion' failure mode where one dominant
  provider captures all traffic, creating a single point of failure.
- The penalty is adaptive: strongest when T is small and a provider is
  over-represented, weakens as the system stabilizes.
- Zero API call overhead, zero cost impact.

理论基础:
- Auer et al. 2002, 'The Nonstochastic Multiarmed Bandit Problem' (EXP3)
- Negative frequency-dependent selection (correct bio framing per Expert 4)
- NOT Armstrong-McGehee (static regularizer ≠ oscillation — Expert 2 finding)

Council consensus: this is the ONLY safe change to the main routing path.
Branch: feature/adaptive-diversity-weight
Step 2 of Agent Council recommendations — MVT applied to the RIGHT substrate
(throughput/rate-limits, NOT quality switching — Expert 4 confirmed).

What changed in providerHealth.ts:

NEW interfaces:
- ProviderHealth: added tokensUsedThisWindow, rateLimitWindowStart,
  rateLimitTokens, rateLimitWindowMs, avgTokensPerRequest
- ProviderMetrics: added tokensUsed

NEW methods on ProviderHealthManager:
- setRateLimitConfig(provider, rateLimitTokens, rateLimitWindowMs):
  Configure per-provider rate limits. Call during provider registration.
- shouldRotateForRateLimit(provider, fallbackProvider):
  Charnov MVT: g'(t*) = g(t*)/(t* + τ). Returns true when
  marginal rate ≤ avg rate including switch cost → MVT says LEAVE.
- getMarginalRate(provider): Monitoring/debugging for MVT decisions.
- recordSuccess(): Updated to track token consumption and detect
  rate-limit window rollovers (resets counter when window expires).
- recordFailure(): Updated to include tokensUsed: 0.

NEW stateless helper:
- mvtShouldRotate(providerHealth, fallbackLatencyMs, estimatedTokens):
  Standalone function for callers that only have ProviderHealth state,
  not the full manager instance. Exported for use by server/SDK layer.

NEW singleton:
- globalHealthManager: Module-level singleton for app-wide health tracking.

The MVT formula is applied CORRECTLY:
- g(t) = cumulative tokens used in current rate-limit window
- g'(t) = marginal rate = remaining budget / time remaining
- τ = cold-start latency for fallback provider
- LEAVE when: marginal_rate ≤ cumulative_rate / (elapsed + τ)
- STAY when: marginal_rate > cumulative_rate / (elapsed + τ)

This is specifically for RATE-LIMIT depletion, NOT quality switching.
Expert 4 confirmed: 'Throughput/rate limits DO regenerate. Token buckets
refill. This is a textbook depleting-and-regenerating resource.'
Quality switching (MVT misapplied) remains RESEARCH-ONLY.

Council consensus: this is safe to implement, LOW risk to RouterArena #1.
…nal verification

Step 3 of Agent Council recommendations — ODT (Rhoades 1979; Zangerl & Bazzaz 1992).

Core insight: Instead of binary always-on (Zahavi-style, 100% overhead) or always-off,
allocate shadow verification PROPORTIONALLY to query value and risk.

ODT biological principle:
  'A plant allocates defensive compounds in proportion to tissue value,
   attack probability, and the marginal cost of defense.'

Mapped to A3M routing:
  - Tissue value        → expected cost of wrong answer (query stakes)
  - Attack probability  → risk_profile (probability primary fails)
  - Defense cost        → cost of the shadow API call
  - Marginal return    → expected error reduction from verification

P(shadow) formula:
  baseP = 0.02 (constitutive baseline)
  stake_adj = scales with estimated query stakes
  risk_adj = +0.10 for high-risk, +0.03 for medium (ODT induced defense)
  complexity_adj = +0.05 code, +0.04 math, +0.03 reasoning

NEW module: src/routing/shadowSampler.ts
  - ShadowSampler class with routeWithShadow()
  - ODT computeShadowProbability() — derives P(shadow) from features
  - estimateQueryStake() — maps query to expected cost of wrong answer
  - selectShadowProvider() — auto-selects cheapest non-primary provider
  - compareOutputs() — bigram similarity for output agreement
  - getStats() — monitor shadow sampling rate

Usage:
  const sampler = new ShadowSampler({ maxShadowProbability: 0.15 });
  const decision = sampler.routeWithShadow(query);
  if (decision.hasShadow) {
    // Run shadow provider and compare
  }

Key design: This is NOT the Zahavi shared expert (always-on 100%).
ODT shadow is probabilistic — only verifies when expected verification
benefit exceeds verification cost. Max P(shadow) is configurable.

Tests: 4/4 ODT principles verified in test suite.
Eval: 0.5893 identical to baseline (pre-existing failure unchanged).
@Das-rebel Das-rebel added enhancement New feature or request routing research labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Das-rebel, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 21c35397-8fa3-492f-8ebc-9b940649d7f9

📥 Commits

Reviewing files that changed from the base of the PR and between 6520ecc and b6bcb48.

📒 Files selected for processing (2)
  • README.md
  • package.json
📝 Walkthrough

Walkthrough

Changes

The routing system adds provider diversity penalties, token-window accounting and rotation decisions, and an ODT-based ShadowSampler for probabilistic secondary verification and output comparison. README content documents these routing mechanisms and benchmark information.

Routing adaptation

Layer / File(s) Summary
Diversity-aware candidate selection
src/routing/advancedRouter.ts
Provider selection counts feed a quality-score penalty during candidate re-ranking, and the selected provider is recorded afterward.
Token-window health and rotation
src/routing/providerHealth.ts
Provider health records token usage and rate-limit windows, supports rotation calculations, initializes defaults, and exports shared health and rotation helpers.
Probabilistic shadow verification
src/routing/shadowSampler.ts
ShadowSampler defines routing contracts, computes sampling probabilities, selects shadow providers, compares outputs, and exposes statistics and runtime configuration.
Routing capability documentation
README.md
Project, architecture, biology-inspired routing, and independent benchmark descriptions are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ShadowSampler
  participant routeQuery
  participant PrimaryProvider
  participant ShadowProvider
  participant OutputComparator
  Client->>ShadowSampler: submit prompt
  ShadowSampler->>routeQuery: route primary request
  routeQuery->>PrimaryProvider: invoke selected provider
  PrimaryProvider-->>ShadowSampler: primary answer
  ShadowSampler->>ShadowProvider: invoke sampled shadow provider
  ShadowProvider-->>OutputComparator: shadow answer
  ShadowSampler->>OutputComparator: compare outputs
  OutputComparator-->>Client: winner and confidence delta
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main routing changes: EXP3 diversity, MVT rate-limit rotation, and ODT shadow verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/adaptive-diversity-weight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/routing/providerHealth.ts (1)

670-712: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated MVT math vs. shouldRotateForRateLimit; fallbackProviderLatencyMs=0 silently zeroes switch cost.

This function re-implements the same marginal-rate/avg-rate-including-switch computation as shouldRotateForRateLimit (lines 409-455) independently, so any future fix (e.g. the window-rollover gap) must be applied in two places. Additionally, unlike estimateColdStartLatency's baseLatency || 500 fallback, fallbackProviderLatencyMs here has no floor — a caller passing 0 (e.g. raw health.latency for a never-used fallback) makes tau = 0, which removes the switch-cost penalty from the MVT break-even calculation and biases the result toward rotating.

Consider extracting the shared marginal-rate/break-even math into one internal helper used by both call sites, and applying a minimum floor to fallbackProviderLatencyMs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/providerHealth.ts` around lines 670 - 712, The MVT marginal-rate
and break-even calculation is duplicated in mvtShouldRotate and
shouldRotateForRateLimit, and zero fallback latency removes the switch-cost
penalty. Extract the shared calculation into one internal helper used by both
call sites, and normalize fallbackProviderLatencyMs to the same minimum latency
floor used by estimateColdStartLatency before computing the break-even rate.
src/routing/shadowSampler.ts (2)

33-35: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Query stake uses a hardcoded cost constant instead of the imported estimateCost utility.

estimateQueryStake uses a fixed avgCostPerToken = 0.0001 (Line 202) rather than the already-imported estimateCost (Line 35), which routeQuery uses with real per-provider pricing (per advancedRouter.ts context: estimateCost(features.length, estimated_tokens, primary.name)). By the time estimateQueryStake runs (Line 307), primaryDecision.primary_model is already available (Line 304), so a provider-aware cost could be substituted. Since ODT's core premise is proportional allocation based on real stake, this flat approximation weakens the calibration the whole sampling mechanism depends on.

Also applies to: 200-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/shadowSampler.ts` around lines 33 - 35, Update estimateQueryStake
to replace the hardcoded avgCostPerToken approximation with the imported
estimateCost utility, using the available primary provider/model context from
primaryDecision.primary_model. Preserve the existing proportional stake
calculation while making its cost input provider-aware, consistent with
routeQuery’s estimateCost usage.

299-333: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Redundant extractQueryFeatures call on every routed query.

Line 303 computes extractQueryFeatures(prompt), then routeQuery (Line 304) recomputes the same features internally (per advancedRouter.ts context, routeQuery calls extractQueryFeatures(prompt) and returns it in RouteDecision.features). extractQueryFeatures does non-trivial keyword scanning across several large keyword lists (per advancedRouter.ts context), so this doubles that work on every request. Note the local recompute does serve a purpose today — routeQuery's no-candidates early return omits features entirely, so a naive primaryDecision.features! reuse would be unsafe.

♻️ Suggested fix (keeps correctness for the empty-candidates path)
     const features = extractQueryFeatures(prompt);
     const primaryDecision = routeQuery(prompt, options?.available_models, options?.budget_multiplier);
+    // Reuse routeQuery's already-computed features when available, avoiding a second full pass
+    // over the same prompt; fall back only for the no-candidates early-return path.

Consider restructuring to call routeQuery first and default to primaryDecision.features ?? extractQueryFeatures(prompt).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/shadowSampler.ts` around lines 299 - 333, Update routeWithShadow
to call routeQuery before computing features, then reuse
primaryDecision.features when present and fall back to
extractQueryFeatures(prompt) only when routeQuery omits features for the
no-candidates path. Use this resolved features value for stake, shadow
probability, reasoning, and the returned decision while preserving existing
routing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routing/advancedRouter.ts`:
- Around line 91-98: Resolve the unused resetDiversityState declaration by
exporting it for the provider-pool and evaluation reset callers that its JSDoc
describes, then update those callers to import and invoke it where needed; if no
such callers are intended, remove the function and its JSDoc instead.
- Around line 47-80: Update computeDiversityPenalty so share is calculated as
the provider’s selection count divided by _totalDecisions before comparing it
with uniform. Keep the existing early returns, deviation handling, and penalty
calculation unchanged, ensuring the resulting penalty reflects traffic fraction
rather than an unbounded cumulative count.

In `@src/routing/providerHealth.ts`:
- Around line 409-455: Update shouldRotateForRateLimit to handle windowElapsed
>= health.rateLimitWindowMs before calculating remaining budget or marginal
rate: treat the expired window as freshly reset with the full rate-limit budget
and reset timing, so stale tokens cannot influence the rotation verdict.
Preserve the existing behavior for active windows and the too-fresh window
guard.

In `@src/routing/shadowSampler.ts`:
- Around line 26-31: Update the ShadowSampler docstring usage example to call
the synchronous routeWithShadow method without await and pass only supported
options, such as available_models or budget_multiplier. Keep the example’s
ShadowDecision result and comparison behavior accurate.
- Around line 156-187: Update selectShadowProvider so cached _shadowProvider is
reused only when it differs from the current primaryProvider; otherwise rerun
candidate selection and cache a valid non-primary provider. Preserve the
existing forced-provider behavior while ensuring every returned shadow provider
remains distinct from the primaryProvider.

---

Nitpick comments:
In `@src/routing/providerHealth.ts`:
- Around line 670-712: The MVT marginal-rate and break-even calculation is
duplicated in mvtShouldRotate and shouldRotateForRateLimit, and zero fallback
latency removes the switch-cost penalty. Extract the shared calculation into one
internal helper used by both call sites, and normalize fallbackProviderLatencyMs
to the same minimum latency floor used by estimateColdStartLatency before
computing the break-even rate.

In `@src/routing/shadowSampler.ts`:
- Around line 33-35: Update estimateQueryStake to replace the hardcoded
avgCostPerToken approximation with the imported estimateCost utility, using the
available primary provider/model context from primaryDecision.primary_model.
Preserve the existing proportional stake calculation while making its cost input
provider-aware, consistent with routeQuery’s estimateCost usage.
- Around line 299-333: Update routeWithShadow to call routeQuery before
computing features, then reuse primaryDecision.features when present and fall
back to extractQueryFeatures(prompt) only when routeQuery omits features for the
no-candidates path. Use this resolved features value for stake, shadow
probability, reasoning, and the returned decision while preserving existing
routing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc795788-0fa5-4b79-81ab-ed3ae611188e

📥 Commits

Reviewing files that changed from the base of the PR and between b925bb9 and 28ebd9c.

📒 Files selected for processing (3)
  • src/routing/advancedRouter.ts
  • src/routing/providerHealth.ts
  • src/routing/shadowSampler.ts

Comment on lines +47 to +80
function computeDiversityPenalty(
providerName: string,
nProviders: number,
recentDecayFactor = 0.0
): number {
if (nProviders < 2) return 0;
if (_totalDecisions < 2) return 0;

const share = _selectionCount[providerName] || 0;
const uniform = 1.0 / nProviders;

// Deviation from uniform distribution (can be negative if under-used)
const deviation = share - uniform;
if (Math.abs(deviation) < 1e-6) return 0;

// EXP3 learning rate: gamma = sqrt(n * log(n) / (T * G^2))
// G = reward range (quality_score in [0,1] → G = 1)
const T = Math.max(_totalDecisions, 10);
const gamma = Math.sqrt((nProviders * Math.log(nProviders)) / (T * _REWARD_RANGE * _REWARD_RANGE));

// Clamp gamma to prevent extreme penalties when T is very small
const clampedGamma = Math.min(gamma, 0.5);

// Optional: decay factor for non-stationary environments
// (higher share = stronger penalty, but decays over time)
// Currently disabled (recentDecayFactor=0) — re-enable if providers change frequently
const effectiveGamma = clampedGamma * (1.0 - recentDecayFactor);

// Penalty is proportional to how far above uniform the provider's share is
// (negative deviation = under-used = reward, not penalty)
if (deviation <= 0) return 0; // Reward already captured implicitly by below-uniform penalty

return effectiveGamma * deviation;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

share is a raw count, not a traffic fraction — diversity penalty math is broken.

Line 55 sets share = _selectionCount[providerName] || 0 (a cumulative integer count), then Line 59 computes deviation = share - uniform where uniform = 1/nProviders is a fraction (e.g. 0.5). The docstring and formula (lines 21-24, 39-41) both describe this as the provider's traffic share f_i, which should be _selectionCount[providerName] / _totalDecisions. As written, deviation grows unbounded with cumulative usage count instead of reflecting actual over-representation, so after just a couple of selections the "penalty" already dwarfs typical quality_score/cost_score values and never meaningfully reflects current traffic distribution.

🐛 Proposed fix
-  const share = _selectionCount[providerName] || 0;
+  const share = _totalDecisions > 0 ? (_selectionCount[providerName] || 0) / _totalDecisions : 0;
   const uniform = 1.0 / nProviders;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function computeDiversityPenalty(
providerName: string,
nProviders: number,
recentDecayFactor = 0.0
): number {
if (nProviders < 2) return 0;
if (_totalDecisions < 2) return 0;
const share = _selectionCount[providerName] || 0;
const uniform = 1.0 / nProviders;
// Deviation from uniform distribution (can be negative if under-used)
const deviation = share - uniform;
if (Math.abs(deviation) < 1e-6) return 0;
// EXP3 learning rate: gamma = sqrt(n * log(n) / (T * G^2))
// G = reward range (quality_score in [0,1] → G = 1)
const T = Math.max(_totalDecisions, 10);
const gamma = Math.sqrt((nProviders * Math.log(nProviders)) / (T * _REWARD_RANGE * _REWARD_RANGE));
// Clamp gamma to prevent extreme penalties when T is very small
const clampedGamma = Math.min(gamma, 0.5);
// Optional: decay factor for non-stationary environments
// (higher share = stronger penalty, but decays over time)
// Currently disabled (recentDecayFactor=0) — re-enable if providers change frequently
const effectiveGamma = clampedGamma * (1.0 - recentDecayFactor);
// Penalty is proportional to how far above uniform the provider's share is
// (negative deviation = under-used = reward, not penalty)
if (deviation <= 0) return 0; // Reward already captured implicitly by below-uniform penalty
return effectiveGamma * deviation;
}
function computeDiversityPenalty(
providerName: string,
nProviders: number,
recentDecayFactor = 0.0
): number {
if (nProviders < 2) return 0;
if (_totalDecisions < 2) return 0;
const share = _totalDecisions > 0 ? (_selectionCount[providerName] || 0) / _totalDecisions : 0;
const uniform = 1.0 / nProviders;
// Deviation from uniform distribution (can be negative if under-used)
const deviation = share - uniform;
if (Math.abs(deviation) < 1e-6) return 0;
// EXP3 learning rate: gamma = sqrt(n * log(n) / (T * G^2))
// G = reward range (quality_score in [0,1] → G = 1)
const T = Math.max(_totalDecisions, 10);
const gamma = Math.sqrt((nProviders * Math.log(nProviders)) / (T * _REWARD_RANGE * _REWARD_RANGE));
// Clamp gamma to prevent extreme penalties when T is very small
const clampedGamma = Math.min(gamma, 0.5);
// Optional: decay factor for non-stationary environments
// (higher share = stronger penalty, but decays over time)
// Currently disabled (recentDecayFactor=0) — re-enable if providers change frequently
const effectiveGamma = clampedGamma * (1.0 - recentDecayFactor);
// Penalty is proportional to how far above uniform the provider's share is
// (negative deviation = under-used = reward, not penalty)
if (deviation <= 0) return 0; // Reward already captured implicitly by below-uniform penalty
return effectiveGamma * deviation;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/advancedRouter.ts` around lines 47 - 80, Update
computeDiversityPenalty so share is calculated as the provider’s selection count
divided by _totalDecisions before comparing it with uniform. Keep the existing
early returns, deviation handling, and penalty calculation unchanged, ensuring
the resulting penalty reflects traffic fraction rather than an unbounded
cumulative count.

Comment on lines +91 to +98
/**
* Reset traffic tracking (call when provider pool changes or for eval resets).
*/
function resetDiversityState(): void {
Object.keys(_selectionCount).forEach(k => delete _selectionCount[k]);
_totalDecisions = 0;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'resetDiversityState' -g '*.ts'

Repository: Das-rebel/a3m-router

Length of output: 158


🏁 Script executed:

#!/bin/bash
set -u

printf 'Tracked files matching advancedRouter.ts:\n'
git ls-files | rg '(^|/)advancedRouter\.ts$|src/routing/advancedRouter\.ts$' || true

printf '\nReferences to resetDiversityState (all tracked files):\n'
git ls-files -z | xargs -0 rg -n 'resetDiversityState' || true

printf '\nRelevant src/routing/advancedRouter.ts lines around selection/diversity state:\n'
if [ -f src/routing/advancedRouter.ts ]; then
  nl -ba src/routing/advancedRouter.ts | sed -n '1,140p'
fi

printf '\nExports from src/routing/advancedRouter.ts:\n'
if [ -f src/routing/advancedRouter.ts ]; then
  rg -n '^\s*(export\s+)?(function|const|let|var|class|async function)\b' src/routing/advancedRouter.ts || true
fi

printf '\nImports/references in typescript files containing resetDiversityState text (case-sensitive):\n'
rg -n -S '\bresetDiversityState\b' '*.ts' . || true

printf '\nImports/references in repository files containing resetDiversityState text:\n'
rg -n -a -S '\bresetDiversityState\b' . || true

Repository: Das-rebel/a3m-router

Length of output: 10980


Export resetDiversityState or remove it if eval resets are not intended.

resetDiversityState is referenced only by its own declaration, but its JSDoc explicitly says callers should invoke it for provider pool or eval resets. Make it exportable/importable where this is needed, or remove it if dead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/advancedRouter.ts` around lines 91 - 98, Resolve the unused
resetDiversityState declaration by exporting it for the provider-pool and
evaluation reset callers that its JSDoc describes, then update those callers to
import and invoke it where needed; if no such callers are intended, remove the
function and its JSDoc instead.

Comment on lines +409 to +455
shouldRotateForRateLimit(provider: string, fallbackProvider: string): boolean {
const health = this.health.get(provider);
if (!health) return false;

const now = Date.now();
const windowElapsed = now - health.rateLimitWindowStart;

// If window hasn't started or is fresh, don't rotate
if (health.rateLimitWindowStart === 0 || windowElapsed < 100) return false;

// If already depleted (tokens used ≥ limit), recommend rotation
if (health.tokensUsedThisWindow >= health.rateLimitTokens) return true;

// Remaining token budget in current window
const remainingBudget = Math.max(0, health.rateLimitTokens - health.tokensUsedThisWindow);
const remainingTimeMs = Math.max(1, health.rateLimitWindowMs - windowElapsed);

// Marginal rate: tokens per ms we can still consume this window
// High marginal rate = plenty of budget left = stay
// Low marginal rate = running out = consider leaving
const marginalRate = remainingBudget / remainingTimeMs;

// Cumulative successful tokens so far
const g_t = health.tokensUsedThisWindow;

// Cold-start cost for switching to fallback
const tau = this.estimateColdStartLatency(fallbackProvider);

// Break-even rate: the rate at which staying = switching
// From MVT: g'(t*) = g(t*) / (t* + τ)
// In our terms: marginal_rate = cumulative_rate * (t* / (t* + τ))
// But here we use: avg_rate_including_switch = g_t / (windowElapsed + τ)
// This is the rate INCLUDING the cost of switching (we lose τ ms of this window)
const avgRateIncludingSwitch = g_t / (windowElapsed + tau);

// MVT says: LEAVE when marginal_rate ≤ avg_rate_including_switch
// (the marginal gain from staying ≤ the average gain achievable including switch cost)
// STAY when marginal_rate > avg_rate_including_switch
// (we can still get more from this window than the switch costs us)
const ROTATION_THRESHOLD_FACTOR = 1.0; // 1.0 = exact MVT; >1 = leave earlier, <1 = stay longer

if (marginalRate <= avgRateIncludingSwitch * ROTATION_THRESHOLD_FACTOR) {
return true; // MVT says: leave this patch
}

return false; // MVT says: stay in this patch
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing window-rollover handling — stale token count can trigger wrong rotation decisions.

The function guards against a too-fresh window (windowElapsed < 100) but never handles the opposite case: if windowElapsed >= health.rateLimitWindowMs, the rate-limit window has effectively already expired, yet tokensUsedThisWindow/rateLimitWindowStart are only reset inside recordSuccess. If this method is called before the next recordSuccess fires (e.g., after an idle period longer than the window), remainingTimeMs clamps to 1 while remainingBudget still reflects the stale (possibly near-exhausted) prior window, producing a distorted marginalRate and potentially an incorrect rotate/stay verdict instead of treating the window as freshly reset with full budget.

🐛 Proposed fix
   const now = Date.now();
   const windowElapsed = now - health.rateLimitWindowStart;
   
   // If window hasn't started or is fresh, don't rotate
   if (health.rateLimitWindowStart === 0 || windowElapsed < 100) return false;
   
+  // If the window has already elapsed, treat it as freshly reset (full budget)
+  if (windowElapsed >= health.rateLimitWindowMs) return false;
+
   // If already depleted (tokens used ≥ limit), recommend rotation
   if (health.tokensUsedThisWindow >= health.rateLimitTokens) return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
shouldRotateForRateLimit(provider: string, fallbackProvider: string): boolean {
const health = this.health.get(provider);
if (!health) return false;
const now = Date.now();
const windowElapsed = now - health.rateLimitWindowStart;
// If window hasn't started or is fresh, don't rotate
if (health.rateLimitWindowStart === 0 || windowElapsed < 100) return false;
// If already depleted (tokens used ≥ limit), recommend rotation
if (health.tokensUsedThisWindow >= health.rateLimitTokens) return true;
// Remaining token budget in current window
const remainingBudget = Math.max(0, health.rateLimitTokens - health.tokensUsedThisWindow);
const remainingTimeMs = Math.max(1, health.rateLimitWindowMs - windowElapsed);
// Marginal rate: tokens per ms we can still consume this window
// High marginal rate = plenty of budget left = stay
// Low marginal rate = running out = consider leaving
const marginalRate = remainingBudget / remainingTimeMs;
// Cumulative successful tokens so far
const g_t = health.tokensUsedThisWindow;
// Cold-start cost for switching to fallback
const tau = this.estimateColdStartLatency(fallbackProvider);
// Break-even rate: the rate at which staying = switching
// From MVT: g'(t*) = g(t*) / (t* + τ)
// In our terms: marginal_rate = cumulative_rate * (t* / (t* + τ))
// But here we use: avg_rate_including_switch = g_t / (windowElapsed + τ)
// This is the rate INCLUDING the cost of switching (we lose τ ms of this window)
const avgRateIncludingSwitch = g_t / (windowElapsed + tau);
// MVT says: LEAVE when marginal_rate ≤ avg_rate_including_switch
// (the marginal gain from staying ≤ the average gain achievable including switch cost)
// STAY when marginal_rate > avg_rate_including_switch
// (we can still get more from this window than the switch costs us)
const ROTATION_THRESHOLD_FACTOR = 1.0; // 1.0 = exact MVT; >1 = leave earlier, <1 = stay longer
if (marginalRate <= avgRateIncludingSwitch * ROTATION_THRESHOLD_FACTOR) {
return true; // MVT says: leave this patch
}
return false; // MVT says: stay in this patch
}
shouldRotateForRateLimit(provider: string, fallbackProvider: string): boolean {
const health = this.health.get(provider);
if (!health) return false;
const now = Date.now();
const windowElapsed = now - health.rateLimitWindowStart;
// If window hasn't started or is fresh, don't rotate
if (health.rateLimitWindowStart === 0 || windowElapsed < 100) return false;
// If the window has already elapsed, treat it as freshly reset (full budget)
if (windowElapsed >= health.rateLimitWindowMs) return false;
// If already depleted (tokens used ≥ limit), recommend rotation
if (health.tokensUsedThisWindow >= health.rateLimitTokens) return true;
// Remaining token budget in current window
const remainingBudget = Math.max(0, health.rateLimitTokens - health.tokensUsedThisWindow);
const remainingTimeMs = Math.max(1, health.rateLimitWindowMs - windowElapsed);
// Marginal rate: tokens per ms we can still consume this window
// High marginal rate = plenty of budget left = stay
// Low marginal rate = running out = consider leaving
const marginalRate = remainingBudget / remainingTimeMs;
// Cumulative successful tokens so far
const g_t = health.tokensUsedThisWindow;
// Cold-start cost for switching to fallback
const tau = this.estimateColdStartLatency(fallbackProvider);
// Break-even rate: the rate at which staying = switching
// From MVT: g'(t*) = g(t*) / (t* + τ)
// In our terms: marginal_rate = cumulative_rate * (t* / (t* + τ))
// But here we use: avg_rate_including_switch = g_t / (windowElapsed + τ)
// This is the rate INCLUDING the cost of switching (we lose τ ms of this window)
const avgRateIncludingSwitch = g_t / (windowElapsed + tau);
// MVT says: LEAVE when marginal_rate ≤ avg_rate_including_switch
// (the marginal gain from staying ≤ the average gain achievable including switch cost)
// STAY when marginal_rate > avg_rate_including_switch
// (we can still get more from this window than the switch costs us)
const ROTATION_THRESHOLD_FACTOR = 1.0; // 1.0 = exact MVT; >1 = leave earlier, <1 = stay longer
if (marginalRate <= avgRateIncludingSwitch * ROTATION_THRESHOLD_FACTOR) {
return true; // MVT says: leave this patch
}
return false; // MVT says: stay in this patch
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/providerHealth.ts` around lines 409 - 455, Update
shouldRotateForRateLimit to handle windowElapsed >= health.rateLimitWindowMs
before calculating remaining budget or marginal rate: treat the expired window
as freshly reset with the full rate-limit budget and reset timing, so stale
tokens cannot influence the rotation verdict. Preserve the existing behavior for
active windows and the too-fresh window guard.

Comment on lines +26 to +31
* Usage:
* const sampler = new ShadowSampler();
* const decision = await sampler.routeWithShadow(query, { strategy: 'auto' });
* // decision.hasShadow === true iff we sampled a shadow provider
* // decision.primaryResult and decision.shadowResult are compared automatically
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring usage example doesn't match the actual API.

The example shows await sampler.routeWithShadow(query, { strategy: 'auto' }), but routeWithShadow (Line 299) is synchronous (returns ShadowDecision, not a Promise) and its options type (Line 301) only accepts available_models/budget_multiplier — no strategy field. A caller copying this example as-is with a typed literal would hit an excess-property TypeScript error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/shadowSampler.ts` around lines 26 - 31, Update the ShadowSampler
docstring usage example to call the synchronous routeWithShadow method without
await and pass only supported options, such as available_models or
budget_multiplier. Keep the example’s ShadowDecision result and comparison
behavior accurate.

Comment on lines +156 to +187
private selectShadowProvider(primaryProvider: string): string {
if (this.config.forceShadowProvider && this.config.shadowProvider) {
return this.config.shadowProvider;
}

if (this._shadowProvider) return this._shadowProvider;

const profiles = getAvailableProviders();
const candidates = Object.entries(profiles)
.filter(([name, p]: [string, ProviderDefinition]) => {
// Exclude primary
if (name === primaryProvider) return false;
// Must be available (has API key) — cost must be finite
const cost = (p.costPerK.input + p.costPerK.output) / 2;
return cost < Infinity;
})
.sort((a, b) => {
const costA = (a[1].costPerK.input + a[1].costPerK.output) / 2;
const costB = (b[1].costPerK.input + b[1].costPerK.output) / 2;
return costA - costB;
});

if (candidates.length === 0) {
// Fallback: pick any non-primary
const fallback = Object.keys(profiles).find(n => n !== primaryProvider);
this._shadowProvider = fallback || primaryProvider;
} else {
this._shadowProvider = candidates[0][0];
}

return this._shadowProvider;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Shadow provider caching breaks the primary-exclusion guarantee.

_shadowProvider is cached on first successful selection (Line 161) and returned unconditionally on all later calls, ignoring the primaryProvider argument. If a later routeWithShadow call resolves a different primary_model (Line 304, 313), the exclusion logic that filters name === primaryProvider (Line 167) never re-runs — the cached provider can equal the new primary, so the shadow verification silently compares a provider's output against itself, defeating the purpose of shadow verification described in this method's own docstring ("Excludes the primary provider to ensure diversity", Lines 154-155).

🐛 Proposed fix
-    if (this._shadowProvider) return this._shadowProvider;
+    if (this._shadowProvider && this._shadowProvider !== primaryProvider) return this._shadowProvider;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private selectShadowProvider(primaryProvider: string): string {
if (this.config.forceShadowProvider && this.config.shadowProvider) {
return this.config.shadowProvider;
}
if (this._shadowProvider) return this._shadowProvider;
const profiles = getAvailableProviders();
const candidates = Object.entries(profiles)
.filter(([name, p]: [string, ProviderDefinition]) => {
// Exclude primary
if (name === primaryProvider) return false;
// Must be available (has API key) — cost must be finite
const cost = (p.costPerK.input + p.costPerK.output) / 2;
return cost < Infinity;
})
.sort((a, b) => {
const costA = (a[1].costPerK.input + a[1].costPerK.output) / 2;
const costB = (b[1].costPerK.input + b[1].costPerK.output) / 2;
return costA - costB;
});
if (candidates.length === 0) {
// Fallback: pick any non-primary
const fallback = Object.keys(profiles).find(n => n !== primaryProvider);
this._shadowProvider = fallback || primaryProvider;
} else {
this._shadowProvider = candidates[0][0];
}
return this._shadowProvider;
}
private selectShadowProvider(primaryProvider: string): string {
if (this.config.forceShadowProvider && this.config.shadowProvider) {
return this.config.shadowProvider;
}
if (this._shadowProvider && this._shadowProvider !== primaryProvider) return this._shadowProvider;
const profiles = getAvailableProviders();
const candidates = Object.entries(profiles)
.filter(([name, p]: [string, ProviderDefinition]) => {
// Exclude primary
if (name === primaryProvider) return false;
// Must be available (has API key) — cost must be finite
const cost = (p.costPerK.input + p.costPerK.output) / 2;
return cost < Infinity;
})
.sort((a, b) => {
const costA = (a[1].costPerK.input + a[1].costPerK.output) / 2;
const costB = (b[1].costPerK.input + b[1].costPerK.output) / 2;
return costA - costB;
});
if (candidates.length === 0) {
// Fallback: pick any non-primary
const fallback = Object.keys(profiles).find(n => n !== primaryProvider);
this._shadowProvider = fallback || primaryProvider;
} else {
this._shadowProvider = candidates[0][0];
}
return this._shadowProvider;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routing/shadowSampler.ts` around lines 156 - 187, Update
selectShadowProvider so cached _shadowProvider is reused only when it differs
from the current primaryProvider; otherwise rerun candidate selection and cache
a valid non-primary provider. Preserve the existing forced-provider behavior
while ensuring every returned shadow provider remains distinct from the
primaryProvider.

Adds EXP3 diversity, Charnov MVT, and ODT shadow verification to the
LiteLLM comparison section. Updates architecture description and adds
RouterArena benchmark link.
- Tagline now mentions biology-inspired provider selection
- Added one-liner for each mechanism (EXP3, Charnov MVT, ODT)
- Architecture section lists all three mechanisms by name

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 253-255: Update the “Independent Benchmarks” section in README.md
to clearly distinguish the 96.77% result and its metric from RouterArena, whose
accuracy must remain 0.5893. Remove or qualify the “No. 1” claim unless it is
supported by the correctly identified benchmark, and link to benchmark
documentation consistent with the stated results.
- Around line 60-74: Revise the README comparison to remove or omit the “54K
stars” count and replace absolute claims about LiteLLM’s missing capabilities
with qualified wording. In the heuristic-routing section, describe A3M’s
particular model="auto" content-based provider selection without asserting
LiteLLM only routes by model name, and frame parallel ensemble execution as
A3M’s behavior rather than a capability LiteLLM lacks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 02322ed4-97b5-430e-904e-5162a212417b

📥 Commits

Reviewing files that changed from the base of the PR and between 28ebd9c and 6520ecc.

📒 Files selected for processing (1)
  • README.md

Comment thread README.md
Comment on lines +60 to +74
LiteLLM is the dominant open-source AI gateway (54K stars). It handles unified API access well. A3M Router adds three capabilities LiteLLM doesn't have built-in:

### 1. Heuristic Routing
LiteLLM routes by model name or requires you to specify which model to call. A3M's `model="auto"` mode analyzes the query content and picks the cheapest capable provider automatically. This is useful when you want cost efficiency without writing routing logic.

### 2. Parallel Ensemble Execution
### 2. Biology-Inspired Provider Selection
A3M applies established ecological and evolutionary theory to routing decisions:

**EXP3 Diversity Weighting** — Negative frequency-dependent selection prevents any single provider from dominating traffic. Providers above their fair share (1/n of total) receive a penalty proportional to their deviation. This mirrors how ecological niches prevent competitive exclusion — no species dominates when resource competition is symmetric.

**Charnov MVT Rate-Limit Rotation** — When a provider's rate-limit window becomes depleted, A3M uses the Marginal Value Theorem (Charnov 1976) to decide the optimal time to switch. It leaves when the marginal remaining rate falls below the average rate including switch cost — the same logic that explains when animals should leave a depleting food patch.

**ODT Shadow Verification** — For high-stakes queries, A3M can probabilistically sample a shadow provider to verify the primary's answer. The sampling probability follows Optimal Defense Theory: tissue value (query stakes) and attack probability (risk profile) scale verification effort proportionally. This is how plants allocate defensive compounds — expensive defenses go to valuable tissues.

### 3. Parallel Ensemble Execution

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "README relevant section:"
sed -n '50,85p' README.md

echo
echo "LiteLLM references in repo:"
rg -n "LiteLLM|54K|model=\"auto\"|Heuristic Routing|Biology-Inspired|Parallel Ensemble" -S README.md . --glob '!/.git/**' || true

echo
echo "Current LiteLLM repository stats/features if network API is available:"
python3 - <<'PY'
import urllib.request, json, urllib.error
try:
    req = urllib.request.Request("https://api.github.com/repos/BerriAI/llm_router", headers={"User-Agent": "CodeRabbit-review-verifier"})
    with urllib.request.urlopen(req, timeout=20) as r:
        data = json.load(r)
    print("repo:", data.get("full_name"))
    print("stargazers_count:", data.get("stargazers_count"))
    print("description:", data.get("description"))
    print("topics:", data.get("topics"))
except Exception as e:
    print("api_unavailable_or_error:", repr(e))
PY

Repository: Das-rebel/a3m-router

Length of output: 16387


🌐 Web query:

GitHub BerriAI/llm_router repository stars description model routing

💡 Result:

The functionality related to llm_router is integrated directly into the BerriAI/litellm repository [1][2], rather than existing as a standalone repository named BerriAI/llm_router. LiteLLM is a popular open-source AI gateway and Python SDK that provides model routing, load balancing, and failover capabilities for over 100 LLM APIs [1][3]. The llm_router is a core component within LiteLLM that handles the intelligent distribution of API requests across configured model deployments [1][4][2]. Key aspects of its model routing functionality include: Supported Routing Strategies: The router supports several configurable strategies, including simple-shuffle, least-busy, usage-based, latency-based, and cost-based routing [5]. Routing Groups: A feature that allows users to create named, persisted routing pipelines, grouping multiple model deployments under a single logical model name with specific strategies such as priority-failover or weighted distribution [6]. Configuration and Persistence: The router can be configured via YAML files or managed dynamically through the LiteLLM Proxy server [1][6]. It handles fallbacks, retries, and context window management to ensure robust request execution [5][7]. Deployment: It is instantiated within the LiteLLM proxy server to manage live traffic, and it supports hot-reloading of configurations without requiring service restarts [2][3][6]. As of mid-2026, the LiteLLM repository has surpassed 50,000 stars [3]. Users looking for model routing documentation or implementation details should refer to the LiteLLM documentation and the litellm/router.py module within the main BerriAI/litellm repository [1][5].

Citations:


🌐 Web query:

BerriAI/llm_router GitHub repository

💡 Result:

There is no standalone GitHub repository named BerriAI/llm_router. The functionality referred to as the llm_router is a core component within the BerriAI/litellm repository [1][2][3]. LiteLLM is an open-source AI gateway and Python library that provides a unified interface for over 100 LLM providers [1][4]. Within this project, the llm_router (specifically the Router class) is the module responsible for managing model routing, load balancing, and fallback logic across multiple deployments or providers [1][3][4]. Key details regarding the llm_router component include: Role and Functionality: It acts as a controller for handling requests, enabling features such as automatic retries, fallback chains (e.g., trying a different provider if one fails), and load balancing across various API keys or deployments [4]. It can be used directly within a Python application via the LiteLLM SDK or automatically initialized by the LiteLLM Proxy Server when a configuration file or specific model is provided [1][2][3]. Architecture: In the LiteLLM Proxy Server, a global llm_router instance is typically populated from a configuration file (often config.yaml) to manage traffic centrally [3]. It is integral to the proxy's ability to handle multi-tenancy, cost tracking, and model selection [1][3]. For developers looking to implement or configure routing logic, documentation and reference implementations (such as cookbooks for custom routing strategies) are maintained within the main BerriAI/litellm repository [5].

Citations:


Date or omit the LiteLLM star count and soften the capability comparison.

“54K stars” is specific and can become stale, while “A3M Router adds three capabilities LiteLLM doesn’t have built-in” and “LiteLLM routes by model name or requires you to specify which model to call” overstate the gap as LiteLLM supports configurable router strategies, routing groups, fallbacks, retries, load balancing, and cost-based routing. Frame this as A3M’s particular model="auto" content-based heuristic routing and parallel-ensemble behavior instead of absolute missing capability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 60 - 74, Revise the README comparison to remove or
omit the “54K stars” count and replace absolute claims about LiteLLM’s missing
capabilities with qualified wording. In the heuristic-routing section, describe
A3M’s particular model="auto" content-based provider selection without asserting
LiteLLM only routes by model name, and frame parallel ensemble execution as
A3M’s behavior rather than a capability LiteLLM lacks.

Source: MCP tools

Comment thread README.md Outdated
Comment on lines +253 to +255
## Independent Benchmarks

A3M ranks **No. 1** among known public routing baselines on RouterArena (8,400 queries, 96.77% accuracy, $0.0768/1K cost, 1.0000 robustness). See [`docs/BENCHMARK.md`](docs/BENCHMARK.md) for full reproducible benchmarks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not label the separate benchmark as RouterArena accuracy.

The PR objectives state that RouterArena remains 0.5893, while 96.77% belongs to a separate benchmark. This text currently presents 96.77% as RouterArena accuracy and claims “No. 1,” which is misleading. Name the benchmark and metric explicitly, or reconcile the source results before publishing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 253 - 255, Update the “Independent Benchmarks”
section in README.md to clearly distinguish the 96.77% result and its metric
from RouterArena, whose accuracy must remain 0.5893. Remove or qualify the “No.
1” claim unless it is supported by the correctly identified benchmark, and link
to benchmark documentation consistent with the stated results.

GitHub About and npm now lead with biology-inspired routing:
EXP3 diversity, MVT rate-limit rotation, ODT verification.
@Das-rebel
Das-rebel merged commit 551d68a into main Jul 30, 2026
24 of 26 checks passed
@Das-rebel
Das-rebel deleted the feature/adaptive-diversity-weight branch July 30, 2026 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant