@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,382 lines (1,363 loc) • 134 kB
JavaScript
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { g as resolveStateDir, h as resolveOAuthPath } from "./paths-B4BZAPZh.js";
import { y as resolveUserPath } from "./utils-CP9YLh6M.js";
import { n as DEFAULT_AGENT_ID } from "./session-key-CZ6OwgSB.js";
import { t as createSubsystemLogger } from "./subsystem-BCQGGxdd.js";
import { o as resolveAgentModelPrimary, r as resolveAgentConfig } from "./agent-scope-BnZW9Gh2.js";
import { a as saveJsonFile, i as loadJsonFile, r as resolveCopilotApiToken, t as DEFAULT_COPILOT_API_BASE_URL } from "./github-copilot-token-D2zp6kMZ.js";
import { t as formatCliCommand } from "./command-format-DEKzLnLg.js";
import { t as isTruthyEnvValue } from "./env-VriqyjXT.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { execFileSync } from "node:child_process";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { createAssistantMessageEventStream, getEnvApiKey, getOAuthApiKey, getOAuthProviders } from "@mariozechner/pi-ai";
import { BedrockClient, ListFoundationModelsCommand } from "@aws-sdk/client-bedrock";
//#region src/agents/defaults.ts
const DEFAULT_PROVIDER = "anthropic";
const DEFAULT_MODEL = "claude-opus-4-6";
const DEFAULT_CONTEXT_TOKENS = 2e5;
//#endregion
//#region src/agents/auth-profiles/constants.ts
const AUTH_STORE_VERSION = 1;
const AUTH_PROFILE_FILENAME = "auth-profiles.json";
const LEGACY_AUTH_FILENAME = "auth.json";
const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli";
const MINIMAX_CLI_PROFILE_ID = "minimax-portal:minimax-cli";
const AUTH_STORE_LOCK_OPTIONS = {
retries: {
retries: 10,
factor: 2,
minTimeout: 100,
maxTimeout: 1e4,
randomize: true
},
stale: 3e4
};
const EXTERNAL_CLI_SYNC_TTL_MS = 900 * 1e3;
const EXTERNAL_CLI_NEAR_EXPIRY_MS = 600 * 1e3;
const log$1 = createSubsystemLogger("agents/auth-profiles");
//#endregion
//#region src/agents/auth-profiles/display.ts
function resolveAuthProfileDisplayLabel(params) {
const { cfg, store, profileId } = params;
const profile = store.profiles[profileId];
const email = cfg?.auth?.profiles?.[profileId]?.email?.trim() || (profile && "email" in profile ? profile.email?.trim() : void 0);
if (email) return `${profileId} (${email})`;
return profileId;
}
//#endregion
//#region src/utils/normalize-secret-input.ts
/**
* Secret normalization for copy/pasted credentials.
*
* Common footgun: line breaks (especially `\r`) embedded in API keys/tokens.
* We strip line breaks anywhere, then trim whitespace at the ends.
*
* Intentionally does NOT remove ordinary spaces inside the string to avoid
* silently altering "Bearer <token>" style values.
*/
function normalizeSecretInput(value) {
if (typeof value !== "string") return "";
return value.replace(/[\r\n\u2028\u2029]+/g, "").trim();
}
function normalizeOptionalSecretInput(value) {
const normalized = normalizeSecretInput(value);
return normalized ? normalized : void 0;
}
//#endregion
//#region src/shared/pid-alive.ts
function isPidAlive(pid) {
if (!Number.isFinite(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
//#endregion
//#region src/shared/process-scoped-map.ts
function resolveProcessScopedMap(key) {
const proc = process;
const existing = proc[key];
if (existing) return existing;
const created = /* @__PURE__ */ new Map();
proc[key] = created;
return created;
}
//#endregion
//#region src/plugin-sdk/file-lock.ts
const HELD_LOCKS = resolveProcessScopedMap(Symbol.for("openclaw.fileLockHeldLocks"));
function computeDelayMs(retries, attempt) {
const base = Math.min(retries.maxTimeout, Math.max(retries.minTimeout, retries.minTimeout * retries.factor ** attempt));
const jitter = retries.randomize ? 1 + Math.random() : 1;
return Math.min(retries.maxTimeout, Math.round(base * jitter));
}
async function readLockPayload(lockPath) {
try {
const raw = await fs$1.readFile(lockPath, "utf8");
const parsed = JSON.parse(raw);
if (typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string") return null;
return {
pid: parsed.pid,
createdAt: parsed.createdAt
};
} catch {
return null;
}
}
async function resolveNormalizedFilePath(filePath) {
const resolved = path.resolve(filePath);
const dir = path.dirname(resolved);
await fs$1.mkdir(dir, { recursive: true });
try {
const realDir = await fs$1.realpath(dir);
return path.join(realDir, path.basename(resolved));
} catch {
return resolved;
}
}
async function isStaleLock(lockPath, staleMs) {
const payload = await readLockPayload(lockPath);
if (payload?.pid && !isPidAlive(payload.pid)) return true;
if (payload?.createdAt) {
const createdAt = Date.parse(payload.createdAt);
if (!Number.isFinite(createdAt) || Date.now() - createdAt > staleMs) return true;
}
try {
const stat = await fs$1.stat(lockPath);
return Date.now() - stat.mtimeMs > staleMs;
} catch {
return true;
}
}
async function releaseHeldLock(normalizedFile) {
const current = HELD_LOCKS.get(normalizedFile);
if (!current) return;
current.count -= 1;
if (current.count > 0) return;
HELD_LOCKS.delete(normalizedFile);
await current.handle.close().catch(() => void 0);
await fs$1.rm(current.lockPath, { force: true }).catch(() => void 0);
}
async function acquireFileLock(filePath, options) {
const normalizedFile = await resolveNormalizedFilePath(filePath);
const lockPath = `${normalizedFile}.lock`;
const held = HELD_LOCKS.get(normalizedFile);
if (held) {
held.count += 1;
return {
lockPath,
release: () => releaseHeldLock(normalizedFile)
};
}
const attempts = Math.max(1, options.retries.retries + 1);
for (let attempt = 0; attempt < attempts; attempt += 1) try {
const handle = await fs$1.open(lockPath, "wx");
await handle.writeFile(JSON.stringify({
pid: process.pid,
createdAt: (/* @__PURE__ */ new Date()).toISOString()
}, null, 2), "utf8");
HELD_LOCKS.set(normalizedFile, {
count: 1,
handle,
lockPath
});
return {
lockPath,
release: () => releaseHeldLock(normalizedFile)
};
} catch (err) {
if (err.code !== "EEXIST") throw err;
if (await isStaleLock(lockPath, options.stale)) {
await fs$1.rm(lockPath, { force: true }).catch(() => void 0);
continue;
}
if (attempt >= attempts - 1) break;
await new Promise((resolve) => setTimeout(resolve, computeDelayMs(options.retries, attempt)));
}
throw new Error(`file lock timeout for ${normalizedFile}`);
}
async function withFileLock(filePath, options, fn) {
const lock = await acquireFileLock(filePath, options);
try {
return await fn();
} finally {
await lock.release();
}
}
//#endregion
//#region src/agents/cli-credentials.ts
const log = createSubsystemLogger("agents/auth-profiles");
const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.json";
const MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH = ".minimax/oauth_creds.json";
let qwenCliCache = null;
let minimaxCliCache = null;
function resolveQwenCliCredentialsPath(homeDir) {
const baseDir = homeDir ?? resolveUserPath("~");
return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH);
}
function resolveMiniMaxCliCredentialsPath(homeDir) {
const baseDir = homeDir ?? resolveUserPath("~");
return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH);
}
function readQwenCliCredentials(options) {
return readPortalCliOauthCredentials(resolveQwenCliCredentialsPath(options?.homeDir), "qwen-portal");
}
function readPortalCliOauthCredentials(credPath, provider) {
const raw = loadJsonFile(credPath);
if (!raw || typeof raw !== "object") return null;
const data = raw;
const accessToken = data.access_token;
const refreshToken = data.refresh_token;
const expiresAt = data.expiry_date;
if (typeof accessToken !== "string" || !accessToken) return null;
if (typeof refreshToken !== "string" || !refreshToken) return null;
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
return {
type: "oauth",
provider,
access: accessToken,
refresh: refreshToken,
expires: expiresAt
};
}
function readMiniMaxCliCredentials(options) {
return readPortalCliOauthCredentials(resolveMiniMaxCliCredentialsPath(options?.homeDir), "minimax-portal");
}
function readQwenCliCredentialsCached(options) {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const cacheKey = resolveQwenCliCredentialsPath(options?.homeDir);
if (ttlMs > 0 && qwenCliCache && qwenCliCache.cacheKey === cacheKey && now - qwenCliCache.readAt < ttlMs) return qwenCliCache.value;
const value = readQwenCliCredentials({ homeDir: options?.homeDir });
if (ttlMs > 0) qwenCliCache = {
value,
readAt: now,
cacheKey
};
return value;
}
function readMiniMaxCliCredentialsCached(options) {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const cacheKey = resolveMiniMaxCliCredentialsPath(options?.homeDir);
if (ttlMs > 0 && minimaxCliCache && minimaxCliCache.cacheKey === cacheKey && now - minimaxCliCache.readAt < ttlMs) return minimaxCliCache.value;
const value = readMiniMaxCliCredentials({ homeDir: options?.homeDir });
if (ttlMs > 0) minimaxCliCache = {
value,
readAt: now,
cacheKey
};
return value;
}
//#endregion
//#region src/agents/auth-profiles/external-cli-sync.ts
function shallowEqualOAuthCredentials(a, b) {
if (!a) return false;
if (a.type !== "oauth") return false;
return a.provider === b.provider && a.access === b.access && a.refresh === b.refresh && a.expires === b.expires && a.email === b.email && a.enterpriseUrl === b.enterpriseUrl && a.projectId === b.projectId && a.accountId === b.accountId;
}
function isExternalProfileFresh(cred, now) {
if (!cred) return false;
if (cred.type !== "oauth" && cred.type !== "token") return false;
if (cred.provider !== "qwen-portal" && cred.provider !== "minimax-portal") return false;
if (typeof cred.expires !== "number") return true;
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
}
/** Sync external CLI credentials into the store for a given provider. */
function syncExternalCliCredentialsForProvider(store, profileId, provider, readCredentials, now) {
const existing = store.profiles[profileId];
const creds = !existing || existing.provider !== provider || !isExternalProfileFresh(existing, now) ? readCredentials() : null;
if (!creds) return false;
const existingOAuth = existing?.type === "oauth" ? existing : void 0;
if ((!existingOAuth || existingOAuth.provider !== provider || existingOAuth.expires <= now || creds.expires > existingOAuth.expires) && !shallowEqualOAuthCredentials(existingOAuth, creds)) {
store.profiles[profileId] = creds;
log$1.info(`synced ${provider} credentials from external cli`, {
profileId,
expires: new Date(creds.expires).toISOString()
});
return true;
}
return false;
}
/**
* Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI) into the store.
*
* Returns true if any credentials were updated.
*/
function syncExternalCliCredentials(store) {
let mutated = false;
const now = Date.now();
const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID];
const qwenCreds = !existingQwen || existingQwen.provider !== "qwen-portal" || !isExternalProfileFresh(existingQwen, now) ? readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }) : null;
if (qwenCreds) {
const existing = store.profiles[QWEN_CLI_PROFILE_ID];
const existingOAuth = existing?.type === "oauth" ? existing : void 0;
if ((!existingOAuth || existingOAuth.provider !== "qwen-portal" || existingOAuth.expires <= now || qwenCreds.expires > existingOAuth.expires) && !shallowEqualOAuthCredentials(existingOAuth, qwenCreds)) {
store.profiles[QWEN_CLI_PROFILE_ID] = qwenCreds;
mutated = true;
log$1.info("synced qwen credentials from qwen cli", {
profileId: QWEN_CLI_PROFILE_ID,
expires: new Date(qwenCreds.expires).toISOString()
});
}
}
if (syncExternalCliCredentialsForProvider(store, MINIMAX_CLI_PROFILE_ID, "minimax-portal", () => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }), now)) mutated = true;
return mutated;
}
//#endregion
//#region src/agents/agent-paths.ts
function resolveOpenClawAgentDir() {
const override = process.env.OPENCLAW_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
if (override) return resolveUserPath(override);
return resolveUserPath(path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent"));
}
//#endregion
//#region src/agents/auth-profiles/paths.ts
function resolveAuthStorePath(agentDir) {
const resolved = resolveUserPath(agentDir ?? resolveOpenClawAgentDir());
return path.join(resolved, AUTH_PROFILE_FILENAME);
}
function resolveLegacyAuthStorePath(agentDir) {
const resolved = resolveUserPath(agentDir ?? resolveOpenClawAgentDir());
return path.join(resolved, LEGACY_AUTH_FILENAME);
}
function resolveAuthStorePathForDisplay(agentDir) {
const pathname = resolveAuthStorePath(agentDir);
return pathname.startsWith("~") ? pathname : resolveUserPath(pathname);
}
function ensureAuthStoreFile(pathname) {
if (fs.existsSync(pathname)) return;
saveJsonFile(pathname, {
version: AUTH_STORE_VERSION,
profiles: {}
});
}
//#endregion
//#region src/agents/auth-profiles/store.ts
async function updateAuthProfileStoreWithLock(params) {
const authPath = resolveAuthStorePath(params.agentDir);
ensureAuthStoreFile(authPath);
try {
return await withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
const store = ensureAuthProfileStore(params.agentDir);
if (params.updater(store)) saveAuthProfileStore(store, params.agentDir);
return store;
});
} catch {
return null;
}
}
function coerceLegacyStore(raw) {
if (!raw || typeof raw !== "object") return null;
const record = raw;
if ("profiles" in record) return null;
const entries = {};
for (const [key, value] of Object.entries(record)) {
if (!value || typeof value !== "object") continue;
const typed = value;
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") continue;
entries[key] = {
...typed,
provider: String(typed.provider ?? key)
};
}
return Object.keys(entries).length > 0 ? entries : null;
}
function coerceAuthStore(raw) {
if (!raw || typeof raw !== "object") return null;
const record = raw;
if (!record.profiles || typeof record.profiles !== "object") return null;
const profiles = record.profiles;
const normalized = {};
for (const [key, value] of Object.entries(profiles)) {
if (!value || typeof value !== "object") continue;
const typed = value;
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") continue;
if (!typed.provider) continue;
normalized[key] = typed;
}
const order = record.order && typeof record.order === "object" ? Object.entries(record.order).reduce((acc, [provider, value]) => {
if (!Array.isArray(value)) return acc;
const list = value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
if (list.length === 0) return acc;
acc[provider] = list;
return acc;
}, {}) : void 0;
return {
version: Number(record.version ?? AUTH_STORE_VERSION),
profiles: normalized,
order,
lastGood: record.lastGood && typeof record.lastGood === "object" ? record.lastGood : void 0,
usageStats: record.usageStats && typeof record.usageStats === "object" ? record.usageStats : void 0
};
}
function mergeRecord(base, override) {
if (!base && !override) return;
if (!base) return { ...override };
if (!override) return { ...base };
return {
...base,
...override
};
}
function mergeAuthProfileStores(base, override) {
if (Object.keys(override.profiles).length === 0 && !override.order && !override.lastGood && !override.usageStats) return base;
return {
version: Math.max(base.version, override.version ?? base.version),
profiles: {
...base.profiles,
...override.profiles
},
order: mergeRecord(base.order, override.order),
lastGood: mergeRecord(base.lastGood, override.lastGood),
usageStats: mergeRecord(base.usageStats, override.usageStats)
};
}
function mergeOAuthFileIntoStore(store) {
const oauthRaw = loadJsonFile(resolveOAuthPath());
if (!oauthRaw || typeof oauthRaw !== "object") return false;
const oauthEntries = oauthRaw;
let mutated = false;
for (const [provider, creds] of Object.entries(oauthEntries)) {
if (!creds || typeof creds !== "object") continue;
const profileId = `${provider}:default`;
if (store.profiles[profileId]) continue;
store.profiles[profileId] = {
type: "oauth",
provider,
...creds
};
mutated = true;
}
return mutated;
}
function applyLegacyStore(store, legacy) {
for (const [provider, cred] of Object.entries(legacy)) {
const profileId = `${provider}:default`;
if (cred.type === "api_key") {
store.profiles[profileId] = {
type: "api_key",
provider: String(cred.provider ?? provider),
key: cred.key,
...cred.email ? { email: cred.email } : {}
};
continue;
}
if (cred.type === "token") {
store.profiles[profileId] = {
type: "token",
provider: String(cred.provider ?? provider),
token: cred.token,
...typeof cred.expires === "number" ? { expires: cred.expires } : {},
...cred.email ? { email: cred.email } : {}
};
continue;
}
store.profiles[profileId] = {
type: "oauth",
provider: String(cred.provider ?? provider),
access: cred.access,
refresh: cred.refresh,
expires: cred.expires,
...cred.enterpriseUrl ? { enterpriseUrl: cred.enterpriseUrl } : {},
...cred.projectId ? { projectId: cred.projectId } : {},
...cred.accountId ? { accountId: cred.accountId } : {},
...cred.email ? { email: cred.email } : {}
};
}
}
function loadAuthProfileStore() {
const authPath = resolveAuthStorePath();
const asStore = coerceAuthStore(loadJsonFile(authPath));
if (asStore) {
if (syncExternalCliCredentials(asStore)) saveJsonFile(authPath, asStore);
return asStore;
}
const legacy = coerceLegacyStore(loadJsonFile(resolveLegacyAuthStorePath()));
if (legacy) {
const store = {
version: AUTH_STORE_VERSION,
profiles: {}
};
applyLegacyStore(store, legacy);
syncExternalCliCredentials(store);
return store;
}
const store = {
version: AUTH_STORE_VERSION,
profiles: {}
};
syncExternalCliCredentials(store);
return store;
}
function loadAuthProfileStoreForAgent(agentDir, _options) {
const authPath = resolveAuthStorePath(agentDir);
const asStore = coerceAuthStore(loadJsonFile(authPath));
if (asStore) {
if (syncExternalCliCredentials(asStore)) saveJsonFile(authPath, asStore);
return asStore;
}
if (agentDir) {
const mainStore = coerceAuthStore(loadJsonFile(resolveAuthStorePath()));
if (mainStore && Object.keys(mainStore.profiles).length > 0) {
saveJsonFile(authPath, mainStore);
log$1.info("inherited auth-profiles from main agent", { agentDir });
return mainStore;
}
}
const legacy = coerceLegacyStore(loadJsonFile(resolveLegacyAuthStorePath(agentDir)));
const store = {
version: AUTH_STORE_VERSION,
profiles: {}
};
if (legacy) applyLegacyStore(store, legacy);
const mergedOAuth = mergeOAuthFileIntoStore(store);
const syncedCli = syncExternalCliCredentials(store);
const shouldWrite = legacy !== null || mergedOAuth || syncedCli;
if (shouldWrite) saveJsonFile(authPath, store);
if (shouldWrite && legacy !== null) {
const legacyPath = resolveLegacyAuthStorePath(agentDir);
try {
fs.unlinkSync(legacyPath);
} catch (err) {
if (err?.code !== "ENOENT") log$1.warn("failed to delete legacy auth.json after migration", {
err,
legacyPath
});
}
}
return store;
}
function ensureAuthProfileStore(agentDir, options) {
const store = loadAuthProfileStoreForAgent(agentDir, options);
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (!agentDir || authPath === mainAuthPath) return store;
return mergeAuthProfileStores(loadAuthProfileStoreForAgent(void 0, options), store);
}
function saveAuthProfileStore(store, agentDir) {
saveJsonFile(resolveAuthStorePath(agentDir), {
version: AUTH_STORE_VERSION,
profiles: store.profiles,
order: store.order ?? void 0,
lastGood: store.lastGood ?? void 0,
usageStats: store.usageStats ?? void 0
});
}
//#endregion
//#region src/agents/auth-profiles/profiles.ts
function dedupeProfileIds(profileIds) {
return [...new Set(profileIds)];
}
async function setAuthProfileOrder(params) {
const providerKey = normalizeProviderId(params.provider);
const deduped = dedupeProfileIds(params.order && Array.isArray(params.order) ? params.order.map((entry) => String(entry).trim()).filter(Boolean) : []);
return await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
updater: (store) => {
store.order = store.order ?? {};
if (deduped.length === 0) {
if (!store.order[providerKey]) return false;
delete store.order[providerKey];
if (Object.keys(store.order).length === 0) store.order = void 0;
return true;
}
store.order[providerKey] = deduped;
return true;
}
});
}
function upsertAuthProfile(params) {
const credential = params.credential.type === "api_key" ? {
...params.credential,
...typeof params.credential.key === "string" ? { key: normalizeSecretInput(params.credential.key) } : {}
} : params.credential.type === "token" ? {
...params.credential,
token: normalizeSecretInput(params.credential.token)
} : params.credential;
const store = ensureAuthProfileStore(params.agentDir);
store.profiles[params.profileId] = credential;
saveAuthProfileStore(store, params.agentDir);
}
async function upsertAuthProfileWithLock(params) {
return await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
updater: (store) => {
store.profiles[params.profileId] = params.credential;
return true;
}
});
}
function listProfilesForProvider(store, provider) {
const providerKey = normalizeProviderId(provider);
return Object.entries(store.profiles).filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey).map(([id]) => id);
}
async function markAuthProfileGood(params) {
const { store, provider, profileId, agentDir } = params;
const updated = await updateAuthProfileStoreWithLock({
agentDir,
updater: (freshStore) => {
const profile = freshStore.profiles[profileId];
if (!profile || profile.provider !== provider) return false;
freshStore.lastGood = {
...freshStore.lastGood,
[provider]: profileId
};
return true;
}
});
if (updated) {
store.lastGood = updated.lastGood;
return;
}
const profile = store.profiles[profileId];
if (!profile || profile.provider !== provider) return;
store.lastGood = {
...store.lastGood,
[provider]: profileId
};
saveAuthProfileStore(store, agentDir);
}
//#endregion
//#region src/agents/auth-profiles/repair.ts
function getProfileSuffix(profileId) {
const idx = profileId.indexOf(":");
if (idx < 0) return "";
return profileId.slice(idx + 1);
}
function isEmailLike(value) {
const trimmed = value.trim();
if (!trimmed) return false;
return trimmed.includes("@") && trimmed.includes(".");
}
function suggestOAuthProfileIdForLegacyDefault(params) {
const providerKey = normalizeProviderId(params.provider);
if (getProfileSuffix(params.legacyProfileId) !== "default") return null;
const legacyCfg = params.cfg?.auth?.profiles?.[params.legacyProfileId];
if (legacyCfg && normalizeProviderId(legacyCfg.provider) === providerKey && legacyCfg.mode !== "oauth") return null;
const oauthProfiles = listProfilesForProvider(params.store, providerKey).filter((id) => params.store.profiles[id]?.type === "oauth");
if (oauthProfiles.length === 0) return null;
const configuredEmail = legacyCfg?.email?.trim();
if (configuredEmail) {
const byEmail = oauthProfiles.find((id) => {
const cred = params.store.profiles[id];
if (!cred || cred.type !== "oauth") return false;
return cred.email?.trim() === configuredEmail || id === `${providerKey}:${configuredEmail}`;
});
if (byEmail) return byEmail;
}
const lastGood = params.store.lastGood?.[providerKey] ?? params.store.lastGood?.[params.provider];
if (lastGood && oauthProfiles.includes(lastGood)) return lastGood;
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
if (nonLegacy.length === 1) return nonLegacy[0] ?? null;
const emailLike = nonLegacy.filter((id) => isEmailLike(getProfileSuffix(id)));
if (emailLike.length === 1) return emailLike[0] ?? null;
return null;
}
function repairOAuthProfileIdMismatch(params) {
const legacyProfileId = params.legacyProfileId ?? `${normalizeProviderId(params.provider)}:default`;
const legacyCfg = params.cfg.auth?.profiles?.[legacyProfileId];
if (!legacyCfg) return {
config: params.cfg,
changes: [],
migrated: false
};
if (legacyCfg.mode !== "oauth") return {
config: params.cfg,
changes: [],
migrated: false
};
if (normalizeProviderId(legacyCfg.provider) !== normalizeProviderId(params.provider)) return {
config: params.cfg,
changes: [],
migrated: false
};
const toProfileId = suggestOAuthProfileIdForLegacyDefault({
cfg: params.cfg,
store: params.store,
provider: params.provider,
legacyProfileId
});
if (!toProfileId || toProfileId === legacyProfileId) return {
config: params.cfg,
changes: [],
migrated: false
};
const toCred = params.store.profiles[toProfileId];
const toEmail = toCred?.type === "oauth" ? toCred.email?.trim() : void 0;
const nextProfiles = { ...params.cfg.auth?.profiles };
delete nextProfiles[legacyProfileId];
nextProfiles[toProfileId] = {
...legacyCfg,
...toEmail ? { email: toEmail } : {}
};
const providerKey = normalizeProviderId(params.provider);
const nextOrder = (() => {
const order = params.cfg.auth?.order;
if (!order) return;
const resolvedKey = findNormalizedProviderKey(order, providerKey);
if (!resolvedKey) return order;
const existing = order[resolvedKey];
if (!Array.isArray(existing)) return order;
const deduped = dedupeProfileIds(existing.map((id) => id === legacyProfileId ? toProfileId : id).filter((id) => typeof id === "string" && id.trim().length > 0));
return {
...order,
[resolvedKey]: deduped
};
})();
return {
config: {
...params.cfg,
auth: {
...params.cfg.auth,
profiles: nextProfiles,
...nextOrder ? { order: nextOrder } : {}
}
},
changes: [`Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`],
migrated: true,
fromProfileId: legacyProfileId,
toProfileId
};
}
//#endregion
//#region src/agents/auth-profiles/doctor.ts
function formatAuthDoctorHint(params) {
const providerKey = normalizeProviderId(params.provider);
if (providerKey !== "anthropic") return "";
const legacyProfileId = params.profileId ?? "anthropic:default";
const suggested = suggestOAuthProfileIdForLegacyDefault({
cfg: params.cfg,
store: params.store,
provider: providerKey,
legacyProfileId
});
if (!suggested || suggested === legacyProfileId) return "";
const storeOauthProfiles = listProfilesForProvider(params.store, providerKey).filter((id) => params.store.profiles[id]?.type === "oauth").join(", ");
const cfgMode = params.cfg?.auth?.profiles?.[legacyProfileId]?.mode;
const cfgProvider = params.cfg?.auth?.profiles?.[legacyProfileId]?.provider;
return [
"Doctor hint (for GitHub issue):",
`- provider: ${providerKey}`,
`- config: ${legacyProfileId}${cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : ""}`,
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
`- suggested profile: ${suggested}`,
`Fix: run "${formatCliCommand("openclaw doctor --yes")}"`
].join("\n");
}
//#endregion
//#region src/providers/qwen-portal-oauth.ts
const QWEN_OAUTH_TOKEN_ENDPOINT = `https://chat.qwen.ai/api/v1/oauth2/token`;
const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
async function refreshQwenPortalCredentials(credentials) {
if (!credentials.refresh?.trim()) throw new Error("Qwen OAuth refresh token missing; re-authenticate.");
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: credentials.refresh,
client_id: QWEN_OAUTH_CLIENT_ID
})
});
if (!response.ok) {
const text = await response.text();
if (response.status === 400) throw new Error(`Qwen OAuth refresh token expired or invalid. Re-authenticate with \`${formatCliCommand("openclaw models auth login --provider qwen-portal")}\`.`);
throw new Error(`Qwen OAuth refresh failed: ${text || response.statusText}`);
}
const payload = await response.json();
if (!payload.access_token || !payload.expires_in) throw new Error("Qwen OAuth refresh response missing access token.");
return {
...credentials,
access: payload.access_token,
refresh: payload.refresh_token || credentials.refresh,
expires: Date.now() + payload.expires_in * 1e3
};
}
//#endregion
//#region src/agents/chutes-oauth.ts
const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai";
const CHUTES_AUTHORIZE_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/authorize`;
const CHUTES_TOKEN_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/token`;
const CHUTES_USERINFO_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/userinfo`;
const DEFAULT_EXPIRES_BUFFER_MS = 300 * 1e3;
function generateChutesPkce() {
const verifier = randomBytes(32).toString("hex");
return {
verifier,
challenge: createHash("sha256").update(verifier).digest("base64url")
};
}
function parseOAuthCallbackInput(input, expectedState) {
const trimmed = input.trim();
if (!trimmed) return { error: "No input provided" };
let url;
try {
url = new URL(trimmed);
} catch {
if (!/\s/.test(trimmed) && !trimmed.includes("://") && !trimmed.includes("?") && !trimmed.includes("=")) return { error: "Paste the full redirect URL (must include code + state)." };
const qs = trimmed.startsWith("?") ? trimmed : `?${trimmed}`;
try {
url = new URL(`http://localhost/${qs}`);
} catch {
return { error: "Paste the full redirect URL (must include code + state)." };
}
}
const code = url.searchParams.get("code")?.trim();
const state = url.searchParams.get("state")?.trim();
if (!code) return { error: "Missing 'code' parameter in URL" };
if (!state) return { error: "Missing 'state' parameter. Paste the full redirect URL." };
if (state !== expectedState) return { error: "OAuth state mismatch - possible CSRF attack. Please retry login." };
return {
code,
state
};
}
function coerceExpiresAt(expiresInSeconds, now) {
const value = now + Math.max(0, Math.floor(expiresInSeconds)) * 1e3 - DEFAULT_EXPIRES_BUFFER_MS;
return Math.max(value, now + 3e4);
}
async function fetchChutesUserInfo(params) {
const response = await (params.fetchFn ?? fetch)(CHUTES_USERINFO_ENDPOINT, { headers: { Authorization: `Bearer ${params.accessToken}` } });
if (!response.ok) return null;
const data = await response.json();
if (!data || typeof data !== "object") return null;
return data;
}
async function exchangeChutesCodeForTokens(params) {
const fetchFn = params.fetchFn ?? fetch;
const now = params.now ?? Date.now();
const body = new URLSearchParams({
grant_type: "authorization_code",
client_id: params.app.clientId,
code: params.code,
redirect_uri: params.app.redirectUri,
code_verifier: params.codeVerifier
});
if (params.app.clientSecret) body.set("client_secret", params.app.clientSecret);
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Chutes token exchange failed: ${text}`);
}
const data = await response.json();
const access = data.access_token?.trim();
const refresh = data.refresh_token?.trim();
const expiresIn = data.expires_in ?? 0;
if (!access) throw new Error("Chutes token exchange returned no access_token");
if (!refresh) throw new Error("Chutes token exchange returned no refresh_token");
const info = await fetchChutesUserInfo({
accessToken: access,
fetchFn
});
return {
access,
refresh,
expires: coerceExpiresAt(expiresIn, now),
email: info?.username,
accountId: info?.sub,
clientId: params.app.clientId
};
}
async function refreshChutesTokens(params) {
const fetchFn = params.fetchFn ?? fetch;
const now = params.now ?? Date.now();
const refreshToken = params.credential.refresh?.trim();
if (!refreshToken) throw new Error("Chutes OAuth credential is missing refresh token");
const clientId = params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim();
if (!clientId) throw new Error("Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).");
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || void 0;
const body = new URLSearchParams({
grant_type: "refresh_token",
client_id: clientId,
refresh_token: refreshToken
});
if (clientSecret) body.set("client_secret", clientSecret);
const response = await fetchFn(CHUTES_TOKEN_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Chutes token refresh failed: ${text}`);
}
const data = await response.json();
const access = data.access_token?.trim();
const newRefresh = data.refresh_token?.trim();
const expiresIn = data.expires_in ?? 0;
if (!access) throw new Error("Chutes token refresh returned no access_token");
return {
...params.credential,
access,
refresh: newRefresh || refreshToken,
expires: coerceExpiresAt(expiresIn, now),
clientId
};
}
//#endregion
//#region src/agents/auth-profiles/oauth.ts
const OAUTH_PROVIDER_IDS = new Set(getOAuthProviders().map((provider) => provider.id));
const isOAuthProvider = (provider) => OAUTH_PROVIDER_IDS.has(provider);
const resolveOAuthProvider = (provider) => isOAuthProvider(provider) ? provider : null;
/** Bearer-token auth modes that are interchangeable (oauth tokens and raw tokens). */
const BEARER_AUTH_MODES = new Set(["oauth", "token"]);
const isCompatibleModeType = (mode, type) => {
if (!mode || !type) return false;
if (mode === type) return true;
return BEARER_AUTH_MODES.has(mode) && BEARER_AUTH_MODES.has(type);
};
function isProfileConfigCompatible(params) {
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
if (profileConfig && profileConfig.provider !== params.provider) return false;
if (profileConfig && !isCompatibleModeType(profileConfig.mode, params.mode)) return false;
return true;
}
function buildOAuthApiKey(provider, credentials) {
return provider === "google-gemini-cli" || provider === "google-antigravity" ? JSON.stringify({
token: credentials.access,
projectId: credentials.projectId
}) : credentials.access;
}
function buildApiKeyProfileResult(params) {
return {
apiKey: params.apiKey,
provider: params.provider,
email: params.email
};
}
function buildOAuthProfileResult(params) {
return buildApiKeyProfileResult({
apiKey: buildOAuthApiKey(params.provider, params.credentials),
provider: params.provider,
email: params.email
});
}
function isExpiredCredential(expires) {
return typeof expires === "number" && Number.isFinite(expires) && expires > 0 && Date.now() >= expires;
}
function adoptNewerMainOAuthCredential(params) {
if (!params.agentDir) return null;
try {
const mainCred = ensureAuthProfileStore(void 0).profiles[params.profileId];
if (mainCred?.type === "oauth" && mainCred.provider === params.cred.provider && Number.isFinite(mainCred.expires) && (!Number.isFinite(params.cred.expires) || mainCred.expires > params.cred.expires)) {
params.store.profiles[params.profileId] = { ...mainCred };
saveAuthProfileStore(params.store, params.agentDir);
log$1.info("adopted newer OAuth credentials from main agent", {
profileId: params.profileId,
agentDir: params.agentDir,
expires: new Date(mainCred.expires).toISOString()
});
return mainCred;
}
} catch (err) {
log$1.debug("adoptNewerMainOAuthCredential failed", {
profileId: params.profileId,
error: err instanceof Error ? err.message : String(err)
});
}
return null;
}
async function refreshOAuthTokenWithLock(params) {
const authPath = resolveAuthStorePath(params.agentDir);
ensureAuthStoreFile(authPath);
return await withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
const store = ensureAuthProfileStore(params.agentDir);
const cred = store.profiles[params.profileId];
if (!cred || cred.type !== "oauth") return null;
if (Date.now() < cred.expires) return {
apiKey: buildOAuthApiKey(cred.provider, cred),
newCredentials: cred
};
const oauthCreds = { [cred.provider]: cred };
const result = String(cred.provider) === "chutes" ? await (async () => {
const newCredentials = await refreshChutesTokens({ credential: cred });
return {
apiKey: newCredentials.access,
newCredentials
};
})() : String(cred.provider) === "qwen-portal" ? await (async () => {
const newCredentials = await refreshQwenPortalCredentials(cred);
return {
apiKey: newCredentials.access,
newCredentials
};
})() : await (async () => {
const oauthProvider = resolveOAuthProvider(cred.provider);
if (!oauthProvider) return null;
return await getOAuthApiKey(oauthProvider, oauthCreds);
})();
if (!result) return null;
store.profiles[params.profileId] = {
...cred,
...result.newCredentials,
type: "oauth"
};
saveAuthProfileStore(store, params.agentDir);
return result;
});
}
async function tryResolveOAuthProfile(params) {
const { cfg, store, profileId } = params;
const cred = store.profiles[profileId];
if (!cred || cred.type !== "oauth") return null;
if (!isProfileConfigCompatible({
cfg,
profileId,
provider: cred.provider,
mode: cred.type
})) return null;
if (Date.now() < cred.expires) return buildOAuthProfileResult({
provider: cred.provider,
credentials: cred,
email: cred.email
});
const refreshed = await refreshOAuthTokenWithLock({
profileId,
agentDir: params.agentDir
});
if (!refreshed) return null;
return buildApiKeyProfileResult({
apiKey: refreshed.apiKey,
provider: cred.provider,
email: cred.email
});
}
async function resolveApiKeyForProfile(params) {
const { cfg, store, profileId } = params;
const cred = store.profiles[profileId];
if (!cred) return null;
if (!isProfileConfigCompatible({
cfg,
profileId,
provider: cred.provider,
mode: cred.type,
allowOAuthTokenCompatibility: true
})) return null;
if (cred.type === "api_key") {
const key = cred.key?.trim();
if (!key) return null;
return buildApiKeyProfileResult({
apiKey: key,
provider: cred.provider,
email: cred.email
});
}
if (cred.type === "token") {
const token = cred.token?.trim();
if (!token) return null;
if (isExpiredCredential(cred.expires)) return null;
return buildApiKeyProfileResult({
apiKey: token,
provider: cred.provider,
email: cred.email
});
}
const oauthCred = adoptNewerMainOAuthCredential({
store,
profileId,
agentDir: params.agentDir,
cred
}) ?? cred;
if (Date.now() < oauthCred.expires) return buildOAuthProfileResult({
provider: oauthCred.provider,
credentials: oauthCred,
email: oauthCred.email
});
try {
const result = await refreshOAuthTokenWithLock({
profileId,
agentDir: params.agentDir
});
if (!result) return null;
return buildApiKeyProfileResult({
apiKey: result.apiKey,
provider: cred.provider,
email: cred.email
});
} catch (error) {
const refreshedStore = ensureAuthProfileStore(params.agentDir);
const refreshed = refreshedStore.profiles[profileId];
if (refreshed?.type === "oauth" && Date.now() < refreshed.expires) return buildOAuthProfileResult({
provider: refreshed.provider,
credentials: refreshed,
email: refreshed.email ?? cred.email
});
const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({
cfg,
store: refreshedStore,
provider: cred.provider,
legacyProfileId: profileId
});
if (fallbackProfileId && fallbackProfileId !== profileId) try {
const fallbackResolved = await tryResolveOAuthProfile({
cfg,
store: refreshedStore,
profileId: fallbackProfileId,
agentDir: params.agentDir
});
if (fallbackResolved) return fallbackResolved;
} catch {}
if (params.agentDir) try {
const mainCred = ensureAuthProfileStore(void 0).profiles[profileId];
if (mainCred?.type === "oauth" && Date.now() < mainCred.expires) {
refreshedStore.profiles[profileId] = { ...mainCred };
saveAuthProfileStore(refreshedStore, params.agentDir);
log$1.info("inherited fresh OAuth credentials from main agent", {
profileId,
agentDir: params.agentDir,
expires: new Date(mainCred.expires).toISOString()
});
return buildOAuthProfileResult({
provider: mainCred.provider,
credentials: mainCred,
email: mainCred.email
});
}
} catch {}
const message = error instanceof Error ? error.message : String(error);
const hint = formatAuthDoctorHint({
cfg,
store: refreshedStore,
provider: cred.provider,
profileId
});
throw new Error(`OAuth token refresh failed for ${cred.provider}: ${message}. Please try again or re-authenticate.` + (hint ? `\n\n${hint}` : ""), { cause: error });
}
}
//#endregion
//#region src/agents/auth-profiles/usage.ts
function resolveProfileUnusableUntil(stats) {
const values = [stats.cooldownUntil, stats.disabledUntil].filter((value) => typeof value === "number").filter((value) => Number.isFinite(value) && value > 0);
if (values.length === 0) return null;
return Math.max(...values);
}
/**
* Check if a profile is currently in cooldown (due to rate limiting or errors).
*/
function isProfileInCooldown(store, profileId) {
const stats = store.usageStats?.[profileId];
if (!stats) return false;
const unusableUntil = resolveProfileUnusableUntil(stats);
return unusableUntil ? Date.now() < unusableUntil : false;
}
/**
* Return the soonest `unusableUntil` timestamp (ms epoch) among the given
* profiles, or `null` when no profile has a recorded cooldown. Note: the
* returned timestamp may be in the past if the cooldown has already expired.
*/
function getSoonestCooldownExpiry(store, profileIds) {
let soonest = null;
for (const id of profileIds) {
const stats = store.usageStats?.[id];
if (!stats) continue;
const until = resolveProfileUnusableUntil(stats);
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) continue;
if (soonest === null || until < soonest) soonest = until;
}
return soonest;
}
/**
* Clear expired cooldowns from all profiles in the store.
*
* When `cooldownUntil` or `disabledUntil` has passed, the corresponding fields
* are removed and error counters are reset so the profile gets a fresh start
* (circuit-breaker half-open → closed). Without this, a stale `errorCount`
* causes the *next* transient failure to immediately escalate to a much longer
* cooldown — the root cause of profiles appearing "stuck" after rate limits.
*
* `cooldownUntil` and `disabledUntil` are handled independently: if a profile
* has both and only one has expired, only that field is cleared.
*
* Mutates the in-memory store; disk persistence happens lazily on the next
* store write (e.g. `markAuthProfileUsed` / `markAuthProfileFailure`), which
* matches the existing save pattern throughout the auth-profiles module.
*
* @returns `true` if any profile was modified.
*/
function clearExpiredCooldowns(store, now) {
const usageStats = store.usageStats;
if (!usageStats) return false;
const ts = now ?? Date.now();
let mutated = false;
for (const [profileId, stats] of Object.entries(usageStats)) {
if (!stats) continue;
let profileMutated = false;
const cooldownExpired = typeof stats.cooldownUntil === "number" && Number.isFinite(stats.cooldownUntil) && stats.cooldownUntil > 0 && ts >= stats.cooldownUntil;
const disabledExpired = typeof stats.disabledUntil === "number" && Number.isFinite(stats.disabledUntil) && stats.disabledUntil > 0 && ts >= stats.disabledUntil;
if (cooldownExpired) {
stats.cooldownUntil = void 0;
profileMutated = true;
}
if (disabledExpired) {
stats.disabledUntil = void 0;
stats.disabledReason = void 0;
profileMutated = true;
}
if (profileMutated && !resolveProfileUnusableUntil(stats)) {
stats.errorCount = 0;
stats.failureCounts = void 0;
}
if (profileMutated) {
usageStats[profileId] = stats;
mutated = true;
}
}
return mutated;
}
/**
* Mark a profile as successfully used. Resets error count and updates lastUsed.
* Uses store lock to avoid overwriting concurrent usage updates.
*/
async function markAuthProfileUsed(params) {
const { store, profileId, agentDir } = params;
const updated = await updateAuthProfileStoreWithLock({
agentDir,
updater: (freshStore) => {
if (!freshStore.profiles[profileId]) return false;
freshStore.usageStats = freshStore.usageStats ?? {};
freshStore.usageStats[profileId] = {
...freshStore.usageStats[profileId],
lastUsed: Date.now(),
errorCount: 0,
cooldownUntil: void 0,
disabledUntil: void 0,
disabledReason: void 0,
failureCounts: void 0
};
return true;
}
});
if (updated) {
store.usageStats = updated.usageStats;
return;
}
if (!store.profiles[profileId]) return;
store.usageStats = store.usageStats ?? {};
store.usageStats[profileId] = {
...store.usageStats[profileId],
lastUsed: Date.now(),
errorCount: 0,
cooldownUntil: void 0,
disabledUntil: void 0,
disabledReason: void 0,
failureCounts: void 0
};
saveAuthProfileStore(store, agentDir);
}
function calculateAuthProfileCooldownMs(errorCount) {
const normalized = Math.max(1, errorCount);
return Math.min(3600 * 1e3, 60 * 1e3 * 5 ** Math.min(normalized - 1, 3));
}
function resolveAuthCooldownConfig(params) {
const defaults = {
billingBackoffHours: 5,
billingMaxHours: 24,
failureWindowHours: 24
};
const resolveHours = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
const cooldowns = params.cfg?.auth?.cooldowns;
const billingBackoffHours = resolveHours((() => {
const map = cooldowns?.billingBackoffHoursByProvider;
if (!map) return;
for (const [key, value] of Object.entries(map)) if (normalizeProviderId(key) === params.providerId) return value;
})() ?? cooldowns?.billingBackoffHours, defaults.billingBackoffHours);
const billingMaxHours = resolveHours(cooldowns?.billingMaxHours, defaults.billingMaxHours);
const failureWindowHours = resolveHours(cooldowns?.failureWindowHours, defaults.failureWindowHours);
return {
billingBackoffMs: billingBackoffHours * 60 * 60 * 1e3,
billingMaxMs: billingMaxHours * 60 * 60 * 1e3,
failureWindowMs: failureWindowHours * 60 * 60 * 1e3
};
}
function calculateAuthProfileBillingDisableMsWithConfig(params) {
const normalized = Math.max(1, params.errorCount);
const baseMs = Math.max(6e4, params.baseMs);
const maxMs = Math.max(baseMs, params.maxMs);
const raw = baseMs * 2 ** Math.min(normalized - 1, 10);
return Math.min(maxMs, raw);
}
function resolveProfileUnusableUntilForDisplay(store, profileId) {
const stats = store.usageStats?.[profileId];
if (!stats) return null;
return resolveProfileUnusableUntil(stats);
}
function computeNextProfileUsageStats(params) {
const windowMs = params.cfgResolved.failureWindowMs;
const windowExpired = typeof params.existing.lastFailureAt === "number" && params.existing.lastFailureAt > 0 && params.now - params.existing.lastFailureAt > windowMs;
const nextErrorCount = (windowExpired ? 0 : params.existing.errorCount ?? 0) + 1;
const failureCounts = windowExpired ? {} : { ...params.existing.failureCounts };
failureCounts[params.reason] = (failureCounts[params.reason] ?? 0) + 1;
const updatedStats = {
...params.existing,
errorCount: nextErrorCount,
failureCounts,
lastFailureAt: params.now
};
if (params.reason === "billing") {
const backoffMs = calculateAuthProfileBillingDisableMsWithConfig({
errorCount: failureCounts.billing ?? 1,
baseMs: params.cfgResolved.billingBackoffMs,
maxMs: params.cfgResolved.billingMaxMs
});
updatedStats.disabledUntil = params.now + backoffMs;
updatedStats.disabledReason = "billing";
} else {
const backoffMs = calculateAuthProfileCooldownMs(nextErrorCount);
updatedStats.cooldownUntil = params.now + backoffMs;
}
return updatedStats;
}
/**
* Mark a profile as failed for a specific reason. Billing failures are treated
* as "disabled" (longer backoff) vs the regular cooldown window.
*/
async function markAuthProfileFailure(params) {
const { store, profileId, reason, agentDir, cfg } = params;
const updated = await updateAuthProfileStoreWithLock({
agentDir,
updater: (freshStore) => {
const profile = freshStore.profiles[profileId];
if (!profile) return false;
freshStore.usageStats = freshStore.usageStats ?? {};
const existing = freshStore.usageStats[profileId] ?? {};
const now = Date.now();
const cfgResolved = resolveAuthCooldownConfig({
cfg,
providerId: normalizeProviderId(profile.provider)
});
freshStore.usageStats[profileId] = computeNextProfileUsageStats({
existing,
now,
reason,
cfgResolved
});
return true;
}
});
if (updated) {
store.usageStats = updated.usageStats;
return;
}
if (!store.profiles[profileId]) return;
store.usageStats = store.usageStats ?? {};
const existing = store.usageStats[profileId] ?? {};
const now = Date.now();
const cfgResolved = resolveAuthCooldownConfig({
cfg,
providerId: normalizeProviderId(store.profiles[profileId]?.provider ?? "")
});
store.usageStats[profileId] = computeNextProfileUsa