mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 11:52:26 +03:00
Um modelo grátis fora da tabela herdando $5/$15 por milhão e afundando no roteamento cost-aware é o defeito mais caro desta onda: silencioso, e inverte exatamente a decisão que o operador quer. Parar de chutar 1500ms de latência para modelo desconhecido e usar a mediana observada do pool — com contador de quantas vezes o chute dispara — é trocar heurística por medição do jeito certo. O contador é o que permite saber se valeu. Revalidei após reconstruir a branch sobre o tip: **33/33** nas suítes da PR, typecheck:core limpo, `check-api-typecheck` OK (289). **Duas integrações:** 1. `computeSnapshotWeights` conflitou com o #12794 (health via breaker + quality), já mergeado. Os dois compõem e ambos ficaram: o seu termo de `reliability` — que era a única chave que o caminho de snapshot ainda ignorava — mais o health observado e o quality do #12794. 2. `scripts/quality/run-all-gates.mjs` conflitou com o `check:provider-order-sync` do #12790. Aditivo, os dois gates coexistem. **Nota de dívida:** o `virtualFactory.ts` cruzou o teto de 1200 linhas pela primeira vez (1187 → 1207) somando esta onda. Congelei em vez de dividir e registrei os dois candidatos a extração na justificativa — `computeSnapshotWeights` (~85 linhas) e o grupo de elegibilidade de credencial (~70). Qualquer um dos dois volta o arquivo para baixo do cap.
58 lines
1.7 KiB
TypeScript
58 lines
1.7 KiB
TypeScript
// tests/unit/auto-combo-budget.test.ts
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { selectProvider } from "../../open-sse/services/autoCombo/engine.ts";
|
|
import { getSelfHealingManager } from "../../open-sse/services/autoCombo/selfHealing.ts";
|
|
import { DEFAULT_WEIGHTS } from "../../open-sse/services/autoCombo/scoring.ts";
|
|
|
|
const healer = getSelfHealingManager();
|
|
test.beforeEach(() => {
|
|
healer.exclusions.clear();
|
|
healer.incidentMode = false;
|
|
});
|
|
|
|
function config(over: Record<string, unknown> = {}) {
|
|
return {
|
|
id: "budget-probe",
|
|
name: "Budget Probe",
|
|
type: "auto",
|
|
candidatePool: [],
|
|
weights: DEFAULT_WEIGHTS,
|
|
explorationRate: 0,
|
|
...over,
|
|
} as never;
|
|
}
|
|
|
|
function cand(provider: string, model: string, cost: number) {
|
|
return {
|
|
provider,
|
|
model,
|
|
quotaRemaining: 95,
|
|
quotaTotal: 100,
|
|
circuitBreakerState: "CLOSED",
|
|
costPer1MTokens: cost,
|
|
p95LatencyMs: 120,
|
|
latencyStdDev: 8,
|
|
} as never;
|
|
}
|
|
|
|
test("default budget math is unchanged without the new field", () => {
|
|
const c = [cand("openai", "gpt-4o", 5)];
|
|
const r = selectProvider(config({ budgetCap: 1 }), c);
|
|
assert.equal(r.provider, "openai"); // (5/1M)*1000 = $0.005 < $1
|
|
});
|
|
|
|
test("32k estimated tokens enforce a 32x tighter budget", () => {
|
|
const c = [cand("openai", "gpt-4o", 5)];
|
|
// (5/1M)*32000 = $0.16 — exceeds a $0.01 cap only when tokens are honored.
|
|
// Message verified: "No candidate fits within the configured budget cap…" (engine.ts:59-62).
|
|
assert.throws(
|
|
() =>
|
|
selectProvider(
|
|
config({ budgetCap: 0.01, budgetFallback: "strict", estimatedInputTokens: 32000 }),
|
|
c
|
|
),
|
|
/budget cap/i
|
|
);
|
|
});
|