Ir al contenido
OmniRoute source

Quota Sharing Engine (Español)

El motor de reparto de cuotas distribuye equitativamente la cuota temporal de un proveedor (p. ej., la ventana de 5 horas de Codex, 1500 solicitudes/h de Kimi) entre varias claves de API que comparten la misma conexión.

Problema que resuelve: OmniRoute canaliza muchas claves de API a través de la misma cuenta del proveedor ascendente. Sin una lógica de reparto, una ráfaga de la clave A puede agotar la cuota del proveedor para esa hora, dejando bloqueadas las claves B y C hasta que se reinicie la ventana. El motor lo evita mediante:

  1. El seguimiento del consumo acumulado de cada clave por dimensión (%, solicitudes, tokens, $).
  2. La aplicación de un algoritmo equitativo con conservación del trabajo: una clave puede tomar prestadas las cuotas inactivas mientras el conjunto global no esté saturado.
  3. La aplicación del resultado en la ruta crítica (chatCore.ts) antes de que la solicitud llegue al ejecutor ascendente.

Algoritmo: reparto equitativo con conservación del trabajo

Sección titulada «Algoritmo: reparto equitativo con conservación del trabajo»

Implementado en src/lib/quota/fairShare.ts.

Condición Modo Comportamiento
globalUsedPercent < saturationThreshold Generoso La clave puede tomar prestado hasta el límite global menos el total consumido
globalUsedPercent >= saturationThreshold Estricto Aplica estrictamente la cuota equitativa individual

Valor predeterminado: saturationThreshold = 0.5 (variable de entorno QUOTA_SATURATION_THRESHOLD).

Para cada dimensión activa del conjunto, el motor calcula:

fairShareAllowed = poolLimit × (allocationWeight / 100)
consumed = valor acumulado actual para esta clave (de QuotaStore.peek)
remaining = fairShareAllowed - consumed

Después:

  • policy = hard: si consumed > fairShareAllowed y el modo es estricto → bloquear.
  • policy = soft: si consumed > fairShareAllowed y el modo es estricto → penalizar (reducir la prioridad en la combinación; nunca bloquear de forma estricta).
  • policy = burst: permitir mientras exista margen global, independientemente de la cuota equitativa.

capValue + capUnit en una asignación constituyen un límite estricto independiente del modo o de la política. Cualquier dimensión en la que consumed >= capValue siempre bloquea la solicitud.

Una solicitud se bloquea si cualquier dimensión del conjunto la bloquearía. Las dimensiones son independientes: el agotamiento del porcentaje de 5 horas no afecta a la dimensión del porcentaje semanal.

En modo generoso, una clave cuya asignación esté infrautilizada puede usar el excedente de las cuotas no utilizadas de otras claves. La fórmula es:

maxAllowed = globalLimit - consumedByOtherKeys

donde consumedByOtherKeys = consumedTotal - consumedByThisKey. El límite global (limit del conjunto para esa dimensión) siempre constituye el límite estricto.


Implementado en src/lib/quota/sqliteQuotaStore.ts y redisQuotaStore.ts.

Dos bloques por (apiKeyId, dimensionKey):

  • curr: bloque actual (floor(nowMs / windowMs))
  • prev: bloque anterior (curr - 1)

Valor acumulado efectivo:

effectiveBucketIndex = floor(nowMs / windowMs)
bucketStartMs = effectiveBucketIndex × windowMs
elapsed = nowMs - bucketStartMs
weight = 1 - elapsed / windowMs
effective = prev × weight + curr

Precisión: ~99 %. El error es, como máximo, del 1 % del tamaño de la ventana en el límite entre bloques (inherente a la aproximación de 2 bloques).

Controlador de SQLite: un mutex en memoria por clave (apiKeyId | dimensionKey) evita la condición de carrera de lectura-modificación-escritura. El patrón replica el mecanismo contra avalanchas de solicitudes de src/sse/services/auth.ts.

Controlador de Redis: script Lua EVAL para realizar incrementos atómicos; se ejecuta como un único comando de Redis.


  • Tabla: quota_consumption (consulte la migración 073_quota_pools.sql / 074_quota_consumption.sql).
  • Ideal para despliegues de una sola instancia.
  • Toda la persistencia se encuentra en la base de datos SQLite existente de OmniRoute (DATA_DIR/storage.sqlite).
  • Requiere el paquete npm ioredis.
  • Los contadores se almacenan en Redis; los metadatos (grupos/asignaciones) permanecen en SQLite.
  • Ideal para despliegues con múltiples réplicas donde los contadores deben compartirse.

Mediante la interfaz de configuración (/dashboard/settings → Almacén de cuotas), o mediante variables de entorno:

Ventana de terminal
QUOTA_STORE_DRIVER=redis
QUOTA_STORE_REDIS_URL=redis://localhost:6379

La configuración de la base de datos tiene prioridad sobre las variables de entorno. Si driver=redis, pero falta la URL o ioredis no está instalado, la factoría recurre a SQLite y registra una advertencia.

