-
Notifications
You must be signed in to change notification settings - Fork 5
feat(routing): Biology-inspired routing improvements (EXP3 + MVT + ODT) #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
0a5c2b4
9a01bec
28ebd9c
760b468
6520ecc
a494dba
3786985
b6bcb48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,86 @@ import { estimateCost } from "../utils/tokenUtils"; | |
| import { logScaleCostScore } from "../utils/costUtils"; | ||
| import { quickselectTopK, selectTop } from "../utils/sorting"; | ||
|
|
||
| // ============================================================ | ||
| // TRAFFIC SHARES FOR ADAPTIVE DIVERSITY WEIGHT (EXP3-inspired) | ||
| // Based on negative frequency-dependent selection — as provider i's traffic | ||
| // share f_i grows above uniform (1/n), impose a diversity penalty. | ||
| // Formula: gamma = sqrt(n * log(n) / (T * G^2)) [Auer et al. 2002, EXP3] | ||
| // diversityPenalty_i = gamma * (f_i - 1/n) | ||
| // ============================================================ | ||
|
|
||
| /** Number of times each provider has been selected (cumulative) */ | ||
| const _selectionCount: Record<string, number> = {}; | ||
|
|
||
| /** Total routing decisions since last reset */ | ||
| let _totalDecisions = 0; | ||
|
|
||
| /** Reward range estimate for gamma computation (quality_score scale 0-1) */ | ||
| const _REWARD_RANGE = 1.0; | ||
|
|
||
| /** | ||
| * Compute the EXP3-inspired diversity penalty for a provider. | ||
| * Based on: Auer et al. 2002 — "The Nonstochastic Multiarmed Bandit Problem" | ||
| * and the insight that as provider share f_i grows above uniform (1/n), | ||
| * imposing a penalty proportional to (f_i - 1/n) prevents monoculture | ||
| * (competitive exclusion — negative frequency-dependent selection). | ||
| * | ||
| * @param providerName - provider key | ||
| * @param nProviders - total number of available providers | ||
| * @param recentDecayFactor - optional decay for non-stationary environments | ||
| */ | ||
| 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; | ||
| } | ||
|
|
||
| /** | ||
| * Record that a provider was selected (call after each routing decision). | ||
| * Used to compute the diversity penalty on subsequent decisions. | ||
| */ | ||
| function recordSelection(providerName: string): void { | ||
| _selectionCount[providerName] = (_selectionCount[providerName] || 0) + 1; | ||
| _totalDecisions += 1; | ||
| } | ||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
|
|
||
|
Comment on lines
+91
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' . || trueRepository: Das-rebel/a3m-router Length of output: 10980 Export
🤖 Prompt for AI Agents |
||
| // ============================================================ | ||
| // CACHE FOR MODEL PROFILES (avoids O(n*m) rebuild on every routeQuery) | ||
| // ============================================================ | ||
|
|
@@ -767,6 +847,24 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m | |
|
|
||
| let topCandidates = quickselectTopK(candidates, 4, scoreFn); | ||
|
|
||
| // === DIVERSITY PENALTY (EXP3-inspired, negative frequency-dependence) === | ||
| // As provider share f_i grows above uniform (1/n), impose a diversity penalty. | ||
| // This prevents provider monoculture (competitive exclusion) — the core insight | ||
| // from the Paradox of the Plankton / negative frequency-dependent selection. | ||
| // Penalty is applied only to the quality dimension (complexity_bias-weighted), | ||
| // since cost already naturally distributes across providers. | ||
| // See: Auer et al. 2002, "The Nonstochastic Multiarmed Bandit Problem" | ||
| const nProviders = candidates.length; | ||
| for (const c of candidates) { | ||
| const divPenalty = computeDiversityPenalty(c.name, nProviders); | ||
| // Diversity penalty applies to quality dimension only (cost already disperses traffic) | ||
| c.total_score -= divPenalty * complexity_bias; | ||
| } | ||
|
|
||
| // Re-rank after diversity adjustment | ||
| candidates.sort((a, b) => b.total_score - a.total_score); | ||
| topCandidates = candidates.slice(0, 4); | ||
|
|
||
| // Adaptive quality floor: for complex queries, prefer models above the floor | ||
| if (adaptiveQualityFloor > 0) { | ||
| const qualified = topCandidates.filter(c => c.quality_score >= adaptiveQualityFloor); | ||
|
|
@@ -779,6 +877,9 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m | |
| const primary = topCandidates[0]; | ||
| const secondary = topCandidates.slice(1, 3); | ||
|
|
||
| // Record selection for diversity tracking (after final decision, before return) | ||
| recordSelection(primary.name); | ||
|
|
||
| // Calculate confidence based on score gap | ||
| let confidence = 0.5; | ||
| if (candidates.length > 1) { | ||
|
|
||
There was a problem hiding this comment.
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
shareis 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 computesdeviation = share - uniformwhereuniform = 1/nProvidersis a fraction (e.g. 0.5). The docstring and formula (lines 21-24, 39-41) both describe this as the provider's traffic sharef_i, which should be_selectionCount[providerName] / _totalDecisions. As written,deviationgrows unbounded with cumulative usage count instead of reflecting actual over-representation, so after just a couple of selections the "penalty" already dwarfs typicalquality_score/cost_scorevalues and never meaningfully reflects current traffic distribution.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents