Spaces:
Running
Running
| import type { OpenClawConfig } from "../../config/config.js"; | |
| import type { AuthProfileStore } from "./types.js"; | |
| import { normalizeProviderId } from "../model-selection.js"; | |
| import { listProfilesForProvider } from "./profiles.js"; | |
| import { isProfileInCooldown } from "./usage.js"; | |
| function resolveProfileUnusableUntil(stats: { | |
| cooldownUntil?: number; | |
| disabledUntil?: number; | |
| }): number | null { | |
| const values = [stats.cooldownUntil, stats.disabledUntil] | |
| .filter((value): value is number => typeof value === "number") | |
| .filter((value) => Number.isFinite(value) && value > 0); | |
| if (values.length === 0) { | |
| return null; | |
| } | |
| return Math.max(...values); | |
| } | |
| export function resolveAuthProfileOrder(params: { | |
| cfg?: OpenClawConfig; | |
| store: AuthProfileStore; | |
| provider: string; | |
| preferredProfile?: string; | |
| }): string[] { | |
| const { cfg, store, provider, preferredProfile } = params; | |
| const providerKey = normalizeProviderId(provider); | |
| const now = Date.now(); | |
| const storedOrder = (() => { | |
| const order = store.order; | |
| if (!order) { | |
| return undefined; | |
| } | |
| for (const [key, value] of Object.entries(order)) { | |
| if (normalizeProviderId(key) === providerKey) { | |
| return value; | |
| } | |
| } | |
| return undefined; | |
| })(); | |
| const configuredOrder = (() => { | |
| const order = cfg?.auth?.order; | |
| if (!order) { | |
| return undefined; | |
| } | |
| for (const [key, value] of Object.entries(order)) { | |
| if (normalizeProviderId(key) === providerKey) { | |
| return value; | |
| } | |
| } | |
| return undefined; | |
| })(); | |
| const explicitOrder = storedOrder ?? configuredOrder; | |
| const explicitProfiles = cfg?.auth?.profiles | |
| ? Object.entries(cfg.auth.profiles) | |
| .filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey) | |
| .map(([profileId]) => profileId) | |
| : []; | |
| const baseOrder = | |
| explicitOrder ?? | |
| (explicitProfiles.length > 0 ? explicitProfiles : listProfilesForProvider(store, providerKey)); | |
| if (baseOrder.length === 0) { | |
| return []; | |
| } | |
| const filtered = baseOrder.filter((profileId) => { | |
| const cred = store.profiles[profileId]; | |
| if (!cred) { | |
| return false; | |
| } | |
| if (normalizeProviderId(cred.provider) !== providerKey) { | |
| return false; | |
| } | |
| const profileConfig = cfg?.auth?.profiles?.[profileId]; | |
| if (profileConfig) { | |
| if (normalizeProviderId(profileConfig.provider) !== providerKey) { | |
| return false; | |
| } | |
| if (profileConfig.mode !== cred.type) { | |
| const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token"; | |
| if (!oauthCompatible) { | |
| return false; | |
| } | |
| } | |
| } | |
| if (cred.type === "api_key") { | |
| return Boolean(cred.key?.trim()); | |
| } | |
| if (cred.type === "token") { | |
| if (!cred.token?.trim()) { | |
| return false; | |
| } | |
| if ( | |
| typeof cred.expires === "number" && | |
| Number.isFinite(cred.expires) && | |
| cred.expires > 0 && | |
| now >= cred.expires | |
| ) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| if (cred.type === "oauth") { | |
| return Boolean(cred.access?.trim() || cred.refresh?.trim()); | |
| } | |
| return false; | |
| }); | |
| const deduped: string[] = []; | |
| for (const entry of filtered) { | |
| if (!deduped.includes(entry)) { | |
| deduped.push(entry); | |
| } | |
| } | |
| // If user specified explicit order (store override or config), respect it | |
| // exactly, but still apply cooldown sorting to avoid repeatedly selecting | |
| // known-bad/rate-limited keys as the first candidate. | |
| if (explicitOrder && explicitOrder.length > 0) { | |
| // ...but still respect cooldown tracking to avoid repeatedly selecting a | |
| // known-bad/rate-limited key as the first candidate. | |
| const available: string[] = []; | |
| const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = []; | |
| for (const profileId of deduped) { | |
| const cooldownUntil = resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? 0; | |
| if ( | |
| typeof cooldownUntil === "number" && | |
| Number.isFinite(cooldownUntil) && | |
| cooldownUntil > 0 && | |
| now < cooldownUntil | |
| ) { | |
| inCooldown.push({ profileId, cooldownUntil }); | |
| } else { | |
| available.push(profileId); | |
| } | |
| } | |
| const cooldownSorted = inCooldown | |
| .toSorted((a, b) => a.cooldownUntil - b.cooldownUntil) | |
| .map((entry) => entry.profileId); | |
| const ordered = [...available, ...cooldownSorted]; | |
| // Still put preferredProfile first if specified | |
| if (preferredProfile && ordered.includes(preferredProfile)) { | |
| return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)]; | |
| } | |
| return ordered; | |
| } | |
| // Otherwise, use round-robin: sort by lastUsed (oldest first) | |
| // preferredProfile goes first if specified (for explicit user choice) | |
| // lastGood is NOT prioritized - that would defeat round-robin | |
| const sorted = orderProfilesByMode(deduped, store); | |
| if (preferredProfile && sorted.includes(preferredProfile)) { | |
| return [preferredProfile, ...sorted.filter((e) => e !== preferredProfile)]; | |
| } | |
| return sorted; | |
| } | |
| function orderProfilesByMode(order: string[], store: AuthProfileStore): string[] { | |
| const now = Date.now(); | |
| // Partition into available and in-cooldown | |
| const available: string[] = []; | |
| const inCooldown: string[] = []; | |
| for (const profileId of order) { | |
| if (isProfileInCooldown(store, profileId)) { | |
| inCooldown.push(profileId); | |
| } else { | |
| available.push(profileId); | |
| } | |
| } | |
| // Sort available profiles by lastUsed (oldest first = round-robin) | |
| // Then by lastUsed (oldest first = round-robin within type) | |
| const scored = available.map((profileId) => { | |
| const type = store.profiles[profileId]?.type; | |
| const typeScore = type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3; | |
| const lastUsed = store.usageStats?.[profileId]?.lastUsed ?? 0; | |
| return { profileId, typeScore, lastUsed }; | |
| }); | |
| // Primary sort: type preference (oauth > token > api_key). | |
| // Secondary sort: lastUsed (oldest first for round-robin within type). | |
| const sorted = scored | |
| .toSorted((a, b) => { | |
| // First by type (oauth > token > api_key) | |
| if (a.typeScore !== b.typeScore) { | |
| return a.typeScore - b.typeScore; | |
| } | |
| // Then by lastUsed (oldest first) | |
| return a.lastUsed - b.lastUsed; | |
| }) | |
| .map((entry) => entry.profileId); | |
| // Append cooldown profiles at the end (sorted by cooldown expiry, soonest first) | |
| const cooldownSorted = inCooldown | |
| .map((profileId) => ({ | |
| profileId, | |
| cooldownUntil: resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? now, | |
| })) | |
| .toSorted((a, b) => a.cooldownUntil - b.cooldownUntil) | |
| .map((entry) => entry.profileId); | |
| return [...sorted, ...cooldownSorted]; | |
| } | |