FreeLLMAPI / server /dist /services /scoring.js
Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
8.61 kB
// ── Bandit routing score ────────────────────────────────────────────────────
//
// A redesign of the analytics-driven router. Instead of summing a pile of
// hand-tuned, dimensionally-incompatible bonuses (a probability + a raw latency
// term + an intelligence term, each hand-capped to keep orderings sane), every
// signal here is normalized to [0, 1] and combined as a CONVEX COMBINATION:
//
// base = w_relΒ·reliability + w_speedΒ·speed + w_intelΒ·intelligence
// (weights are a preset that sums to 1, so base ∈ [0, 1])
//
// Two always-on GUARDRAILS then multiply the base β€” they never reorder good
// models against each other, they only pull a model down as it gets dangerous:
//
// effective = base Γ— headroomFactor Γ— rateLimitFactor
//
// headroomFactor β†’ protects a model that is nearly out of its free quota
// rateLimitFactor β†’ demotes a model that is currently throwing 429s
//
// Reliability is drawn from a Beta posterior (Thompson sampling) so exploration
// is automatic and proportional to uncertainty β€” a model is never permanently
// frozen out after a couple of failures. Speed and intelligence are
// deterministic. The result stays in a bounded, interpretable range and no term
// needs a manual cap to "still beat a 0%-success model".
export const BANDIT_PRESETS = {
// Reliability leads; speed and intelligence split the rest evenly.
balanced: { reliability: 0.5, speed: 0.25, intelligence: 0.25 },
// Intelligence leads, but reliability still carries real weight so a smart
// model that keeps failing doesn't win.
smartest: { reliability: 0.35, speed: 0.1, intelligence: 0.55 },
// Speed leads; reliability keeps a fast-but-broken model from winning.
fastest: { reliability: 0.35, speed: 0.55, intelligence: 0.1 },
// Reliability dominates β€” for clients that just want it to work.
reliable: { reliability: 0.7, speed: 0.15, intelligence: 0.15 },
};
// Analytics-driven routing is on by default ('balanced'). Operators who want the
// old hand-ordered chain can switch the strategy to 'priority' from the
// dashboard or PUT /api/fallback/routing.
export const DEFAULT_STRATEGY = 'balanced';
// ── Reliability ───────────────────────────────────────────────────────────
// Beta(1,1) prior = uniform: an unseen model is genuinely uncertain, not assumed
// good or bad. With decay-weighted pseudo-counts the alpha/beta are continuous.
export const PRIOR_SUCCESS = 1;
export const PRIOR_FAILURE = 1;
export function reliabilityPosterior(successes, failures) {
return {
alpha: Math.max(0, successes) + PRIOR_SUCCESS,
beta: Math.max(0, failures) + PRIOR_FAILURE,
};
}
// Deterministic expected reliability β€” used for the dashboard display score.
export function expectedReliability(successes, failures) {
const { alpha, beta } = reliabilityPosterior(successes, failures);
return alpha / (alpha + beta);
}
// ── Speed (throughput + TTFB blended into one [0,1] axis) ───────────────────
// Throughput uses a saturating curve so one very fast tiny model can't make a
// perfectly-fine larger model look "slow" (the global-max-normalization bug in
// the fork). TTFB is a simple linear ramp from "instant" to "painfully slow".
export const SPEED_SCALE_TOK_S = 60; // tok/s at which throughput β‰ˆ 0.63
export const TTFB_BEST_MS = 300; // ≀ this β†’ full latency credit
export const TTFB_WORST_MS = 5000; // β‰₯ this β†’ zero latency credit
const THROUGHPUT_WEIGHT = 0.6; // within the speed axis
const TTFB_WEIGHT = 0.4;
// Optimistic prior so unmeasured models still get explored on the speed axis.
export const SPEED_PRIOR = 0.6;
function throughputScore(tokPerSec) {
if (tokPerSec <= 0)
return 0;
return 1 - Math.exp(-tokPerSec / SPEED_SCALE_TOK_S);
}
function ttfbScore(ttfbMs) {
if (ttfbMs <= TTFB_BEST_MS)
return 1;
if (ttfbMs >= TTFB_WORST_MS)
return 0;
return 1 - (ttfbMs - TTFB_BEST_MS) / (TTFB_WORST_MS - TTFB_BEST_MS);
}
/**
* Blend throughput and TTFB into a single [0,1] speed score.
* `tokPerSec <= 0` means no successful samples β†’ return the exploration prior.
* `ttfbMs === null` means we have throughput but no first-byte timing β†’ fall
* back to throughput alone rather than guessing latency.
*/
export function speedScore(tokPerSec, ttfbMs) {
if (tokPerSec <= 0 && ttfbMs === null)
return SPEED_PRIOR;
const tp = throughputScore(tokPerSec);
if (ttfbMs === null)
return tp;
if (tokPerSec <= 0)
return ttfbScore(ttfbMs);
return THROUGHPUT_WEIGHT * tp + TTFB_WEIGHT * ttfbScore(ttfbMs);
}
// ── Intelligence ────────────────────────────────────────────────────────────
// Caller supplies a composite (tier-first, rank-as-tiebreaker β€” see router) and
// the min/max across the enabled chain. We min-max normalize to [0,1], 1 = best.
export function intelligenceScore(composite, min, max) {
if (max <= min)
return 1; // single model or all equal β†’ neutral-high
return (composite - min) / (max - min);
}
// ── Guardrail: free-quota headroom ──────────────────────────────────────────
// Multiplier that stays at 1 while a model has comfortable monthly headroom and
// ramps down to a floor as it approaches its free-tier cap, so we stop steering
// traffic at a model we're about to burn out. Unknown budget (0) β†’ no opinion.
export const HEADROOM_FLOOR = 0.1;
export const HEADROOM_RAMP_START = 0.2; // start protecting at 20% remaining
export function headroomFactor(usedTokens, budgetTokens) {
if (!budgetTokens || budgetTokens <= 0)
return 1; // unknown budget β†’ no opinion
const remaining = Math.max(0, 1 - usedTokens / budgetTokens);
if (remaining >= HEADROOM_RAMP_START)
return 1;
// Linear from (0 remaining β†’ floor) to (RAMP_START remaining β†’ 1).
return HEADROOM_FLOOR + (1 - HEADROOM_FLOOR) * (remaining / HEADROOM_RAMP_START);
}
// ── Guardrail: live rate-limit penalty ──────────────────────────────────────
// Maps the existing 0..MAX_PENALTY 429 penalty to a multiplier. At max penalty a
// model keeps 40% of its score β€” demoted hard but never fully excluded, so it
// can recover once the penalty decays.
export const MAX_PENALTY = 10;
export const RATE_LIMIT_MAX_DAMP = 0.6;
export function rateLimitFactor(penalty) {
const p = Math.min(Math.max(0, penalty), MAX_PENALTY);
return 1 - (p / MAX_PENALTY) * RATE_LIMIT_MAX_DAMP;
}
// ── Beta sampler (Marsaglia & Tsang via two Gamma draws) ────────────────────
function randomNormal() {
const u1 = Math.random() || Number.EPSILON;
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * Math.random());
}
function sampleGamma(shape) {
if (shape < 1)
return sampleGamma(shape + 1) * Math.pow(Math.random() || Number.EPSILON, 1 / shape);
const d = shape - 1 / 3;
const c = 1 / Math.sqrt(9 * d);
for (;;) {
let x, v;
do {
x = randomNormal();
v = 1 + c * x;
} while (v <= 0);
v = v ** 3;
const u = Math.random();
if (u < 1 - 0.0331 * x ** 4)
return d * v;
if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
return d * v;
}
}
export function sampleBeta(alpha, beta) {
const x = sampleGamma(alpha);
const y = sampleGamma(beta);
const sum = x + y;
return sum > 0 ? x / sum : 0.5;
}
/**
* Convex base (∈[0,1]) Γ— the two guardrail multipliers. The weights are assumed
* to sum to 1; if a caller passes a non-normalized vector we renormalize so the
* base never escapes [0,1].
*/
export function combineScore(inputs, weights) {
const wSum = weights.reliability + weights.speed + weights.intelligence || 1;
const base = (weights.reliability * inputs.reliability +
weights.speed * inputs.speed +
weights.intelligence * inputs.intelligence) / wSum;
return base * inputs.headroom * inputs.rateLimit;
}
//# sourceMappingURL=scoring.js.map