Spaces:
Paused
Paused
File size: 5,855 Bytes
5f8456f 90c89b6 5f8456f 4ebb914 5f8456f be3d093 56be298 5f8456f 4ebb914 56be298 5f8456f 90c89b6 56be298 5f8456f 4ebb914 56be298 5f8456f 90c89b6 56be298 5f8456f 90c89b6 5f8456f 56be298 90c89b6 56be298 90c89b6 5f8456f 56be298 5f8456f 56be298 5f8456f 4ebb914 5f8456f 4ebb914 56be298 5f8456f 56be298 5f8456f b94940f 5f8456f 56be298 5f8456f 4ebb914 5f8456f 56be298 5f8456f 56be298 5f8456f 56be298 5f8456f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | /**
* Model Fetcher — background model list refresh from Codex backend.
*
* - Probes known endpoints to discover the models list
* - Normalizes and merges into the model store
* - Non-fatal: all errors log warnings but never crash the server
*/
import { CodexApi } from "../proxy/codex-api.js";
import { applyBackendModelsForPlan } from "./model-store.js";
import type { AccountPool } from "../auth/account-pool.js";
import type { CookieJar } from "../proxy/cookie-jar.js";
import type { ProxyPool } from "../proxy/proxy-pool.js";
import { jitter } from "../utils/jitter.js";
const REFRESH_INTERVAL_HOURS = 1;
const INITIAL_DELAY_MS = 1_000; // 1s after startup (fast plan-map population for mixed-plan routing)
const RETRY_DELAY_MS = 10_000; // 10s retry when accounts aren't ready yet
const MAX_RETRIES = 12; // ~2 minutes of retries before falling back to hourly
let _refreshTimer: ReturnType<typeof setTimeout> | null = null;
let _accountPool: AccountPool | null = null;
let _cookieJar: CookieJar | null = null;
let _proxyPool: ProxyPool | null = null;
let _hasFetchedOnce = false;
/**
* Fetch models from the Codex backend, one query per distinct plan type.
* Returns true if at least one plan's models were fetched successfully.
*/
async function fetchModelsFromBackend(
accountPool: AccountPool,
cookieJar: CookieJar,
proxyPool: ProxyPool | null,
): Promise<boolean> {
if (!accountPool.isAuthenticated()) return false;
const planAccounts = accountPool.getDistinctPlanAccounts();
if (planAccounts.length === 0) {
console.warn("[ModelFetcher] No available accounts — skipping model fetch");
return false;
}
console.log(`[ModelFetcher] Fetching models for ${planAccounts.length} plan(s): ${planAccounts.map((p) => p.planType).join(", ")}`);
let anySuccess = false;
const results = await Promise.allSettled(
planAccounts.map(async (pa) => {
try {
const proxyUrl = proxyPool?.resolveProxyUrl(pa.entryId);
const api = new CodexApi(pa.token, pa.accountId, cookieJar, pa.entryId, proxyUrl);
const models = await api.getModels();
if (models && models.length > 0) {
applyBackendModelsForPlan(pa.planType, models);
console.log(`[ModelFetcher] Plan "${pa.planType}": ${models.length} models`);
anySuccess = true;
} else {
console.log(`[ModelFetcher] Plan "${pa.planType}": empty model list — keeping existing`);
}
} finally {
accountPool.release(pa.entryId);
}
}),
);
for (const r of results) {
if (r.status === "rejected") {
const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
console.warn(`[ModelFetcher] Plan fetch failed: ${msg}`);
}
}
return anySuccess;
}
/**
* Start the background model refresh loop.
* - First fetch after a short delay (auth must be ready)
* - If accounts aren't ready, retry every 10s (up to ~2 min) before falling back to hourly
* - Subsequent fetches every ~1 hour with jitter
*/
export function startModelRefresh(
accountPool: AccountPool,
cookieJar: CookieJar,
proxyPool?: ProxyPool,
): void {
_accountPool = accountPool;
_cookieJar = cookieJar;
_proxyPool = proxyPool ?? null;
_hasFetchedOnce = false;
// Initial fetch after short delay
_refreshTimer = setTimeout(() => {
attemptInitialFetch(accountPool, cookieJar, 0);
}, INITIAL_DELAY_MS);
console.log("[ModelFetcher] Scheduled initial model fetch in 1s");
}
/**
* Attempt initial fetch with fast retry.
* Accounts may still be refreshing tokens at startup (Electron race condition).
* Retry every 10s until success or max retries, then fall back to hourly.
*/
function attemptInitialFetch(
accountPool: AccountPool,
cookieJar: CookieJar,
attempt: number,
): void {
fetchModelsFromBackend(accountPool, cookieJar, _proxyPool)
.then((success) => {
if (success) {
_hasFetchedOnce = true;
scheduleNext(accountPool, cookieJar);
} else if (attempt < MAX_RETRIES) {
console.log(`[ModelFetcher] Accounts not ready, retry ${attempt + 1}/${MAX_RETRIES} in ${RETRY_DELAY_MS / 1000}s`);
_refreshTimer = setTimeout(() => {
attemptInitialFetch(accountPool, cookieJar, attempt + 1);
}, RETRY_DELAY_MS);
} else {
console.warn("[ModelFetcher] Max retries reached, falling back to hourly refresh");
scheduleNext(accountPool, cookieJar);
}
})
.catch(() => {
scheduleNext(accountPool, cookieJar);
});
}
function scheduleNext(
accountPool: AccountPool,
cookieJar: CookieJar,
): void {
const intervalMs = jitter(REFRESH_INTERVAL_HOURS * 3600 * 1000, 0.15);
_refreshTimer = setTimeout(async () => {
try {
await fetchModelsFromBackend(accountPool, cookieJar, _proxyPool);
} finally {
scheduleNext(accountPool, cookieJar);
}
}, intervalMs);
}
/**
* Trigger an immediate model refresh (e.g. after hot-reload or account login).
* No-op if startModelRefresh() hasn't been called yet.
*/
export function triggerImmediateRefresh(): void {
if (_accountPool && _cookieJar) {
fetchModelsFromBackend(_accountPool, _cookieJar, _proxyPool)
.then((success) => {
if (success) _hasFetchedOnce = true;
})
.catch((err) => {
const msg = err instanceof Error ? err.message : String(err);
console.warn(`[ModelFetcher] Immediate refresh failed: ${msg}`);
});
}
}
/** Whether at least one successful backend fetch has completed. */
export function hasFetchedModels(): boolean {
return _hasFetchedOnce;
}
/**
* Stop the background refresh timer.
*/
export function stopModelRefresh(): void {
if (_refreshTimer) {
clearTimeout(_refreshTimer);
_refreshTimer = null;
console.log("[ModelFetcher] Stopped model refresh");
}
}
|