mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 05:41:24 +00:00
refactor(src): split oversized modules
This commit is contained in:
199
src/agents/auth-profiles/order.ts
Normal file
199
src/agents/auth-profiles/order.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { normalizeProviderId } from "../model-selection.js";
|
||||
import { listProfilesForProvider } from "./profiles.js";
|
||||
import type { AuthProfileStore } from "./types.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?: ClawdbotConfig;
|
||||
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
|
||||
.sort((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
|
||||
.sort((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,
|
||||
}))
|
||||
.sort((a, b) => a.cooldownUntil - b.cooldownUntil)
|
||||
.map((entry) => entry.profileId);
|
||||
|
||||
return [...sorted, ...cooldownSorted];
|
||||
}
|
||||
Reference in New Issue
Block a user