223 lines
7.7 KiB
TypeScript
223 lines
7.7 KiB
TypeScript
import {
|
|
isProfileInCooldown,
|
|
resolveAuthProfileDisplayLabel,
|
|
resolveAuthStorePathForDisplay,
|
|
} from "../../agents/auth-profiles.js";
|
|
import {
|
|
ensureAuthProfileStore,
|
|
getCustomProviderApiKey,
|
|
resolveAuthProfileOrder,
|
|
resolveEnvApiKey,
|
|
} from "../../agents/model-auth.js";
|
|
import { normalizeProviderId } from "../../agents/model-selection.js";
|
|
import type { ClawdbotConfig } from "../../config/config.js";
|
|
import { shortenHomePath } from "../../utils.js";
|
|
|
|
export type ModelAuthDetailMode = "compact" | "verbose";
|
|
|
|
const maskApiKey = (value: string): string => {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) return "missing";
|
|
if (trimmed.length <= 16) return trimmed;
|
|
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
|
|
};
|
|
|
|
export const resolveAuthLabel = async (
|
|
provider: string,
|
|
cfg: ClawdbotConfig,
|
|
modelsPath: string,
|
|
agentDir?: string,
|
|
mode: ModelAuthDetailMode = "compact",
|
|
): Promise<{ label: string; source: string }> => {
|
|
const formatPath = (value: string) => shortenHomePath(value);
|
|
const store = ensureAuthProfileStore(agentDir, {
|
|
allowKeychainPrompt: false,
|
|
});
|
|
const order = resolveAuthProfileOrder({ cfg, store, provider });
|
|
const providerKey = normalizeProviderId(provider);
|
|
const lastGood = (() => {
|
|
const map = store.lastGood;
|
|
if (!map) return undefined;
|
|
for (const [key, value] of Object.entries(map)) {
|
|
if (normalizeProviderId(key) === providerKey) return value;
|
|
}
|
|
return undefined;
|
|
})();
|
|
const nextProfileId = order[0];
|
|
const now = Date.now();
|
|
|
|
const formatUntil = (timestampMs: number) => {
|
|
const remainingMs = Math.max(0, timestampMs - now);
|
|
const minutes = Math.round(remainingMs / 60_000);
|
|
if (minutes < 1) return "soon";
|
|
if (minutes < 60) return `${minutes}m`;
|
|
const hours = Math.round(minutes / 60);
|
|
if (hours < 48) return `${hours}h`;
|
|
const days = Math.round(hours / 24);
|
|
return `${days}d`;
|
|
};
|
|
|
|
if (order.length > 0) {
|
|
if (mode === "compact") {
|
|
const profileId = nextProfileId;
|
|
if (!profileId) return { label: "missing", source: "missing" };
|
|
const profile = store.profiles[profileId];
|
|
const configProfile = cfg.auth?.profiles?.[profileId];
|
|
const missing =
|
|
!profile ||
|
|
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
|
(configProfile?.mode &&
|
|
configProfile.mode !== profile.type &&
|
|
!(configProfile.mode === "oauth" && profile.type === "token"));
|
|
|
|
const more = order.length > 1 ? ` (+${order.length - 1})` : "";
|
|
if (missing) return { label: `${profileId} missing${more}`, source: "" };
|
|
|
|
if (profile.type === "api_key") {
|
|
return {
|
|
label: `${profileId} api-key ${maskApiKey(profile.key)}${more}`,
|
|
source: "",
|
|
};
|
|
}
|
|
if (profile.type === "token") {
|
|
const exp =
|
|
typeof profile.expires === "number" &&
|
|
Number.isFinite(profile.expires) &&
|
|
profile.expires > 0
|
|
? profile.expires <= now
|
|
? " expired"
|
|
: ` exp ${formatUntil(profile.expires)}`
|
|
: "";
|
|
return {
|
|
label: `${profileId} token ${maskApiKey(profile.token)}${exp}${more}`,
|
|
source: "",
|
|
};
|
|
}
|
|
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
|
|
const label = display === profileId ? profileId : display;
|
|
const exp =
|
|
typeof profile.expires === "number" &&
|
|
Number.isFinite(profile.expires) &&
|
|
profile.expires > 0
|
|
? profile.expires <= now
|
|
? " expired"
|
|
: ` exp ${formatUntil(profile.expires)}`
|
|
: "";
|
|
return { label: `${label} oauth${exp}${more}`, source: "" };
|
|
}
|
|
|
|
const labels = order.map((profileId) => {
|
|
const profile = store.profiles[profileId];
|
|
const configProfile = cfg.auth?.profiles?.[profileId];
|
|
const flags: string[] = [];
|
|
if (profileId === nextProfileId) flags.push("next");
|
|
if (lastGood && profileId === lastGood) flags.push("lastGood");
|
|
if (isProfileInCooldown(store, profileId)) {
|
|
const until = store.usageStats?.[profileId]?.cooldownUntil;
|
|
if (typeof until === "number" && Number.isFinite(until) && until > now) {
|
|
flags.push(`cooldown ${formatUntil(until)}`);
|
|
} else {
|
|
flags.push("cooldown");
|
|
}
|
|
}
|
|
if (
|
|
!profile ||
|
|
(configProfile?.provider && configProfile.provider !== profile.provider) ||
|
|
(configProfile?.mode &&
|
|
configProfile.mode !== profile.type &&
|
|
!(configProfile.mode === "oauth" && profile.type === "token"))
|
|
) {
|
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
return `${profileId}=missing${suffix}`;
|
|
}
|
|
if (profile.type === "api_key") {
|
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
return `${profileId}=${maskApiKey(profile.key)}${suffix}`;
|
|
}
|
|
if (profile.type === "token") {
|
|
if (
|
|
typeof profile.expires === "number" &&
|
|
Number.isFinite(profile.expires) &&
|
|
profile.expires > 0
|
|
) {
|
|
flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`);
|
|
}
|
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
return `${profileId}=token:${maskApiKey(profile.token)}${suffix}`;
|
|
}
|
|
const display = resolveAuthProfileDisplayLabel({
|
|
cfg,
|
|
store,
|
|
profileId,
|
|
});
|
|
const suffix =
|
|
display === profileId
|
|
? ""
|
|
: display.startsWith(profileId)
|
|
? display.slice(profileId.length).trim()
|
|
: `(${display})`;
|
|
if (
|
|
typeof profile.expires === "number" &&
|
|
Number.isFinite(profile.expires) &&
|
|
profile.expires > 0
|
|
) {
|
|
flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`);
|
|
}
|
|
const suffixLabel = suffix ? ` ${suffix}` : "";
|
|
const suffixFlags = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
return `${profileId}=OAuth${suffixLabel}${suffixFlags}`;
|
|
});
|
|
return {
|
|
label: labels.join(", "),
|
|
source: `auth-profiles.json: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
|
|
};
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey(provider);
|
|
if (envKey) {
|
|
const isOAuthEnv =
|
|
envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") ||
|
|
envKey.source.toLowerCase().includes("oauth");
|
|
const label = isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey);
|
|
return { label, source: mode === "verbose" ? envKey.source : "" };
|
|
}
|
|
const customKey = getCustomProviderApiKey(cfg, provider);
|
|
if (customKey) {
|
|
return {
|
|
label: maskApiKey(customKey),
|
|
source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "",
|
|
};
|
|
}
|
|
return { label: "missing", source: "missing" };
|
|
};
|
|
|
|
export const formatAuthLabel = (auth: { label: string; source: string }) => {
|
|
if (!auth.source || auth.source === auth.label || auth.source === "missing") {
|
|
return auth.label;
|
|
}
|
|
return `${auth.label} (${auth.source})`;
|
|
};
|
|
|
|
export const resolveProfileOverride = (params: {
|
|
rawProfile?: string;
|
|
provider: string;
|
|
cfg: ClawdbotConfig;
|
|
agentDir?: string;
|
|
}): { profileId?: string; error?: string } => {
|
|
const raw = params.rawProfile?.trim();
|
|
if (!raw) return {};
|
|
const store = ensureAuthProfileStore(params.agentDir, {
|
|
allowKeychainPrompt: false,
|
|
});
|
|
const profile = store.profiles[raw];
|
|
if (!profile) {
|
|
return { error: `Auth profile "${raw}" not found.` };
|
|
}
|
|
if (profile.provider !== params.provider) {
|
|
return {
|
|
error: `Auth profile "${raw}" is for ${profile.provider}, not ${params.provider}.`,
|
|
};
|
|
}
|
|
return { profileId: raw };
|
|
};
|