Files
OmniRoute/open-sse/services/quotaPreflight.ts
diegosouzapw 1c2f553a7c feat: features 07/09/04/06 — codex clamp, openrouter cache, quota preflight & monitor
feat(codex): clamp reasoning effort per model (feature-07)
- Add MAX_EFFORT_BY_MODEL table in CodexExecutor
- Add clampEffort() applied after effort derivation
- Logs debug when clamp is applied

feat(catalog): OpenRouter catalog with persistent cache (feature-09)
- New src/lib/catalog/openrouterCatalog.ts with TTL 24h + stale-if-error
- New GET /api/models/openrouter-catalog endpoint (authenticated)
- Reduces redundant OpenRouter API calls from dashboard

feat(quota): quota preflight with per-provider toggle (feature-04)
- New open-sse/services/quotaPreflight.ts
- Toggle: providerSpecificData.quotaPreflightEnabled (default: false)
- Extensible via registerQuotaFetcher() pattern
- Graceful degradation when no fetcher registered

feat(quota): quota session monitor with per-provider toggle (feature-06)
- New open-sse/services/quotaMonitor.ts
- Toggle: providerSpecificData.quotaMonitorEnabled (default: false)
- Adaptive polling: 60s normal / 15s critical
- Alert deduplication (5min suppression window)
- timer.unref() ensures clean process exit

feat(providers): support providerSpecificData patch in PUT /api/providers/[id]
- Partial merge of providerSpecificData (preserves existing keys)
- Schema updated to accept providerSpecificData in updateProviderConnectionSchema
2026-03-08 17:36:17 -03:00

80 lines
2.1 KiB
TypeScript

/**
* quotaPreflight.ts — Feature 04
* Quota Preflight & Troca Proativa de Conta
*
* Toggle: providerSpecificData.quotaPreflightEnabled (default: false)
* Providers register quota fetchers via registerQuotaFetcher().
* Graceful degradation when no fetcher registered.
*/
export interface PreflightQuotaResult {
proceed: boolean;
reason?: string;
quotaPercent?: number;
}
export interface QuotaInfo {
used: number;
total: number;
percentUsed: number;
}
export type QuotaFetcher = (connectionId: string) => Promise<QuotaInfo | null>;
const EXHAUSTION_THRESHOLD = 0.95;
const WARN_THRESHOLD = 0.8;
const quotaFetcherRegistry = new Map<string, QuotaFetcher>();
export function registerQuotaFetcher(provider: string, fetcher: QuotaFetcher): void {
quotaFetcherRegistry.set(provider, fetcher);
}
export function isQuotaPreflightEnabled(connection: Record<string, unknown>): boolean {
const psd = connection?.providerSpecificData as Record<string, unknown> | undefined;
return psd?.quotaPreflightEnabled === true;
}
export async function preflightQuota(
provider: string,
connectionId: string,
connection: Record<string, unknown>
): Promise<PreflightQuotaResult> {
if (!isQuotaPreflightEnabled(connection)) {
return { proceed: true };
}
const fetcher = quotaFetcherRegistry.get(provider);
if (!fetcher) {
return { proceed: true };
}
let quota: QuotaInfo | null = null;
try {
quota = await fetcher(connectionId);
} catch {
return { proceed: true };
}
if (!quota) {
return { proceed: true };
}
const { percentUsed } = quota;
if (percentUsed >= EXHAUSTION_THRESHOLD) {
console.info(
`[QuotaPreflight] ${provider}/${connectionId}: ${(percentUsed * 100).toFixed(1)}% used — switching`
);
return { proceed: false, reason: "quota_exhausted", quotaPercent: percentUsed };
}
if (percentUsed >= WARN_THRESHOLD) {
console.warn(
`[QuotaPreflight] ${provider}/${connectionId}: ${(percentUsed * 100).toFixed(1)}% used — approaching limit`
);
}
return { proceed: true, quotaPercent: percentUsed };
}