Orden de selección del controlador:

  1. Configuración de la base de datos quotaStore.driver
  2. Variable de entorno QUOTA_STORE_DRIVER
  3. Predeterminado: sqlite

Un grupo puede tener varias dimensiones. Cada dimensión es independiente:

QuotaDimension {
unit: "percent" | "requests" | "tokens" | "usd",
window: "5h" | "hourly" | "daily" | "weekly" | "monthly",
limit: number, // límite máximo global del grupo para esta dimensión
}

Ejemplo: plan Codex (5h% + semanal%):

[
{ "unit": "percent", "window": "5h", "limit": 100 },
{ "unit": "percent", "window": "weekly", "limit": 100 }
]

Una solicitud debe satisfacer todas las dimensiones para ser permitida.


Implementado en src/lib/quota/planResolver.ts.

Precedencia (de mayor a menor):

  1. Anulación manual en la base de datos — tabla provider_plans, por connectionId.
  2. Catálogo conocido — src/lib/quota/planRegistry.ts (solo datos).
  3. Plan vacío — sin dimensiones; se requiere configuración manual.
Proveedor Dimensiones
codex percent/5h/100, percent/weekly/100
glm tokens/5h (limit=0, desconocido), tokens/weekly
minimax tokens/5h, tokens/weekly
bailian percent/5h/100, percent/weekly/100, percent/monthly/100
kimi requests/hourly/1500
alibaba requests/monthly/90000
openai, anthropic Sin valor predeterminado — se requiere configuración manual

Se ejecuta antes del ejecutor del servicio de origen, después de las comprobaciones de autenticación y políticas:

resolveComboTargets / handleSingleModel
→ enforceQuotaShare(apiKeyId, connectionId, provider, estimatedCost)
→ getQuotaStore().peek() por dimensión
→ fairShare.decideFairShare()
→ si se bloquea → devolver 429 (buildErrorBody, regla estricta n.º 12)
→ si se permite + se reduce la prioridad → establecer quotaSoftPenalty=true en el candidato
→ executor.execute()

Apertura ante fallos: si enforceQuotaShare genera una excepción, la solicitud se permite con un registro pino.warn. Esto evita que un error del motor de cuotas bloquee todo el tráfico.

Después de una respuesta correcta:

el ejecutor devuelve un resultado correcto
→ spendRecorder.recordConsumption(apiKeyId, connectionId, provider, actualCost)
→ getQuotaStore().consume() por dimensión
→ apertura ante fallos: los errores se registran como pino.warn y nunca se propagan al cliente

Nota sobre desviaciones: si consume falla después de la respuesta, el contador móvil registra un consumo inferior al real. La señal de saturación del proveedor (p. ej., anthropic-ratelimit-unified-5h-utilization) corrige la estimación global en la siguiente solicitud.

Penalización leve de combinaciones (open-sse/services/combo.ts)

Sección titulada «Penalización leve de combinaciones (open-sse/services/combo.ts)»

Cuando decision.deprioritize === true:

if (candidate.quotaSoftPenalty) {
score *= QUOTA_SOFT_DEPRIORITIZE_FACTOR; // valor predeterminado: 0.7
}

La penalización se aplica después de todos los demás factores de puntuación. Reduce la probabilidad de que la combinación automática seleccione una clave saturada sin bloquearla por completo.


/dashboard/costs/quota-share — Página principal de grupos

Sección titulada «/dashboard/costs/quota-share — Página principal de grupos»

Componentes (todos en src/app/(dashboard)/dashboard/costs/quota-share/):

Componente Propósito
QuotaConceptCard Tarjeta introductoria que explica el uso compartido de cuotas a nuevos usuarios
CreatePoolModal Crea un nuevo grupo de cuotas (conexión + nombre + asignaciones iniciales)
PoolCard Resumen por grupo: nombre, conexión, número de asignaciones
DimensionBar Barra apilada por dimensión: proporción de cada clave + uso global
AllocationTable Tabla con consumo, proporción justa, déficit/superávit e indicador de préstamo
BurnRateChart Gráfico de líneas de la tasa de consumo EMA (Recharts con carga diferida mediante dynamic())
EditAllocationsModal Edita los pesos, límites y políticas de asignación de un grupo

Hooks de la página:

  • usePools — obtiene GET /api/quota/pools cada 30 s.
  • usePoolUsage — obtiene GET /api/quota/pools/[id]/usage bajo demanda.
  • useLocalStoragePoolMigration — se ejecuta una vez al montar para migrar datos heredados de LS.

/dashboard/costs/quota-share/plans — Configuración del plan del proveedor

Sección titulada «/dashboard/costs/quota-share/plans — Configuración del plan del proveedor»
  • ProviderPlanConfigClient.tsx: menú desplegable para seleccionar un proveedor, ver el plan resuelto (automático desde el catálogo o mediante anulación manual) y editar dimensiones.
  • Los cambios se escriben en PUT /api/quota/plans/[connectionId].
  • La eliminación revierte al plan del catálogo o a un plan vacío.

