UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,382 lines (1,363 loc) 134 kB
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