Zum Inhalt springen
OmniRoute source

Quota Sharing Engine (Deutsch)

Die Quota-Sharing-Engine verteilt das zeitbasierte Kontingent eines Anbieters (z. B. Codex- 5-Stunden-Fenster, Kimi 1500 Anfragen/h) fair auf mehrere API-Schlüssel, die dieselbe Verbindung verwenden.

Gelöstes Problem: OmniRoute leitet viele API-Schlüssel über dasselbe Konto beim Upstream-Anbieter weiter. Ohne Freigabelogik kann eine Lastspitze von Schlüssel A das Anbieterkontingent für die Stunde ausschöpfen, sodass die Schlüssel B und C blockiert bleiben, bis das Fenster zurückgesetzt wird. Die Engine verhindert dies durch:

  1. Nachverfolgung des rollierenden Verbrauchs jedes Schlüssels pro Dimension (%, Anfragen, Token, $).
  2. Anwendung eines arbeitserhaltenden Fair-Share-Algorithmus: Ein Schlüssel darf ungenutzte Anteile verwenden, solange der globale Pool nicht ausgelastet ist.
  3. Durchsetzung des Ergebnisses im kritischen Pfad (chatCore.ts), bevor die Anfrage den Upstream-Executor erreicht.

Implementiert in src/lib/quota/fairShare.ts.

Bedingung Modus Verhalten
globalUsedPercent < saturationThreshold Großzügig Schlüssel darf bis zum globalen Limit abzüglich des Gesamtverbrauchs zusätzliche Anteile nutzen
globalUsedPercent >= saturationThreshold Strikt Individuellen fairen Anteil strikt durchsetzen

Der Standardwert für saturationThreshold ist 0.5 (Umgebungsvariable QUOTA_SATURATION_THRESHOLD).

Für jede aktive Dimension im Pool berechnet die Engine:

fairShareAllowed = poolLimit × (allocationWeight / 100)
consumed = aktueller rollierender Wert für diesen Schlüssel (aus QuotaStore.peek)
remaining = fairShareAllowed - consumed

Dann gilt:

  • policy = hard: Wenn consumed > fairShareAllowed und der Modus strikt ist → blockieren.
  • policy = soft: Wenn consumed > fairShareAllowed und der Modus strikt ist → benachteiligen (in der Kombination niedriger priorisieren; niemals hart blockieren).
  • policy = burst: Zulassen, solange globaler Spielraum vorhanden ist, unabhängig vom fairen Anteil.

capValue + capUnit einer Zuweisung bilden eine harte Obergrenze, unabhängig von Modus oder Richtlinie. Jede Dimension, für die consumed >= capValue gilt, blockiert die Anfrage immer.

Eine Anfrage wird blockiert, wenn irgendeine Dimension im Pool sie blockieren würde. Die Dimensionen sind unabhängig voneinander — die Ausschöpfung von 5h% wirkt sich nicht auf die Dimension weekly% aus.

Im großzügigen Modus kann ein Schlüssel, dessen Zuweisung nicht vollständig genutzt wurde, Überschüsse aus den nicht genutzten Anteilen anderer Schlüssel verwenden. Die Formel lautet:

maxAllowed = globalLimit - consumedByOtherKeys

wobei consumedByOtherKeys = consumedTotal - consumedByThisKey gilt. Die globale Obergrenze (limit des Pools für diese Dimension) ist stets die harte Obergrenze.


Implementiert in src/lib/quota/sqliteQuotaStore.ts und redisQuotaStore.ts.

Zwei Buckets pro (apiKeyId, dimensionKey):

  • curr: aktueller Bucket (floor(nowMs / windowMs))
  • prev: vorheriger Bucket (curr - 1)

Effektiver rollierender Wert:

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

Genauigkeit: ca. 99 %. Der Fehler beträgt an der Grenze zwischen Buckets höchstens 1 % der Fenstergröße (bedingt durch die 2-Bucket-Approximation).

SQLite-Treiber: Ein In-Memory-Mutex pro Schlüssel (apiKeyId | dimensionKey) verhindert den Read-Modify-Write-Wettlauf. Das Muster entspricht dem Anti-Thundering-Herd-Ansatz aus src/sse/services/auth.ts.