Variable Valor predeterminado Descripción
QUOTA_STORE_DRIVER sqlite Controlador que se utilizará: sqlite o redis
QUOTA_STORE_REDIS_URL (vacío) URL de Redis, p. ej., redis://localhost:6379
QUOTA_SATURATION_THRESHOLD 0.5 0..1; >= threshold activa el modo estricto
QUOTA_SOFT_DEPRIORITIZE_FACTOR 0.7 0..1; multiplicador de la puntuación combinada de política flexible
QUOTA_CONSUMPTION_RETENTION_DAYS 14 Días antes de que GC elimine los bloques antiguos de quota_consumption

La configuración de la base de datos (quotaStore.*) prevalece sobre las variables de entorno.


Compruebe que ioredis esté instalado (npm ls ioredis) y que se pueda acceder a QUOTA_STORE_REDIS_URL. Si falla la conexión, la fábrica recurre a SQLite (se registra con el nivel warn).

peek devuelve datos obsoletos / apertura en caso de fallo

Sección titulada «peek devuelve datos obsoletos / apertura en caso de fallo»

Si peek genera una excepción, enforceQuotaShare interpreta el resultado como “permitir” (apertura en caso de fallo). Revise los registros de pino para buscar entradas de quota:enforce y quota:factory e identificar la causa raíz.

Si el uso real del proveedor difiere de los contadores, es algo esperado: la ventana deslizante de 2 bloques tiene un error de aproximadamente el 1 % en los límites de la ventana, y consume se ejecuta tras la respuesta sin esperar a que termine. La señal de saturación (saturationSignals.ts) lee la utilización real del proveedor con un TTL de 30 s y ajusta globalUsedPercent en consecuencia.

El grupo muestra “sin datos” para la tasa de consumo

Sección titulada «El grupo muestra “sin datos” para la tasa de consumo»

computeBurnRate requiere al menos 2 muestras históricas. Los grupos nuevos sin llamadas previas a consume mostrarán tokensPerSecond: 0 y timeToExhaustionMs: null.


Cuando /dashboard/costs/quota-share se carga por primera vez, el hook useLocalStoragePoolMigration comprueba:

  1. Que localStorage.getItem("omniroute:quota-share:pools") no esté vacío.
  2. Que GET /api/quota/pools devuelva [] (la BD está vacía).

Si ambas condiciones se cumplen, envía cada pool heredado a POST /api/quota/pools por lotes y, a continuación, elimina la clave de localStorage. La migración es idempotente: la condición 2 evita que se vuelva a realizar la migración.


quota-share es una estrategia de enrutamiento exclusivamente interna (INTERNAL_ROUTING_STRATEGY_VALUES en src/shared/constants/routingStrategies.ts). Se utiliza exclusivamente con las combinaciones de pools qtSd/ generadas por el sistema y se excluye deliberadamente de ROUTING_STRATEGY_VALUES para que nunca aparezca como una opción seleccionable por el usuario en la interfaz de usuario o la API.


El motor quota-share incluye dos niveles de cobertura automatizada:

Suite Comando Qué cubre
Unitarias (29 pruebas) node --import tsx/esm --test tests/unit/quota-share-strategy.test.ts Planificador DRR, control de saturación, límites de concurrencia, cálculo de fairShare, gestión de colas de trabajo pendiente
Matriz de integración npm run test:combo:matrix Decisión de enrutamiento integral a través del pipeline real de combinaciones; equidad DRR + pérdida de prioridad por saturación mediante puntos de integración reales (registerQuotaFetcher, setLKGP, __setHeadroomSaturationFetcherForTests)

La matriz de integración se ejecuta en CI junto con las 19 estrategias públicas. La suite de pruebas unitarias puede ejecutarse de forma independiente.


Tres tablas añadidas por las migraciones 078, 079 y 085:

  • quota_pools + quota_allocations: definiciones de pools y asignaciones por clave.
  • quota_consumption: contadores móviles de 2 buckets por (apiKeyId, dimensionKey).
  • provider_plans: anulaciones manuales de planes de proveedores (dimensiones JSON por connectionId).

Todas las tablas se añaden mediante migraciones idempotentes con CREATE TABLE IF NOT EXISTS.


Código fuente de OmniRoute (a58000c7685f)

HagiCode

HagiCode es un espacio de trabajo de programación con agentes, flujos estructurados, ejecución multiagente y vistas de Hero Dungeon.

Convierte ideas en software útil con un flujo de trabajo con agentes más inteligente, rápido y ameno.

Interfaz principal de HagiCode con tema claro
  • SmartLos flujos estructurados convierten la intención en un itinerario ejecutable desde la idea hasta la entrega.
  • EfficientLos flujos multiagente permiten avanzar en paralelo con la investigación, implementación y revisión.
  • FunHero Dungeon hace que las largas sesiones de programación sean visuales y colaborativas.
Visitar HagiCode