Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/gratis/src/perf.ts

Raw
/**
 * Passive per-model and per-backend performance, measured only from real hops.
 * Quality order stays primary: measurements shorten deadlines, demote chronic
 * offenders, and promote proven backends one place; speed never promotes.
 */

/** Weight of the newest sample in each moving average. */
const ALPHA = 0.3;
/** Failures lose half their weight over this span, so demotion wears off. */
const FAILURE_HALF_LIFE_MS = 30 * 60_000;
/** User votes (manual cools) are judgements, so they fade over days. */
const VOTE_HALF_LIFE_MS = 7 * 24 * 60 * 60_000;
/** One vote does not demote; two recent votes do. */
const VOTE_WEIGHT = 0.35;
/** Below this many samples a model keeps its static rank and default deadline. */
const MIN_SAMPLES = 3;
/** Score above which a model or backend sorts behind its peers. */
const CHRONIC_FAILURE = 0.5;
/** A backend this reliable over this many requests may move up one place. */
const PROMOTE_SAMPLES = 20;
const PROMOTE_MAX_FAILURE = 0.1;
/** ...but only past a neighbor at least this unreliable. */
const UNRELIABLE_FAILURE = 0.3;
/** Persisted state keeps at most this many models, most recent first. */
const MAX_STORED = 500;
export const MIN_DEADLINE_MS = 3_000;
const DEADLINE_FACTOR = 3;

export type Sample =
	| { kind: "ok"; firstTokenMs: number; tokensPerSecond?: number }
	| { kind: "fail" };

interface Stat {
	failure: number;
	samples: number;
	timed: number;
	firstTokenMs?: number;
	tokensPerSecond?: number;
	at: number;
	votes?: number;
	votedAt?: number;
}

export interface ModelPerf {
	id: string;
	samples: number;
	failureRate: number;
	firstTokenMs?: number;
	tokensPerSecond?: number;
	demoted: boolean;
}

/** JSON shape of the routing state file. */
export interface PerfState {
	version: 1;
	models: Record<string, Stat>;
	backends: Record<string, Stat>;
}

const average = (previous: number | undefined, next: number) =>
	previous === undefined ? next : previous + ALPHA * (next - previous);

const finite = (value: unknown): value is number =>
	typeof value === "number" && Number.isFinite(value);

/** Accept only well-formed stats from disk; anything else is dropped. */
function validStat(value: unknown): Stat | undefined {
	const stat = value as Partial<Stat> | undefined;
	if (
		!stat ||
		!finite(stat.failure) ||
		!finite(stat.samples) ||
		!finite(stat.timed) ||
		!finite(stat.at)
	)
		return undefined;
	return {
		failure: Math.min(1, Math.max(0, stat.failure)),
		samples: Math.max(0, Math.floor(stat.samples)),
		timed: Math.max(0, Math.floor(stat.timed)),
		at: stat.at,
		...(finite(stat.firstTokenMs) ? { firstTokenMs: stat.firstTokenMs } : {}),
		...(finite(stat.tokensPerSecond)
			? { tokensPerSecond: stat.tokensPerSecond }
			: {}),
		...(finite(stat.votes) && finite(stat.votedAt)
			? { votes: Math.max(0, stat.votes), votedAt: stat.votedAt }
			: {}),
	};
}

