File size: 8,606 Bytes
077865a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// ── 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