Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,25 +368,52 @@ function loadAccounts() {

// ─── Dynamic model catalog from cloud ─────────────────────

// Tracks whether the cloud model catalog has been successfully merged at
// least once since startup. When false, trySyncModelCatalog() will fire
// off a fetch whenever an account becomes active — covering the case where
// the startup fetch was skipped because no active account existed yet.
let _modelCatalogSynced = false;
// Coalesces concurrent calls so multiple simultaneous status transitions
// (e.g. bulk-add accounts) only trigger one fetch.
let _modelCatalogSyncPromise = null;

async function fetchAndMergeModelCatalog() {
// Use the first active account to fetch the catalog.
const acct = accounts.find(a => a.status === 'active' && a.apiKey);
if (!acct) {
log.debug('No active account for model catalog fetch');
return;
return false;
}
try {
const { getCascadeModelConfigs } = await import('./windsurf-api.js');
const { mergeCloudModels } = await import('./models.js');
const proxy = getEffectiveProxy(acct.id) || null;
const { configs } = await getCascadeModelConfigs(acct.apiKey, proxy);
const added = mergeCloudModels(configs);
_modelCatalogSynced = true;
log.info(`Model catalog: ${configs.length} cloud models, ${added} new entries merged`);
return true;
} catch (e) {
log.warn(`Model catalog fetch failed: ${e.message}`);
return false;
}
}

/**
* Fire-and-forget: trigger a cloud model catalog sync if one hasn't succeeded
* yet and at least one active account exists. Safe to call from any account
* status transition path — coalesces concurrent calls into a single fetch.
*/
export function trySyncModelCatalog() {
if (_modelCatalogSynced) return;
if (_modelCatalogSyncPromise) return;
const acct = accounts.find(a => a.status === 'active' && a.apiKey);
if (!acct) return;
_modelCatalogSyncPromise = fetchAndMergeModelCatalog()
.catch(e => log.warn(`trySyncModelCatalog: ${e.message}`))
.finally(() => { _modelCatalogSyncPromise = null; });
}

async function registerWithCodeium(idToken) {
const { WindsurfClient } = await import('./client.js');
const client = new WindsurfClient('', 0, '');
Expand Down Expand Up @@ -431,6 +458,7 @@ export function addAccountByKey(apiKey, label = '', apiServerUrl = '') {
accounts.push(account);
saveAccounts();
log.info(`Account added: ${safeAccountRef(account)} [api_key]`);
trySyncModelCatalog();
return account;
}

Expand Down Expand Up @@ -464,6 +492,7 @@ export async function addAccountByToken(token, label = '') {
accounts.push(account);
saveAccounts();
log.info(`Account added: ${safeAccountRef(account)} [token] server=${account.apiServerUrl}`);
trySyncModelCatalog();
return account;
}

Expand Down Expand Up @@ -509,6 +538,7 @@ export async function addAccountByEmail(email, password) {
}
saveAccounts();
log.info(`Account added via email: ${safeAccountRef(account)}`);
trySyncModelCatalog();
return account;
}

Expand Down Expand Up @@ -595,6 +625,7 @@ export function setAccountStatus(id, status) {
if (status === 'active') account.errorCount = 0;
saveAccounts();
log.info(`Account ${id} status set to ${status}`);
if (status === 'active') trySyncModelCatalog();
return true;
}

Expand All @@ -608,6 +639,7 @@ export function resetAccountErrors(id) {
account.status = 'active';
saveAccounts();
log.info(`Account ${id} errors reset`);
trySyncModelCatalog();
return true;
}

Expand Down Expand Up @@ -1103,6 +1135,7 @@ function maybeRecoverErrorAccount(account, now) {
account.status = 'active';
account.errorCount = 0;
log.info(`Account ${safeAccountRef(account)} half-open recovery after ${Math.round((now - since) / 60000)}m in error state`);
trySyncModelCatalog();
}

/**
Expand Down Expand Up @@ -1158,6 +1191,7 @@ export function reportSuccess(apiKey) {
if (account.errorCount > 0) {
account.errorCount = 0;
account.status = 'active';
trySyncModelCatalog();
}
account.internalErrorStreak = 0;
// v2.0.56: any successful chat clears the ban-signal streak — Windsurf's
Expand Down Expand Up @@ -2168,7 +2202,8 @@ export async function initAuth() {

// Fetch live model catalog from cloud and merge into hardcoded catalog.
// Fire-and-forget — the hardcoded catalog is sufficient until this completes.
fetchAndMergeModelCatalog().catch(e => log.warn(`Model catalog fetch: ${e.message}`));
// trySyncModelCatalog also fires again when the first account becomes active.
trySyncModelCatalog();

// Periodic Firebase token refresh (every 50 min). Firebase ID tokens expire
// after 60 min; refreshing at 50 keeps a comfortable margin.
Expand Down