Redis-Treiber: Lua-EVAL-Skript für atomare Inkrementierung — wird als einzelner Redis-Befehl ausgeführt.


  • Tabelle: quota_consumption (siehe Migration 073_quota_pools.sql / 074_quota_consumption.sql).
  • Am besten für Bereitstellungen mit einer einzelnen Instanz geeignet.
  • Die gesamte Persistenz erfolgt in der bestehenden OmniRoute-SQLite-DB (DATA_DIR/storage.sqlite).
  • Erfordert das npm-Paket ioredis.
  • Zähler werden in Redis gespeichert; Metadaten (Pools/Zuweisungen) verbleiben in SQLite.
  • Am besten für Bereitstellungen mit mehreren Replikaten geeignet, bei denen Zähler gemeinsam genutzt werden müssen.

Über die Einstellungsoberfläche (/dashboard/settings → Quota-Speicher) oder über Umgebungsvariablen:

Terminal-Fenster
QUOTA_STORE_DRIVER=redis
QUOTA_STORE_REDIS_URL=redis://localhost:6379

Die DB-Einstellung hat Vorrang vor der Umgebungsvariable. Wenn driver=redis festgelegt ist, aber die URL fehlt oder ioredis nicht installiert ist, greift die Factory auf SQLite zurück und protokolliert eine Warnung.

Reihenfolge der Treiberauswahl:

  1. DB-Einstellung quotaStore.driver
  2. Umgebungsvariable QUOTA_STORE_DRIVER
  3. Standard: sqlite

Ein Pool kann mehrere Dimensionen haben. Jede Dimension ist unabhängig:

QuotaDimension {
unit: "percent" | "requests" | "tokens" | "usd",
window: "5h" | "hourly" | "daily" | "weekly" | "monthly",
limit: number, // globale Pool-Obergrenze für diese Dimension
}

Beispiel: Codex-Tarif (5h% + wöchentlich%):

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

Eine Anfrage muss alle Dimensionen erfüllen, um zugelassen zu werden.


Implementiert in src/lib/quota/planResolver.ts.

Priorität (von der höchsten zur niedrigsten):

  1. Manuelle DB-Überschreibung — Tabelle provider_plans, pro connectionId.
  2. Bekannter Katalog — src/lib/quota/planRegistry.ts (nur Daten).
  3. Leerer Tarif — keine Dimensionen, manuelle Konfiguration erforderlich.
Anbieter Dimensionen
codex percent/5h/100, percent/weekly/100
glm tokens/5h (limit=0, unbekannt), 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 Kein Standard — manuelle Konfiguration erforderlich

Wird nach Authentifizierungs- und Richtlinienprüfungen, aber vor dem Upstream-Executor ausgeführt:

resolveComboTargets / handleSingleModel
→ enforceQuotaShare(apiKeyId, connectionId, provider, estimatedCost)
→ getQuotaStore().peek() pro Dimension
→ fairShare.decideFairShare()
→ falls blockieren → 429 zurückgeben (buildErrorBody, feste Regel Nr. 12)
→ falls zulassen + depriorisieren → quotaSoftPenalty=true für Kandidaten setzen
→ executor.execute()

Fail-open: Wenn enforceQuotaShare eine Ausnahme auslöst, wird die Anfrage zugelassen und eine pino.warn-Meldung protokolliert. Dadurch wird verhindert, dass ein Fehler in der Kontingent-Engine den gesamten Datenverkehr blockiert.

Nach einer erfolgreichen Antwort:

Executor gibt Erfolg zurück
→ spendRecorder.recordConsumption(apiKeyId, connectionId, provider, actualCost)
→ getQuotaStore().consume() pro Dimension
→ Fail-open: Fehler werden als pino.warn protokolliert und niemals an den Client weitergegeben

Abweichungshinweis: Wenn consume nach der Antwort fehlschlägt, erfasst der rollierende Zähler zu wenig. Das Sättigungssignal des Anbieters (z. B. anthropic-ratelimit-unified-5h-utilization) korrigiert die globale Schätzung bei der nächsten Anfrage.

