mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-20 22:02:19 +03:00
feat(auto-combo): add weighted score router strategy (#12155)
Add a direct low-level mode for users who require explicit control over provider selection. score selects the highest configured weighted score directly while reusing the existing exploration rate. Exact ties preserve configured candidate order. rules and all other strategies remain unchanged.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **feat(routing):** add a `score` Auto router strategy that selects the highest configured weighted score and reuses `explorationRate`.
|
||||
@@ -414,6 +414,8 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
|
||||
`config.auto.routerStrategy`) to one of:
|
||||
|
||||
- `rules` — default weighted scoring
|
||||
- `score` — selects the highest configured weighted score. Exact ties preserve configured
|
||||
candidate order; the existing `explorationRate` samples from the full ranked pool.
|
||||
- `cost` / `eco` — cheapest healthy provider
|
||||
- `latency` / `fast` — lowest p95 latency with reliability penalty
|
||||
- `sla-aware` / `sla` — prefer candidates that satisfy p95 latency, error-rate, and optional
|
||||
@@ -422,7 +424,7 @@ Persisted `strategy: "auto"` combos can set `config.routerStrategy` (or legacy
|
||||
|
||||
### Router strategies in detail
|
||||
|
||||
The auto-combo engine exposes 5 pluggable **RouterStrategy** implementations that
|
||||
The auto-combo engine exposes 6 pluggable **RouterStrategy** implementations that
|
||||
you can swap via `config.routerStrategy` (or the legacy `config.auto.routerStrategy`).
|
||||
Each strategy picks one provider from the candidate pool, given a `RoutingContext`
|
||||
(task type, tool/vision hints, token estimate, optional SLA policy, optional
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* Inspired by ClawRouter commit 14c83c258 "refactor: extract routing into pluggable RouterStrategy system".
|
||||
* Provides a RouterStrategy interface and built-in implementations:
|
||||
* - RulesStrategy (default): wraps the existing 15-factor scoring engine
|
||||
* - ScoreStrategy: highest configured weighted score, with explicit exploration
|
||||
* - CostStrategy: always picks cheapest available model
|
||||
* - LatencyStrategy: prioritizes low p95 latency with reliability weighting
|
||||
* - SLAStrategy: prefers candidates that satisfy latency/error/cost SLOs
|
||||
* - LKGPStrategy: tries last known good provider first
|
||||
*/
|
||||
|
||||
import type { ProviderCandidate, ScoredProvider } from "./scoring.ts";
|
||||
import type { ProviderCandidate, ScoredProvider, ScoringWeights } from "./scoring.ts";
|
||||
import { scorePool } from "./scoring.ts";
|
||||
import { getTaskFitness } from "./taskFitness.ts";
|
||||
import { clamp01 } from "../../utils/number.ts";
|
||||
@@ -32,6 +33,8 @@ export interface RoutingContext {
|
||||
lastKnownGoodProvider?: string;
|
||||
lkgpEnabled?: boolean;
|
||||
sla?: SlaRoutingPolicy;
|
||||
weights?: ScoringWeights;
|
||||
explorationRate?: number;
|
||||
}
|
||||
|
||||
export interface RoutingDecision {
|
||||
@@ -108,6 +111,38 @@ class RulesStrategyImpl implements RouterStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
// ── ScoreStrategy: configured score wins, with explicit exploration ──────────
|
||||
|
||||
class ScoreStrategyImpl implements RouterStrategy {
|
||||
readonly name = "score";
|
||||
readonly description = "Selects the highest configured weighted score, with explicit exploration";
|
||||
|
||||
select(pool: ProviderCandidate[], context: RoutingContext): RoutingDecision {
|
||||
const eligible = pool.filter((candidate) => candidate.circuitBreakerState !== "OPEN");
|
||||
const ranked = scorePool(
|
||||
eligible.length > 0 ? eligible : pool,
|
||||
context.taskType,
|
||||
context.weights,
|
||||
getTaskFitness
|
||||
);
|
||||
if (ranked.length === 0) throw new Error("[ScoreStrategy] No candidates to score");
|
||||
|
||||
const explorationRate = Math.min(1, Math.max(0, context.explorationRate ?? 0));
|
||||
const isExploration = Math.random() < explorationRate && ranked.length > 1;
|
||||
const selected = isExploration ? ranked[Math.floor(Math.random() * ranked.length)] : ranked[0];
|
||||
|
||||
return {
|
||||
provider: selected.provider,
|
||||
model: selected.model,
|
||||
strategy: this.name,
|
||||
reason: `ScoreStrategy: score=${selected.score.toFixed(3)}${isExploration ? " (exploration)" : ""}`,
|
||||
candidatesConsidered: ranked.length,
|
||||
finalScore: selected.score,
|
||||
connectionId: selected.connectionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── CostStrategy: always picks cheapest healthy provider ─────────────────────
|
||||
|
||||
class CostStrategyImpl implements RouterStrategy {
|
||||
@@ -337,12 +372,14 @@ class LKGPStrategyImpl implements RouterStrategy {
|
||||
const strategyRegistry = new Map<string, RouterStrategy>();
|
||||
|
||||
const rulesStrategy = new RulesStrategyImpl();
|
||||
const scoreStrategy = new ScoreStrategyImpl();
|
||||
const costStrategy = new CostStrategyImpl();
|
||||
const latencyStrategy = new LatencyStrategyImpl();
|
||||
const slaStrategy = new SLAStrategyImpl();
|
||||
const lkgpStrategy = new LKGPStrategyImpl();
|
||||
|
||||
strategyRegistry.set("rules", rulesStrategy);
|
||||
strategyRegistry.set("score", scoreStrategy);
|
||||
strategyRegistry.set("cost", costStrategy);
|
||||
strategyRegistry.set("eco", costStrategy); // alias
|
||||
strategyRegistry.set("latency", latencyStrategy);
|
||||
|
||||
@@ -377,6 +377,8 @@ export async function resolveAutoStrategyOrder(
|
||||
boolean | undefined,
|
||||
estimatedInputTokens,
|
||||
sla: slaPolicy,
|
||||
weights,
|
||||
explorationRate,
|
||||
},
|
||||
routingStrategy
|
||||
);
|
||||
|
||||
@@ -67,6 +67,7 @@ export const MODE_PACK_OPTIONS = [
|
||||
|
||||
export const ROUTER_STRATEGY_OPTIONS = [
|
||||
{ id: "rules", label: "Rules (6-Factor Scoring)" },
|
||||
{ id: "score", label: "Highest Weighted Score" },
|
||||
{ id: "cost", label: "Cost Optimized" },
|
||||
{ id: "latency", label: "Latency Optimized" },
|
||||
{ id: "sla-aware", label: "SLA-aware" },
|
||||
|
||||
@@ -37,6 +37,7 @@ export type AnyRoutingStrategyValue = RoutingStrategyValue | InternalRoutingStra
|
||||
|
||||
export const AUTO_ROUTING_STRATEGY_VALUES = [
|
||||
"rules",
|
||||
"score",
|
||||
"cost",
|
||||
"eco",
|
||||
"latency",
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
listStrategies,
|
||||
type RoutingContext,
|
||||
} from "../../open-sse/services/autoCombo/routerStrategy.ts";
|
||||
import type { ProviderCandidate } from "../../open-sse/services/autoCombo/scoring.ts";
|
||||
import {
|
||||
DEFAULT_WEIGHTS,
|
||||
type ProviderCandidate,
|
||||
} from "../../open-sse/services/autoCombo/scoring.ts";
|
||||
|
||||
function cand(p: Partial<ProviderCandidate> & { provider: string }): ProviderCandidate {
|
||||
return {
|
||||
@@ -33,6 +36,32 @@ function cand(p: Partial<ProviderCandidate> & { provider: string }): ProviderCan
|
||||
|
||||
const ctx: RoutingContext = { taskType: "default" };
|
||||
|
||||
// ── score ────────────────────────────────────────────────────────────────────
|
||||
test("score — exploits the configured winner and uses explorationRate", (t) => {
|
||||
const pool = [
|
||||
cand({ provider: "cheap", costPer1MTokens: 1 }),
|
||||
cand({ provider: "expensive", costPer1MTokens: 9 }),
|
||||
];
|
||||
const weights = { ...DEFAULT_WEIGHTS, costInv: 1, quota: 0, health: 0, latencyInv: 0 };
|
||||
|
||||
t.mock.method(Math, "random", () => 0.99);
|
||||
|
||||
assert.equal(
|
||||
getStrategy("score").select(pool, { ...ctx, weights, explorationRate: 0 }).provider,
|
||||
"cheap"
|
||||
);
|
||||
assert.equal(
|
||||
getStrategy("score").select(pool, { ...ctx, weights, explorationRate: 1 }).provider,
|
||||
"expensive"
|
||||
);
|
||||
});
|
||||
|
||||
test("score — exact ties preserve configured candidate order", () => {
|
||||
const pool = [cand({ provider: "first" }), cand({ provider: "second" })];
|
||||
|
||||
assert.equal(getStrategy("score").select(pool, { ...ctx, explorationRate: 0 }).provider, "first");
|
||||
});
|
||||
|
||||
// ── cost ─────────────────────────────────────────────────────────────────────
|
||||
test("cost — selects the cheapest healthy candidate", () => {
|
||||
const pool = [
|
||||
@@ -307,6 +336,7 @@ test("selectWithStrategy — unknown strategy silently falls back to rules", ()
|
||||
|
||||
test("listStrategies — exposes every registered strategy + aliases", () => {
|
||||
const names = listStrategies().map((s) => s.name);
|
||||
assert.ok(names.includes("score"), "listStrategies missing 'score'");
|
||||
for (const n of ["rules", "cost", "eco", "latency", "fast", "sla-aware", "sla", "lkgp"]) {
|
||||
assert.ok(names.includes(n), `listStrategies missing '${n}'`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user