Quota Sharing Engine (Deutsch)
Überblick
Abschnitt betitelt „Überblick“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:
- Nachverfolgung des rollierenden Verbrauchs jedes Schlüssels pro Dimension (%, Anfragen, Token, $).
- Anwendung eines arbeitserhaltenden Fair-Share-Algorithmus: Ein Schlüssel darf ungenutzte Anteile verwenden, solange der globale Pool nicht ausgelastet ist.
- Durchsetzung des Ergebnisses im kritischen Pfad (
chatCore.ts), bevor die Anfrage den Upstream-Executor erreicht.
Algorithmus: Arbeitserhaltendes Fair Sharing
Abschnitt betitelt „Algorithmus: Arbeitserhaltendes Fair Sharing“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).
Entscheidung pro Dimension
Abschnitt betitelt „Entscheidung pro Dimension“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 - consumedDann gilt:
policy = hard: Wennconsumed > fairShareAllowedund der Modus strikt ist → blockieren.policy = soft: Wennconsumed > fairShareAllowedund 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.
Absolute Obergrenze
Abschnitt betitelt „Absolute Obergrenze“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.
Mehrdimensionale Prüfung
Abschnitt betitelt „Mehrdimensionale Prüfung“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.
Nutzung ungenutzter Anteile
Abschnitt betitelt „Nutzung ungenutzter Anteile“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 - consumedByOtherKeyswobei consumedByOtherKeys = consumedTotal - consumedByThisKey gilt. Die globale Obergrenze
(limit des Pools für diese Dimension) ist stets die harte Obergrenze.
Zähler mit gleitendem Fenster
Abschnitt betitelt „Zähler mit gleitendem Fenster“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 × windowMselapsed = nowMs - bucketStartMsweight = 1 - elapsed / windowMs
effective = prev × weight + currGenauigkeit: ca. 99 %. Der Fehler beträgt an der Grenze zwischen Buckets höchstens 1 % der Fenstergröße (bedingt durch die 2-Bucket-Approximation).
Nebenläufigkeit
Abschnitt betitelt „Nebenläufigkeit“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.
Treiber
Abschnitt betitelt „Treiber“SQLite (Standard, ohne Installation)
Abschnitt betitelt „SQLite (Standard, ohne Installation)“- Tabelle:
quota_consumption(siehe Migration073_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).
Redis (optional, mehrere Instanzen)
Abschnitt betitelt „Redis (optional, mehrere Instanzen)“- 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.
Wechseln des Treibers
Abschnitt betitelt „Wechseln des Treibers“Über die Einstellungsoberfläche (/dashboard/settings → Quota-Speicher) oder über Umgebungsvariablen:
QUOTA_STORE_DRIVER=redisQUOTA_STORE_REDIS_URL=redis://localhost:6379Die 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:
- DB-Einstellung
quotaStore.driver - Umgebungsvariable
QUOTA_STORE_DRIVER - Standard:
sqlite
Mehrere Dimensionen
Abschnitt betitelt „Mehrere Dimensionen“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.
Tarifauflösung
Abschnitt betitelt „Tarifauflösung“Implementiert in src/lib/quota/planResolver.ts.
Priorität (von der höchsten zur niedrigsten):
- Manuelle DB-Überschreibung — Tabelle
provider_plans, proconnectionId. - Bekannter Katalog —
src/lib/quota/planRegistry.ts(nur Daten). - Leerer Tarif — keine Dimensionen, manuelle Konfiguration erforderlich.
Bekannter Katalog
Abschnitt betitelt „Bekannter Katalog“| 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 |
Pipeline-Integration
Abschnitt betitelt „Pipeline-Integration“PRE-Hook (open-sse/handlers/chatCore.ts)
Abschnitt betitelt „PRE-Hook (open-sse/handlers/chatCore.ts)“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.
POST-Hook (Verbrauch erfassen)
Abschnitt betitelt „POST-Hook (Verbrauch erfassen)“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 weitergegebenAbweichungshinweis: 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.
UI-Rundgang
Abschnitt betitelt „UI-Rundgang“/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 SekundenGET /api/quota/poolsab.usePoolUsage— ruft bei BedarfGET /api/quota/pools/[id]/usageab.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.
Umgebungsvariablen
Abschnitt betitelt „Umgebungsvariablen“| 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.
Fehlerbehebung
Abschnitt betitelt „Fehlerbehebung“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).
peek gibt veraltete Daten zurück / Fail-Open
Abschnitt betitelt „peek gibt veraltete Daten zurück / Fail-Open“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.
Abweichung des Verbrauchszählers
Abschnitt betitelt „Abweichung des Verbrauchszählers“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.
Migration von localStorage
Abschnitt betitelt „Migration von localStorage“Beim ersten Laden von /dashboard/costs/quota-share prüft der Hook useLocalStoragePoolMigration:
localStorage.getItem("omniroute:quota-share:pools")ist nicht leer.GET /api/quota/poolsgibt[]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.
Interne Strategieklassifizierung
Abschnitt betitelt „Interne Strategieklassifizierung“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.
Testabdeckung
Abschnitt betitelt „Testabdeckung“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.
Zusammenfassung des DB-Schemas
Abschnitt betitelt „Zusammenfassung des DB-Schemas“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.
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.

- 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.