mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 23:04:32 +00:00
feat(models): add per-agent auth order overrides
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||||
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
||||||
- Commands: accept /models as an alias for /model.
|
- Commands: accept /models as an alias for /model.
|
||||||
|
- Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete
|
||||||
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
||||||
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
||||||
- CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured.
|
- CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured.
|
||||||
|
|||||||
@@ -130,6 +130,39 @@ describe("resolveAuthProfileOrder", () => {
|
|||||||
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
|
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers store order over config order", () => {
|
||||||
|
const order = resolveAuthProfileOrder({
|
||||||
|
cfg: {
|
||||||
|
auth: {
|
||||||
|
order: { anthropic: ["anthropic:default", "anthropic:work"] },
|
||||||
|
profiles: cfg.auth.profiles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
...store,
|
||||||
|
order: { anthropic: ["anthropic:work", "anthropic:default"] },
|
||||||
|
},
|
||||||
|
provider: "anthropic",
|
||||||
|
});
|
||||||
|
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pushes cooldown profiles to the end even with store order", () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const order = resolveAuthProfileOrder({
|
||||||
|
store: {
|
||||||
|
...store,
|
||||||
|
order: { anthropic: ["anthropic:default", "anthropic:work"] },
|
||||||
|
usageStats: {
|
||||||
|
"anthropic:default": { cooldownUntil: now + 60_000 },
|
||||||
|
"anthropic:work": { lastUsed: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: "anthropic",
|
||||||
|
});
|
||||||
|
expect(order).toEqual(["anthropic:work", "anthropic:default"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("pushes cooldown profiles to the end even with configured order", () => {
|
it("pushes cooldown profiles to the end even with configured order", () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const order = resolveAuthProfileOrder({
|
const order = resolveAuthProfileOrder({
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ export type ProfileUsageStats = {
|
|||||||
export type AuthProfileStore = {
|
export type AuthProfileStore = {
|
||||||
version: number;
|
version: number;
|
||||||
profiles: Record<string, AuthProfileCredential>;
|
profiles: Record<string, AuthProfileCredential>;
|
||||||
|
/**
|
||||||
|
* Optional per-agent preferred profile order overrides.
|
||||||
|
* This lets you lock/override auth rotation for a specific agent without
|
||||||
|
* changing the global config.
|
||||||
|
*/
|
||||||
|
order?: Record<string, string[]>;
|
||||||
lastGood?: Record<string, string>;
|
lastGood?: Record<string, string>;
|
||||||
/** Usage statistics per profile for round-robin rotation */
|
/** Usage statistics per profile for round-robin rotation */
|
||||||
usageStats?: Record<string, ProfileUsageStats>;
|
usageStats?: Record<string, ProfileUsageStats>;
|
||||||
@@ -133,6 +139,7 @@ function syncAuthProfileStore(
|
|||||||
): void {
|
): void {
|
||||||
target.version = source.version;
|
target.version = source.version;
|
||||||
target.profiles = source.profiles;
|
target.profiles = source.profiles;
|
||||||
|
target.order = source.order;
|
||||||
target.lastGood = source.lastGood;
|
target.lastGood = source.lastGood;
|
||||||
target.usageStats = source.usageStats;
|
target.usageStats = source.usageStats;
|
||||||
}
|
}
|
||||||
@@ -270,9 +277,25 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
|||||||
if (!typed.provider) continue;
|
if (!typed.provider) continue;
|
||||||
normalized[key] = typed as AuthProfileCredential;
|
normalized[key] = typed as AuthProfileCredential;
|
||||||
}
|
}
|
||||||
|
const order =
|
||||||
|
record.order && typeof record.order === "object"
|
||||||
|
? Object.entries(record.order as Record<string, unknown>).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;
|
||||||
|
},
|
||||||
|
{} as Record<string, string[]>,
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
return {
|
return {
|
||||||
version: Number(record.version ?? AUTH_STORE_VERSION),
|
version: Number(record.version ?? AUTH_STORE_VERSION),
|
||||||
profiles: normalized,
|
profiles: normalized,
|
||||||
|
order,
|
||||||
lastGood:
|
lastGood:
|
||||||
record.lastGood && typeof record.lastGood === "object"
|
record.lastGood && typeof record.lastGood === "object"
|
||||||
? (record.lastGood as Record<string, string>)
|
? (record.lastGood as Record<string, string>)
|
||||||
@@ -680,12 +703,49 @@ export function saveAuthProfileStore(
|
|||||||
const payload = {
|
const payload = {
|
||||||
version: AUTH_STORE_VERSION,
|
version: AUTH_STORE_VERSION,
|
||||||
profiles: store.profiles,
|
profiles: store.profiles,
|
||||||
|
order: store.order ?? undefined,
|
||||||
lastGood: store.lastGood ?? undefined,
|
lastGood: store.lastGood ?? undefined,
|
||||||
usageStats: store.usageStats ?? undefined,
|
usageStats: store.usageStats ?? undefined,
|
||||||
} satisfies AuthProfileStore;
|
} satisfies AuthProfileStore;
|
||||||
saveJsonFile(authPath, payload);
|
saveJsonFile(authPath, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setAuthProfileOrder(params: {
|
||||||
|
agentDir?: string;
|
||||||
|
provider: string;
|
||||||
|
order?: string[] | null;
|
||||||
|
}): Promise<AuthProfileStore | null> {
|
||||||
|
const providerKey = normalizeProviderId(params.provider);
|
||||||
|
const sanitized =
|
||||||
|
params.order && Array.isArray(params.order)
|
||||||
|
? params.order
|
||||||
|
.map((entry) => String(entry).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const deduped: string[] = [];
|
||||||
|
for (const entry of sanitized) {
|
||||||
|
if (!deduped.includes(entry)) deduped.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
store.order[providerKey] = deduped;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function upsertAuthProfile(params: {
|
export function upsertAuthProfile(params: {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
credential: AuthProfileCredential;
|
credential: AuthProfileCredential;
|
||||||
@@ -863,6 +923,14 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
}): string[] {
|
}): string[] {
|
||||||
const { cfg, store, provider, preferredProfile } = params;
|
const { cfg, store, provider, preferredProfile } = params;
|
||||||
const providerKey = normalizeProviderId(provider);
|
const providerKey = normalizeProviderId(provider);
|
||||||
|
const storedOrder = (() => {
|
||||||
|
const order = store.order;
|
||||||
|
if (!order) return undefined;
|
||||||
|
for (const [key, value] of Object.entries(order)) {
|
||||||
|
if (normalizeProviderId(key) === providerKey) return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
const configuredOrder = (() => {
|
const configuredOrder = (() => {
|
||||||
const order = cfg?.auth?.order;
|
const order = cfg?.auth?.order;
|
||||||
if (!order) return undefined;
|
if (!order) return undefined;
|
||||||
@@ -871,6 +939,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})();
|
})();
|
||||||
|
const explicitOrder = storedOrder ?? configuredOrder;
|
||||||
const explicitProfiles = cfg?.auth?.profiles
|
const explicitProfiles = cfg?.auth?.profiles
|
||||||
? Object.entries(cfg.auth.profiles)
|
? Object.entries(cfg.auth.profiles)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -880,7 +949,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
.map(([profileId]) => profileId)
|
.map(([profileId]) => profileId)
|
||||||
: [];
|
: [];
|
||||||
const baseOrder =
|
const baseOrder =
|
||||||
configuredOrder ??
|
explicitOrder ??
|
||||||
(explicitProfiles.length > 0
|
(explicitProfiles.length > 0
|
||||||
? explicitProfiles
|
? explicitProfiles
|
||||||
: listProfilesForProvider(store, providerKey));
|
: listProfilesForProvider(store, providerKey));
|
||||||
@@ -895,8 +964,10 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
if (!deduped.includes(entry)) deduped.push(entry);
|
if (!deduped.includes(entry)) deduped.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user specified explicit order in config, respect it exactly
|
// If user specified explicit order (store override or config), respect it
|
||||||
if (configuredOrder && configuredOrder.length > 0) {
|
// exactly, but still apply cooldown sorting to avoid repeatedly selecting
|
||||||
|
// known-bad/rate-limited keys as the first candidate.
|
||||||
|
if (explicitOrder && explicitOrder.length > 0) {
|
||||||
// ...but still respect cooldown tracking to avoid repeatedly selecting a
|
// ...but still respect cooldown tracking to avoid repeatedly selecting a
|
||||||
// known-bad/rate-limited key as the first candidate.
|
// known-bad/rate-limited key as the first candidate.
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1118,8 +1189,8 @@ export async function markAuthProfileGood(params: {
|
|||||||
saveAuthProfileStore(store, agentDir);
|
saveAuthProfileStore(store, agentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAuthStorePathForDisplay(): string {
|
export function resolveAuthStorePathForDisplay(agentDir?: string): string {
|
||||||
const pathname = resolveAuthStorePath();
|
const pathname = resolveAuthStorePath(agentDir);
|
||||||
return pathname.startsWith("~") ? pathname : resolveUserPath(pathname);
|
return pathname.startsWith("~") ? pathname : resolveUserPath(pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1131,7 +1131,7 @@ describe("directive behavior", () => {
|
|||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
const storePath = path.join(home, "sessions.json");
|
const storePath = path.join(home, "sessions.json");
|
||||||
const authDir = path.join(home, ".clawdbot", "agent");
|
const authDir = path.join(home, ".clawdbot", "agents", "main", "agent");
|
||||||
await fs.mkdir(authDir, { recursive: true, mode: 0o700 });
|
await fs.mkdir(authDir, { recursive: true, mode: 0o700 });
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(authDir, "auth-profiles.json"),
|
path.join(authDir, "auth-profiles.json"),
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
|
|
||||||
import { resolveAgentConfig } from "../../agents/agent-scope.js";
|
|
||||||
import {
|
import {
|
||||||
|
resolveAgentConfig,
|
||||||
|
resolveAgentDir,
|
||||||
|
resolveDefaultAgentId,
|
||||||
|
} from "../../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
|
isProfileInCooldown,
|
||||||
resolveAuthProfileDisplayLabel,
|
resolveAuthProfileDisplayLabel,
|
||||||
resolveAuthStorePathForDisplay,
|
resolveAuthStorePathForDisplay,
|
||||||
} from "../../agents/auth-profiles.js";
|
} from "../../agents/auth-profiles.js";
|
||||||
@@ -20,6 +24,7 @@ import {
|
|||||||
buildModelAliasIndex,
|
buildModelAliasIndex,
|
||||||
type ModelAliasIndex,
|
type ModelAliasIndex,
|
||||||
modelKey,
|
modelKey,
|
||||||
|
normalizeProviderId,
|
||||||
resolveConfiguredModelRef,
|
resolveConfiguredModelRef,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
@@ -73,18 +78,104 @@ const maskApiKey = (value: string): string => {
|
|||||||
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
|
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ModelAuthDetailMode = "compact" | "verbose";
|
||||||
|
|
||||||
const resolveAuthLabel = async (
|
const resolveAuthLabel = async (
|
||||||
provider: string,
|
provider: string,
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
modelsPath: string,
|
modelsPath: string,
|
||||||
|
agentDir?: string,
|
||||||
|
mode: ModelAuthDetailMode = "compact",
|
||||||
): Promise<{ label: string; source: string }> => {
|
): Promise<{ label: string; source: string }> => {
|
||||||
const formatPath = (value: string) => shortenHomePath(value);
|
const formatPath = (value: string) => shortenHomePath(value);
|
||||||
const store = ensureAuthProfileStore();
|
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||||
const order = resolveAuthProfileOrder({ cfg, store, provider });
|
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 (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 labels = order.map((profileId) => {
|
||||||
const profile = store.profiles[profileId];
|
const profile = store.profiles[profileId];
|
||||||
const configProfile = cfg.auth?.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 (
|
if (
|
||||||
!profile ||
|
!profile ||
|
||||||
(configProfile?.provider &&
|
(configProfile?.provider &&
|
||||||
@@ -93,13 +184,23 @@ const resolveAuthLabel = async (
|
|||||||
configProfile.mode !== profile.type &&
|
configProfile.mode !== profile.type &&
|
||||||
!(configProfile.mode === "oauth" && profile.type === "token"))
|
!(configProfile.mode === "oauth" && profile.type === "token"))
|
||||||
) {
|
) {
|
||||||
return `${profileId}=missing`;
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
||||||
|
return `${profileId}=missing${suffix}`;
|
||||||
}
|
}
|
||||||
if (profile.type === "api_key") {
|
if (profile.type === "api_key") {
|
||||||
return `${profileId}=${maskApiKey(profile.key)}`;
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
||||||
|
return `${profileId}=${maskApiKey(profile.key)}${suffix}`;
|
||||||
}
|
}
|
||||||
if (profile.type === "token") {
|
if (profile.type === "token") {
|
||||||
return `${profileId}=token:${maskApiKey(profile.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({
|
const display = resolveAuthProfileDisplayLabel({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -112,13 +213,20 @@ const resolveAuthLabel = async (
|
|||||||
: display.startsWith(profileId)
|
: display.startsWith(profileId)
|
||||||
? display.slice(profileId.length).trim()
|
? display.slice(profileId.length).trim()
|
||||||
: `(${display})`;
|
: `(${display})`;
|
||||||
return `${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`;
|
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 {
|
return {
|
||||||
label: labels.join(", "),
|
label: labels.join(", "),
|
||||||
source: `auth-profiles.json: ${formatPath(
|
source: `auth-profiles.json: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
|
||||||
resolveAuthStorePathForDisplay(),
|
|
||||||
)}`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,13 +236,13 @@ const resolveAuthLabel = async (
|
|||||||
envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") ||
|
envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") ||
|
||||||
envKey.source.toLowerCase().includes("oauth");
|
envKey.source.toLowerCase().includes("oauth");
|
||||||
const label = isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey);
|
const label = isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey);
|
||||||
return { label, source: envKey.source };
|
return { label, source: mode === "verbose" ? envKey.source : "" };
|
||||||
}
|
}
|
||||||
const customKey = getCustomProviderApiKey(cfg, provider);
|
const customKey = getCustomProviderApiKey(cfg, provider);
|
||||||
if (customKey) {
|
if (customKey) {
|
||||||
return {
|
return {
|
||||||
label: maskApiKey(customKey),
|
label: maskApiKey(customKey),
|
||||||
source: `models.json: ${formatPath(modelsPath)}`,
|
source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { label: "missing", source: "missing" };
|
return { label: "missing", source: "missing" };
|
||||||
@@ -151,10 +259,13 @@ const resolveProfileOverride = (params: {
|
|||||||
rawProfile?: string;
|
rawProfile?: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
|
agentDir?: string;
|
||||||
}): { profileId?: string; error?: string } => {
|
}): { profileId?: string; error?: string } => {
|
||||||
const raw = params.rawProfile?.trim();
|
const raw = params.rawProfile?.trim();
|
||||||
if (!raw) return {};
|
if (!raw) return {};
|
||||||
const store = ensureAuthProfileStore();
|
const store = ensureAuthProfileStore(params.agentDir, {
|
||||||
|
allowKeychainPrompt: false,
|
||||||
|
});
|
||||||
const profile = store.profiles[raw];
|
const profile = store.profiles[raw];
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return { error: `Auth profile "${raw}" not found.` };
|
return { error: `Auth profile "${raw}" not found.` };
|
||||||
@@ -363,6 +474,10 @@ export async function handleDirectiveOnly(params: {
|
|||||||
currentReasoningLevel,
|
currentReasoningLevel,
|
||||||
currentElevatedLevel,
|
currentElevatedLevel,
|
||||||
} = params;
|
} = params;
|
||||||
|
const activeAgentId = params.sessionKey
|
||||||
|
? resolveAgentIdFromSessionKey(params.sessionKey)
|
||||||
|
: resolveDefaultAgentId(params.cfg);
|
||||||
|
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
|
||||||
const runtimeIsSandboxed = (() => {
|
const runtimeIsSandboxed = (() => {
|
||||||
const sessionKey = params.sessionKey?.trim();
|
const sessionKey = params.sessionKey?.trim();
|
||||||
if (!sessionKey) return false;
|
if (!sessionKey) return false;
|
||||||
@@ -384,6 +499,10 @@ export async function handleDirectiveOnly(params: {
|
|||||||
const isModelListAlias =
|
const isModelListAlias =
|
||||||
modelDirective === "status" || modelDirective === "list";
|
modelDirective === "status" || modelDirective === "list";
|
||||||
if (!directives.rawModelDirective || isModelListAlias) {
|
if (!directives.rawModelDirective || isModelListAlias) {
|
||||||
|
const modelsPath = `${agentDir}/models.json`;
|
||||||
|
const formatPath = (value: string) => shortenHomePath(value);
|
||||||
|
const authMode: ModelAuthDetailMode =
|
||||||
|
modelDirective === "status" ? "verbose" : "compact";
|
||||||
if (allowedModelCatalog.length === 0) {
|
if (allowedModelCatalog.length === 0) {
|
||||||
const resolvedDefault = resolveConfiguredModelRef({
|
const resolvedDefault = resolveConfiguredModelRef({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
@@ -423,9 +542,6 @@ export async function handleDirectiveOnly(params: {
|
|||||||
if (fallbackCatalog.length === 0) {
|
if (fallbackCatalog.length === 0) {
|
||||||
return { text: "No models available." };
|
return { text: "No models available." };
|
||||||
}
|
}
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
|
||||||
const modelsPath = `${agentDir}/models.json`;
|
|
||||||
const formatPath = (value: string) => shortenHomePath(value);
|
|
||||||
const authByProvider = new Map<string, string>();
|
const authByProvider = new Map<string, string>();
|
||||||
for (const entry of fallbackCatalog) {
|
for (const entry of fallbackCatalog) {
|
||||||
if (authByProvider.has(entry.provider)) continue;
|
if (authByProvider.has(entry.provider)) continue;
|
||||||
@@ -433,6 +549,8 @@ export async function handleDirectiveOnly(params: {
|
|||||||
entry.provider,
|
entry.provider,
|
||||||
params.cfg,
|
params.cfg,
|
||||||
modelsPath,
|
modelsPath,
|
||||||
|
agentDir,
|
||||||
|
authMode,
|
||||||
);
|
);
|
||||||
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
||||||
}
|
}
|
||||||
@@ -441,7 +559,8 @@ export async function handleDirectiveOnly(params: {
|
|||||||
const lines = [
|
const lines = [
|
||||||
`Current: ${current}`,
|
`Current: ${current}`,
|
||||||
`Default: ${defaultLabel}`,
|
`Default: ${defaultLabel}`,
|
||||||
`Auth file: ${formatPath(resolveAuthStorePathForDisplay())}`,
|
`Agent: ${activeAgentId}`,
|
||||||
|
`Auth file: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
|
||||||
`⚠️ Model catalog unavailable; showing configured models only.`,
|
`⚠️ Model catalog unavailable; showing configured models only.`,
|
||||||
];
|
];
|
||||||
const byProvider = new Map<string, typeof fallbackCatalog>();
|
const byProvider = new Map<string, typeof fallbackCatalog>();
|
||||||
@@ -469,9 +588,6 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}
|
}
|
||||||
return { text: lines.join("\n") };
|
return { text: lines.join("\n") };
|
||||||
}
|
}
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
|
||||||
const modelsPath = `${agentDir}/models.json`;
|
|
||||||
const formatPath = (value: string) => shortenHomePath(value);
|
|
||||||
const authByProvider = new Map<string, string>();
|
const authByProvider = new Map<string, string>();
|
||||||
for (const entry of allowedModelCatalog) {
|
for (const entry of allowedModelCatalog) {
|
||||||
if (authByProvider.has(entry.provider)) continue;
|
if (authByProvider.has(entry.provider)) continue;
|
||||||
@@ -479,6 +595,8 @@ export async function handleDirectiveOnly(params: {
|
|||||||
entry.provider,
|
entry.provider,
|
||||||
params.cfg,
|
params.cfg,
|
||||||
modelsPath,
|
modelsPath,
|
||||||
|
agentDir,
|
||||||
|
authMode,
|
||||||
);
|
);
|
||||||
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
||||||
}
|
}
|
||||||
@@ -487,7 +605,8 @@ export async function handleDirectiveOnly(params: {
|
|||||||
const lines = [
|
const lines = [
|
||||||
`Current: ${current}`,
|
`Current: ${current}`,
|
||||||
`Default: ${defaultLabel}`,
|
`Default: ${defaultLabel}`,
|
||||||
`Auth file: ${formatPath(resolveAuthStorePathForDisplay())}`,
|
`Agent: ${activeAgentId}`,
|
||||||
|
`Auth file: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
|
||||||
];
|
];
|
||||||
if (resetModelOverride) {
|
if (resetModelOverride) {
|
||||||
lines.push(`(previous selection reset to default)`);
|
lines.push(`(previous selection reset to default)`);
|
||||||
@@ -684,15 +803,16 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}
|
}
|
||||||
modelSelection = resolved.selection;
|
modelSelection = resolved.selection;
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
if (directives.rawModelProfile) {
|
if (directives.rawModelProfile) {
|
||||||
const profileResolved = resolveProfileOverride({
|
const profileResolved = resolveProfileOverride({
|
||||||
rawProfile: directives.rawModelProfile,
|
rawProfile: directives.rawModelProfile,
|
||||||
provider: modelSelection.provider,
|
provider: modelSelection.provider,
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
});
|
agentDir,
|
||||||
if (profileResolved.error) {
|
});
|
||||||
return { text: profileResolved.error };
|
if (profileResolved.error) {
|
||||||
}
|
return { text: profileResolved.error };
|
||||||
|
}
|
||||||
profileOverride = profileResolved.profileId;
|
profileOverride = profileResolved.profileId;
|
||||||
}
|
}
|
||||||
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
@@ -933,6 +1053,7 @@ export async function persistInlineDirectives(params: {
|
|||||||
rawProfile: directives.rawModelProfile,
|
rawProfile: directives.rawModelProfile,
|
||||||
provider: resolved.ref.provider,
|
provider: resolved.ref.provider,
|
||||||
cfg,
|
cfg,
|
||||||
|
agentDir,
|
||||||
});
|
});
|
||||||
if (profileResolved.error) {
|
if (profileResolved.error) {
|
||||||
throw new Error(profileResolved.error);
|
throw new Error(profileResolved.error);
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
modelsAliasesListCommand,
|
modelsAliasesListCommand,
|
||||||
modelsAliasesRemoveCommand,
|
modelsAliasesRemoveCommand,
|
||||||
modelsAuthAddCommand,
|
modelsAuthAddCommand,
|
||||||
|
modelsAuthOrderClearCommand,
|
||||||
|
modelsAuthOrderGetCommand,
|
||||||
|
modelsAuthOrderSetCommand,
|
||||||
modelsAuthPasteTokenCommand,
|
modelsAuthPasteTokenCommand,
|
||||||
modelsAuthSetupTokenCommand,
|
modelsAuthSetupTokenCommand,
|
||||||
modelsFallbacksAddCommand,
|
modelsFallbacksAddCommand,
|
||||||
@@ -360,4 +363,72 @@ export function registerModelsCli(program: Command) {
|
|||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const order = auth
|
||||||
|
.command("order")
|
||||||
|
.description("Manage per-agent auth profile order overrides");
|
||||||
|
|
||||||
|
order
|
||||||
|
.command("get")
|
||||||
|
.description("Show per-agent auth order override (from auth-profiles.json)")
|
||||||
|
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||||
|
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||||
|
.option("--json", "Output JSON", false)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await modelsAuthOrderGetCommand(
|
||||||
|
{
|
||||||
|
provider: opts.provider as string,
|
||||||
|
agent: opts.agent as string | undefined,
|
||||||
|
json: Boolean(opts.json),
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
order
|
||||||
|
.command("set")
|
||||||
|
.description("Set per-agent auth order override (locks rotation to this list)")
|
||||||
|
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||||
|
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||||
|
.argument("<profileIds...>", "Auth profile ids (e.g. anthropic:claude-cli)")
|
||||||
|
.action(async (profileIds: string[], opts) => {
|
||||||
|
try {
|
||||||
|
await modelsAuthOrderSetCommand(
|
||||||
|
{
|
||||||
|
provider: opts.provider as string,
|
||||||
|
agent: opts.agent as string | undefined,
|
||||||
|
order: profileIds,
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
order
|
||||||
|
.command("clear")
|
||||||
|
.description("Clear per-agent auth order override (fall back to config/round-robin)")
|
||||||
|
.requiredOption("--provider <name>", "Provider id (e.g. anthropic)")
|
||||||
|
.option("--agent <id>", "Agent id (default: configured default agent)")
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await modelsAuthOrderClearCommand(
|
||||||
|
{
|
||||||
|
provider: opts.provider as string,
|
||||||
|
agent: opts.agent as string | undefined,
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export {
|
|||||||
modelsAuthPasteTokenCommand,
|
modelsAuthPasteTokenCommand,
|
||||||
modelsAuthSetupTokenCommand,
|
modelsAuthSetupTokenCommand,
|
||||||
} from "./models/auth.js";
|
} from "./models/auth.js";
|
||||||
|
export {
|
||||||
|
modelsAuthOrderClearCommand,
|
||||||
|
modelsAuthOrderGetCommand,
|
||||||
|
modelsAuthOrderSetCommand,
|
||||||
|
} from "./models/auth-order.js";
|
||||||
export {
|
export {
|
||||||
modelsFallbacksAddCommand,
|
modelsFallbacksAddCommand,
|
||||||
modelsFallbacksClearCommand,
|
modelsFallbacksClearCommand,
|
||||||
|
|||||||
129
src/commands/models/auth-order.ts
Normal file
129
src/commands/models/auth-order.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
|
ensureAuthProfileStore,
|
||||||
|
setAuthProfileOrder,
|
||||||
|
type AuthProfileStore,
|
||||||
|
} from "../../agents/auth-profiles.js";
|
||||||
|
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||||
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { shortenHomePath } from "../../utils.js";
|
||||||
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
|
|
||||||
|
function resolveTargetAgent(cfg: ReturnType<typeof loadConfig>, raw?: string): {
|
||||||
|
agentId: string;
|
||||||
|
agentDir: string;
|
||||||
|
} {
|
||||||
|
const agentId = raw?.trim()
|
||||||
|
? normalizeAgentId(raw.trim())
|
||||||
|
: resolveDefaultAgentId(cfg);
|
||||||
|
const agentDir = resolveAgentDir(cfg, agentId);
|
||||||
|
return { agentId, agentDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeOrder(store: AuthProfileStore, provider: string): string[] {
|
||||||
|
const providerKey = normalizeProviderId(provider);
|
||||||
|
const order = store.order?.[providerKey];
|
||||||
|
return Array.isArray(order) ? order : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function modelsAuthOrderGetCommand(
|
||||||
|
opts: { provider: string; agent?: string; json?: boolean },
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) {
|
||||||
|
const rawProvider = opts.provider?.trim();
|
||||||
|
if (!rawProvider) throw new Error("Missing --provider.");
|
||||||
|
const provider = normalizeProviderId(rawProvider);
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent);
|
||||||
|
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||||
|
const order = describeOrder(store, provider);
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
runtime.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
agentDir,
|
||||||
|
provider,
|
||||||
|
authStorePath: shortenHomePath(`${agentDir}/auth-profiles.json`),
|
||||||
|
order: order.length > 0 ? order : null,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.log(`Agent: ${agentId}`);
|
||||||
|
runtime.log(`Provider: ${provider}`);
|
||||||
|
runtime.log(`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`);
|
||||||
|
runtime.log(
|
||||||
|
order.length > 0 ? `Order override: ${order.join(", ")}` : "Order override: (none)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function modelsAuthOrderClearCommand(
|
||||||
|
opts: { provider: string; agent?: string },
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) {
|
||||||
|
const rawProvider = opts.provider?.trim();
|
||||||
|
if (!rawProvider) throw new Error("Missing --provider.");
|
||||||
|
const provider = normalizeProviderId(rawProvider);
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent);
|
||||||
|
const updated = await setAuthProfileOrder({ agentDir, provider, order: null });
|
||||||
|
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||||
|
|
||||||
|
runtime.log(`Agent: ${agentId}`);
|
||||||
|
runtime.log(`Provider: ${provider}`);
|
||||||
|
runtime.log("Cleared per-agent order override.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function modelsAuthOrderSetCommand(
|
||||||
|
opts: { provider: string; agent?: string; order: string[] },
|
||||||
|
runtime: RuntimeEnv,
|
||||||
|
) {
|
||||||
|
const rawProvider = opts.provider?.trim();
|
||||||
|
if (!rawProvider) throw new Error("Missing --provider.");
|
||||||
|
const provider = normalizeProviderId(rawProvider);
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||||
|
const providerKey = normalizeProviderId(provider);
|
||||||
|
const requested = (opts.order ?? [])
|
||||||
|
.map((entry) => String(entry).trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (requested.length === 0) {
|
||||||
|
throw new Error("Missing profile ids. Provide one or more profile ids.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const profileId of requested) {
|
||||||
|
const cred = store.profiles[profileId];
|
||||||
|
if (!cred) {
|
||||||
|
throw new Error(`Auth profile "${profileId}" not found in ${agentDir}.`);
|
||||||
|
}
|
||||||
|
if (normalizeProviderId(cred.provider) !== providerKey) {
|
||||||
|
throw new Error(
|
||||||
|
`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await setAuthProfileOrder({
|
||||||
|
agentDir,
|
||||||
|
provider,
|
||||||
|
order: requested,
|
||||||
|
});
|
||||||
|
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
|
||||||
|
|
||||||
|
runtime.log(`Agent: ${agentId}`);
|
||||||
|
runtime.log(`Provider: ${provider}`);
|
||||||
|
runtime.log(`Order override: ${describeOrder(updated, provider).join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user