Sanfte Combo-Abwertung (open-sse/services/combo.ts)

Abschnitt betitelt „Sanfte Combo-Abwertung (open-sse/services/combo.ts)“

Wenn decision.deprioritize === true:

if (candidate.quotaSoftPenalty) {
score *= QUOTA_SOFT_DEPRIORITIZE_FACTOR; // Standardwert 0.7
}

Die Abwertung wird nach allen anderen Bewertungsfaktoren angewendet. Sie verringert die Wahrscheinlichkeit, dass die automatische Combo einen ausgelasteten Schlüssel auswählt, ohne ihn vollständig zu blockieren.


/dashboard/costs/quota-share — Hauptseite der Pools

Abschnitt betitelt „/dashboard/costs/quota-share — Hauptseite der Pools“

Komponenten (alle in src/app/(dashboard)/dashboard/costs/quota-share/):

Komponente Zweck
QuotaConceptCard Einführungskarte, die neuen Benutzern die gemeinsame Quotennutzung erklärt
CreatePoolModal Erstellt einen neuen Quoten-Pool (Verbindung + Name + anfängliche Zuweisungen)
PoolCard Zusammenfassung pro Pool: Name, Verbindung, Anzahl der Zuweisungen
DimensionBar Gestapelter Balken pro Dimension: Anteil jedes Schlüssels + globale Nutzung
AllocationTable Tabelle mit Verbrauch, fairem Anteil, Defizit/Überschuss und Ausleihkennzeichen
BurnRateChart Liniendiagramm der EMA-Verbrauchsrate (Recharts verzögert über dynamic())
EditAllocationsModal Bearbeitet Zuweisungsgewichtungen, Obergrenzen und Richtlinien eines Pools

Die Hooks der Seite:

  • usePools — ruft alle 30 Sekunden GET /api/quota/pools ab.
  • usePoolUsage — ruft bei Bedarf GET /api/quota/pools/[id]/usage ab.
  • useLocalStoragePoolMigration — wird beim Einbinden einmal ausgeführt, um veraltete LS-Daten zu migrieren.

/dashboard/costs/quota-share/plans — Konfiguration des Anbieterplans

Abschnitt betitelt „/dashboard/costs/quota-share/plans — Konfiguration des Anbieterplans“
  • ProviderPlanConfigClient.tsx: Dropdown-Menü zur Auswahl eines Anbieters, zur Anzeige des aufgelösten Plans (automatisch aus dem Katalog oder manuell überschrieben) und zur Bearbeitung von Dimensionen.
  • Änderungen werden über PUT /api/quota/plans/[connectionId] gespeichert.
  • Durch Löschen wird auf den Katalog oder einen leeren Plan zurückgesetzt.

Variable Standardwert Beschreibung
QUOTA_STORE_DRIVER sqlite Zu verwendender Treiber: sqlite oder redis
QUOTA_STORE_REDIS_URL (leer) Redis-URL, z. B. redis://localhost:6379
QUOTA_SATURATION_THRESHOLD 0.5 0..1; >= Schwellenwert aktiviert den strikten Modus
QUOTA_SOFT_DEPRIORITIZE_FACTOR 0.7 0..1; Multiplikator für den Kombinationswert der weichen Richtlinie
QUOTA_CONSUMPTION_RETENTION_DAYS 14 Tage, bevor die GC alte quota_consumption-Buckets entfernt

DB-Einstellungen (quotaStore.*) überschreiben Umgebungsvariablen.


Redis ist konfiguriert, stellt aber keine Verbindung her

Abschnitt betitelt „Redis ist konfiguriert, stellt aber keine Verbindung her“

Prüfen Sie, ob ioredis installiert ist (npm ls ioredis) und ob QUOTA_STORE_REDIS_URL erreichbar ist. Bei einem Verbindungsfehler greift die Factory auf SQLite zurück (protokolliert auf warn-Ebene).

Wenn peek eine Ausnahme auslöst, behandelt enforceQuotaShare das Ergebnis als „zulassen“ (Fail-Open). Prüfen Sie die pino-Protokolle auf Einträge zu quota:enforce und quota:factory, um die Ursache zu ermitteln.

