mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 03:42:21 +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.
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import type { TierAssignment, TierConfig, ProviderTier } from "./tierTypes";
|
|
import { PROVIDER_TIER } from "./tierTypes";
|
|
import { getModelPricing } from "./providerCostData";
|
|
import { isExplicitlyFree } from "./providerCostData";
|
|
import { mergeTierConfig, DEFAULT_TIER_CONFIG } from "./tierConfig";
|
|
|
|
let dbPersistenceChecked = false;
|
|
|
|
const tierCache = new Map<string, TierAssignment>();
|
|
let currentConfig: TierConfig = DEFAULT_TIER_CONFIG;
|
|
|
|
function cacheKey(provider: string, model: string): string {
|
|
return `${provider}::${model}`;
|
|
}
|
|
|
|
function matchGlob(pattern: string, text: string): boolean {
|
|
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
return new RegExp(`^${regexStr}$`, "i").test(text);
|
|
}
|
|
|
|
export function classifyTier(provider: string, model: string): TierAssignment {
|
|
const key = cacheKey(provider, model);
|
|
|
|
if (tierCache.has(key)) {
|
|
return tierCache.get(key)!;
|
|
}
|
|
|
|
if (isExplicitlyFree(provider, currentConfig)) {
|
|
const assignment: TierAssignment = {
|
|
provider,
|
|
model,
|
|
tier: PROVIDER_TIER.FREE,
|
|
reason: `Provider '${provider}' is in explicit free providers list`,
|
|
costPer1MInput: 0,
|
|
costPer1MOutput: 0,
|
|
hasFreeTier: true,
|
|
};
|
|
tierCache.set(key, assignment);
|
|
return assignment;
|
|
}
|
|
|
|
const providerOverride = currentConfig.providerOverrides.find(
|
|
(o) => o.provider.toLowerCase() === provider.toLowerCase()
|
|
);
|
|
if (providerOverride) {
|
|
const pricing = getModelPricing(provider, model);
|
|
const assignment: TierAssignment = {
|
|
provider,
|
|
model,
|
|
tier: providerOverride.tier,
|
|
reason: `Provider-level override: '${provider}' → ${providerOverride.tier}`,
|
|
costPer1MInput: pricing.inputCostPer1M,
|
|
costPer1MOutput: pricing.outputCostPer1M,
|
|
hasFreeTier: pricing.isFree,
|
|
freeQuotaLimit: pricing.freeQuotaLimit,
|
|
};
|
|
tierCache.set(key, assignment);
|
|
return assignment;
|
|
}
|
|
|
|
const modelOverride = currentConfig.modelOverrides.find(
|
|
(o) => o.provider.toLowerCase() === provider.toLowerCase() && matchGlob(o.modelPattern, model)
|
|
);
|
|
if (modelOverride) {
|
|
const pricing = getModelPricing(provider, model);
|
|
const assignment: TierAssignment = {
|
|
provider,
|
|
model,
|
|
tier: modelOverride.tier,
|
|
reason: `Model-level override: '${provider}/${model}' matches '${modelOverride.modelPattern}' → ${modelOverride.tier}`,
|
|
costPer1MInput: pricing.inputCostPer1M,
|
|
costPer1MOutput: pricing.outputCostPer1M,
|
|
hasFreeTier: pricing.isFree,
|
|
freeQuotaLimit: pricing.freeQuotaLimit,
|
|
};
|
|
tierCache.set(key, assignment);
|
|
return assignment;
|
|
}
|
|
|
|
const pricing = getModelPricing(provider, model);
|
|
let tier: ProviderTier;
|
|
let reason: string;
|
|
|
|
if (pricing.isFree || pricing.inputCostPer1M <= currentConfig.defaults.freeThreshold) {
|
|
tier = PROVIDER_TIER.FREE;
|
|
reason = `Cost-based: $${pricing.inputCostPer1M}/M input ≤ free threshold ($${currentConfig.defaults.freeThreshold}/M)`;
|
|
} else if (pricing.inputCostPer1M <= currentConfig.defaults.cheapThreshold) {
|
|
tier = PROVIDER_TIER.CHEAP;
|
|
reason = `Cost-based: $${pricing.inputCostPer1M}/M input ≤ cheap threshold ($${currentConfig.defaults.cheapThreshold}/M)`;
|
|
} else {
|
|
tier = PROVIDER_TIER.PREMIUM;
|
|
reason = `Cost-based: $${pricing.inputCostPer1M}/M input > cheap threshold ($${currentConfig.defaults.cheapThreshold}/M)`;
|
|
}
|
|
|
|
const assignment: TierAssignment = {
|
|
provider,
|
|
model,
|
|
tier,
|
|
reason,
|
|
costPer1MInput: pricing.inputCostPer1M,
|
|
costPer1MOutput: pricing.outputCostPer1M,
|
|
hasFreeTier: pricing.isFree,
|
|
freeQuotaLimit: pricing.freeQuotaLimit,
|
|
};
|
|
|
|
tierCache.set(key, assignment);
|
|
return assignment;
|
|
}
|
|
|
|
export function setTierConfig(config?: Partial<TierConfig> | null): void {
|
|
if (config === null || config === undefined) {
|
|
try {
|
|
const { loadTierConfig } = require("../../src/lib/db/tierConfig");
|
|
currentConfig = loadTierConfig();
|
|
} catch {
|
|
currentConfig = DEFAULT_TIER_CONFIG;
|
|
}
|
|
} else {
|
|
currentConfig = mergeTierConfig(config);
|
|
}
|
|
tierCache.clear();
|
|
}
|
|
|
|
export function getTierConfig(): TierConfig {
|
|
return { ...currentConfig };
|
|
}
|
|
|
|
export function clearTierCache(): void {
|
|
tierCache.clear();
|
|
}
|
|
|
|
export function classifyTiers(
|
|
targets: Array<{ provider: string; model: string }>
|
|
): TierAssignment[] {
|
|
return targets.map((t) => classifyTier(t.provider, t.model));
|
|
}
|
|
|
|
export function getTierStats(): Record<ProviderTier, number> {
|
|
const stats: Record<ProviderTier, number> = { free: 0, cheap: 0, premium: 0 };
|
|
for (const assignment of tierCache.values()) {
|
|
stats[assignment.tier]++;
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
export let tierAsyncFallbackTotal = 0; // exported for testability
|
|
|
|
export async function classifyTierAsync(provider: string, model: string): Promise<TierAssignment> {
|
|
try {
|
|
const { getPricingForModel } = await import("@/lib/db/settings");
|
|
const db = await getPricingForModel(provider, model);
|
|
const input = Number((db as { input?: unknown } | null)?.input);
|
|
const output = Number((db as { output?: unknown } | null)?.output);
|
|
if (Number.isFinite(input) && input >= 0) {
|
|
const out = Number.isFinite(output) && output >= 0 ? output : input;
|
|
// Same thresholds as the sync path below; a DB $0 lands FREE by threshold
|
|
// (the DB carries no isFree flag of its own).
|
|
let tier: ProviderTier;
|
|
if (input <= currentConfig.defaults.freeThreshold) tier = PROVIDER_TIER.FREE;
|
|
else if (input <= currentConfig.defaults.cheapThreshold) tier = PROVIDER_TIER.CHEAP;
|
|
else tier = PROVIDER_TIER.PREMIUM;
|
|
const sync = getModelPricing(provider, model); // keeps freeQuotaLimit when DB is mute
|
|
const assignment: TierAssignment = {
|
|
provider,
|
|
model,
|
|
tier,
|
|
reason: `DB cost-based: $${input}/M input`,
|
|
costPer1MInput: input,
|
|
costPer1MOutput: out,
|
|
hasFreeTier: input === 0,
|
|
freeQuotaLimit: sync.freeQuotaLimit,
|
|
};
|
|
tierCache.set(cacheKey(provider, model), assignment);
|
|
return assignment;
|
|
}
|
|
} catch {
|
|
// fall through to sync
|
|
}
|
|
tierAsyncFallbackTotal++;
|
|
return classifyTier(provider, model);
|
|
}
|