export function createPerf(now: () => number = Date.now) {
	const models = new Map<string, Stat>();
	const backends = new Map<string, Stat>();
	let onChange: (() => void) | undefined;

	const failureOf = (stat: Stat) =>
		stat.failure * 0.5 ** ((now() - stat.at) / FAILURE_HALF_LIFE_MS);
	const votesOf = (stat: Stat) =>
		stat.votes && stat.votedAt !== undefined
			? stat.votes * 0.5 ** ((now() - stat.votedAt) / VOTE_HALF_LIFE_MS)
			: 0;

	/** Measured failures count once there is enough evidence; votes always do. */
	const score = (stat: Stat | undefined) =>
		stat === undefined
			? 0
			: (stat.samples >= MIN_SAMPLES ? failureOf(stat) : 0) + votesOf(stat);
	const chronic = (stat: Stat | undefined) => score(stat) > CHRONIC_FAILURE;

	const fresh = (): Stat => ({ failure: 0, samples: 0, timed: 0, at: now() });

	function update(map: Map<string, Stat>, key: string, sample: Sample): void {
		const stat = map.get(key) ?? fresh();
		const failure = failureOf(stat);
		stat.failure =
			failure + ALPHA * ((sample.kind === "fail" ? 1 : 0) - failure);
		stat.samples++;
		stat.at = now();
		if (sample.kind === "ok") {
			stat.timed++;
			stat.firstTokenMs = average(stat.firstTokenMs, sample.firstTokenMs);
			if (sample.tokensPerSecond !== undefined)
				stat.tokensPerSecond = average(
					stat.tokensPerSecond,
					sample.tokensPerSecond,
				);
		}
		map.set(key, stat);
	}

	/** Stable: healthy entries keep their static order ahead of chronic ones. */
	const demote = <T>(
		items: readonly T[],
		stat: (item: T) => Stat | undefined,
	) =>
		[...items].sort(
			(a, b) => Number(chronic(stat(a))) - Number(chronic(stat(b))),
		);

	const proven = (stat: Stat | undefined) =>
		stat !== undefined &&
		stat.samples >= PROMOTE_SAMPLES &&
		score(stat) < PROMOTE_MAX_FAILURE;
	const unreliable = (stat: Stat | undefined) =>
		stat !== undefined &&
		stat.samples >= MIN_SAMPLES &&
		score(stat) >= UNRELIABLE_FAILURE;

	return {
		record(backend: string, modelId: string, sample: Sample): void {
			update(models, modelId, sample);
			update(backends, backend, sample);
			onChange?.();
		},
		/** A user's manual cool: a quality judgement against the model. */
		vote(modelId: string): void {
			const stat = models.get(modelId) ?? fresh();
			stat.votes = votesOf(stat) + VOTE_WEIGHT;
			stat.votedAt = now();
			models.set(modelId, stat);
			onChange?.();
		},
		/** Three times the model's typical first token, within 3 s and `max`. */
		deadline(modelId: string, max: number): number {
			const stat = models.get(modelId);
			if (!stat?.firstTokenMs || stat.timed < MIN_SAMPLES) return max;
			return Math.min(
				max,
				Math.max(
					Math.min(MIN_DEADLINE_MS, max),
					DEADLINE_FACTOR * stat.firstTokenMs,
				),
			);
		},
		orderModels<T extends { id: string }>(candidates: readonly T[]): T[] {
			return demote(candidates, (model) => models.get(model.id));
		},
		/**
		 * Chronic backends go last; then one pass lets a proven backend overtake
		 * an unreliable neighbor, so each backend moves up at most one place.
		 */
		orderBackends<T extends string>(order: readonly T[]): T[] {
			const ordered = demote(order, (backend) => backends.get(backend));
			for (let index = 1; index < ordered.length; index++) {
				const ahead = ordered[index - 1] as T;
				const current = ordered[index] as T;
				if (
					proven(backends.get(current)) &&
					unreliable(backends.get(ahead)) &&
					!chronic(backends.get(ahead))
				) {
					ordered[index - 1] = current;
					ordered[index] = ahead;
					index++;
				}
			}
			return ordered;
		},
		/** Measured models, most used first. */
		snapshot(): ModelPerf[] {
			return [...models]
				.filter(([, stat]) => stat.samples > 0)
				.map(([id, stat]) => ({
					id,
					samples: stat.samples,
					failureRate: failureOf(stat),
					...(stat.firstTokenMs === undefined
						? {}
						: { firstTokenMs: stat.firstTokenMs }),
					...(stat.tokensPerSecond === undefined
						? {}
						: { tokensPerSecond: stat.tokensPerSecond }),
					demoted: chronic(stat),
				}))
				.sort((a, b) => b.samples - a.samples);
		},
		/** Serializable state, newest models first, bounded. */
		export(): PerfState {
			const newest = (a: [string, Stat], b: [string, Stat]) =>
				Math.max(b[1].at, b[1].votedAt ?? 0) -
				Math.max(a[1].at, a[1].votedAt ?? 0);
			return {
				version: 1,
				models: Object.fromEntries(
					[...models].sort(newest).slice(0, MAX_STORED),
				),
				backends: Object.fromEntries(backends),
			};
		},
		/** Merge persisted state; entries already measured this session win. */
		import(state: unknown): void {
			const parsed = state as Partial<PerfState> | undefined;
			if (parsed?.version !== 1) return;
			for (const [target, source] of [
				[models, parsed.models],
				[backends, parsed.backends],
			] as const) {
				if (!source || typeof source !== "object") continue;
				for (const [key, value] of Object.entries(source)) {
					const stat = validStat(value);
					if (stat && !target.has(key)) target.set(key, stat);
				}
			}
		},
		/** Called after every change, e.g. to schedule a save. */
		onChange(listener: () => void): void {
			onChange = listener;
		},
	};
}