Wenn die tatsächliche Anbieternutzung von den Zählern abweicht, ist dies zu erwarten — das gleitende 2-Bucket-Fenster weist an den Fenstergrenzen einen Fehler von etwa 1 % auf, und consume wird nach der Antwort ohne Warten auf den Abschluss ausgeführt. Das Sättigungssignal (saturationSignals.ts) liest die tatsächliche Anbieterauslastung mit einer TTL von 30 Sekunden und passt globalUsedPercent entsprechend an.

Pool zeigt „keine Daten“ für die Verbrauchsrate

Abschnitt betitelt „Pool zeigt „keine Daten“ für die Verbrauchsrate“

computeBurnRate benötigt mindestens 2 historische Stichproben. Neue Pools ohne vorherige consume-Aufrufe zeigen tokensPerSecond: 0 und timeToExhaustionMs: null an.


Beim ersten Laden von /dashboard/costs/quota-share prüft der Hook useLocalStoragePoolMigration:

  1. localStorage.getItem("omniroute:quota-share:pools") ist nicht leer.
  2. GET /api/quota/pools gibt [] zurück (die DB ist leer).

Wenn beide Bedingungen erfüllt sind, sendet er jeden Legacy-Pool gesammelt an POST /api/quota/pools und entfernt anschließend den localStorage-Schlüssel. Die Migration ist idempotent: Bedingung 2 verhindert eine erneute Migration.


quota-share ist eine ausschließlich interne Routing-Strategie (INTERNAL_ROUTING_STRATEGY_VALUES in src/shared/constants/routingStrategies.ts). Sie wird ausschließlich von systemseitig erstellten qtSd/-Pool-Kombinationen verwendet und bewusst aus ROUTING_STRATEGY_VALUES ausgeschlossen, sodass sie weder in der Benutzeroberfläche noch in der API als vom Benutzer auswählbare Option erscheint.


Die Quota-Share-Engine wird mit zwei Ebenen automatisierter Tests ausgeliefert:

Suite Befehl Abgedeckte Bereiche
Unit-Tests (29 Tests) node --import tsx/esm --test tests/unit/quota-share-strategy.test.ts DRR-Scheduler, Sättigungssteuerung, Nebenläufigkeitsgrenzen, fairShare-Berechnung, Einreihung in die Rückstauwarteschlange
Integrationsmatrix npm run test:combo:matrix Durchgängige Routing-Entscheidung über die reale Kombinationspipeline; DRR-Fairness und Herabpriorisierung bei Sättigung über Live-Schnittstellen (registerQuotaFetcher, setLKGP, __setHeadroomSaturationFetcherForTests)

Die Integrationsmatrix wird in der CI zusammen mit allen 19 öffentlichen Strategien ausgeführt. Die Unit-Test-Suite kann eigenständig ausgeführt werden.


Drei durch die Migrationen 078, 079 und 085 hinzugefügte Tabellen:

  • quota_pools + quota_allocations — Pool-Definitionen und Zuweisungen pro Schlüssel.
  • quota_consumption — rollierende Zähler mit 2 Buckets pro (apiKeyId, dimensionKey).
  • provider_plans — manuelle Überschreibungen von Anbieterplänen (Dimensionen als JSON pro connectionId).

Alle Tabellen werden über idempotente CREATE TABLE IF NOT EXISTS-Migrationen hinzugefügt.


OmniRoute-Quellcode (a58000c7685f)

HagiCode

HagiCode ist ein agentischer Coding-Arbeitsplatz mit strukturierten Workflows, Multi-Agent-Ausführung und Hero-Dungeon-Ansichten.

Mit einem intelligenteren, schnelleren und unterhaltsameren agentischen Workflow wird aus Ideen nutzbare Software.

HagiCode-Hauptoberfläche im hellen Design
  • SmartStrukturierte Workflows machen aus Absichten einen umsetzbaren Weg von der Idee bis zur Auslieferung.
  • EfficientMulti-Agent-Workflows führen Recherche, Umsetzung und Prüfung parallel aus.
  • FunHero Dungeon macht lange Coding-Sitzungen anschaulich und gemeinschaftlich.
HagiCode besuchen