feat(models): add per-agent auth order overrides

This commit is contained in:
Peter Steinberger
2026-01-09 14:07:29 +00:00
parent 944f15e401
commit 3e400ff9f2
8 changed files with 467 additions and 36 deletions

View File

@@ -6,6 +6,7 @@
- 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
- 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.
- 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.

View File

@@ -130,6 +130,39 @@ describe("resolveAuthProfileOrder", () => {
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", () => {
const now = Date.now();
const order = resolveAuthProfileOrder({

View File

@@ -82,6 +82,12 @@ export type ProfileUsageStats = {
export type AuthProfileStore = {
version: number;
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>;
/** Usage statistics per profile for round-robin rotation */
usageStats?: Record<string, ProfileUsageStats>;
@@ -133,6 +139,7 @@ function syncAuthProfileStore(
): void {
target.version = source.version;
target.profiles = source.profiles;
target.order = source.order;
target.lastGood = source.lastGood;
target.usageStats = source.usageStats;
}
@@ -270,9 +277,25 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
if (!typed.provider) continue;
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 {
version: Number(record.version ?? AUTH_STORE_VERSION),
profiles: normalized,
order,
lastGood:
record.lastGood && typeof record.lastGood === "object"
? (record.lastGood as Record<string, string>)
@@ -680,12 +703,49 @@ export function saveAuthProfileStore(
const payload = {
version: AUTH_STORE_VERSION,
profiles: store.profiles,
order: store.order ?? undefined,
lastGood: store.lastGood ?? undefined,
usageStats: store.usageStats ?? undefined,
} satisfies AuthProfileStore;
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: {
profileId: string;
credential: AuthProfileCredential;
@@ -863,6 +923,14 @@ export function resolveAuthProfileOrder(params: {
}): string[] {
const { cfg, store, provider, preferredProfile } = params;
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 order = cfg?.auth?.order;
if (!order) return undefined;
@@ -871,6 +939,7 @@ export function resolveAuthProfileOrder(params: {
}
return undefined;
})();
const explicitOrder = storedOrder ?? configuredOrder;
const explicitProfiles = cfg?.auth?.profiles
? Object.entries(cfg.auth.profiles)
.filter(
@@ -880,7 +949,7 @@ export function resolveAuthProfileOrder(params: {
.map(([profileId]) => profileId)
: [];
const baseOrder =
configuredOrder ??
explicitOrder ??
(explicitProfiles.length > 0
? explicitProfiles
: listProfilesForProvider(store, providerKey));
@@ -895,8 +964,10 @@ export function resolveAuthProfileOrder(params: {
if (!deduped.includes(entry)) deduped.push(entry);
}
// If user specified explicit order in config, respect it exactly
if (configuredOrder && configuredOrder.length > 0) {
// If user specified explicit order (store override or config), respect it
// exactly, but still apply cooldown sorting to avoid repeatedly selecting
// known-bad/rate-limited keys as the first candidate.
if (explicitOrder && explicitOrder.length > 0) {
// ...but still respect cooldown tracking to avoid repeatedly selecting a
// known-bad/rate-limited key as the first candidate.
const now = Date.now();
@@ -1118,8 +1189,8 @@ export async function markAuthProfileGood(params: {
saveAuthProfileStore(store, agentDir);
}
export function resolveAuthStorePathForDisplay(): string {
const pathname = resolveAuthStorePath();
export function resolveAuthStorePathForDisplay(agentDir?: string): string {
const pathname = resolveAuthStorePath(agentDir);
return pathname.startsWith("~") ? pathname : resolveUserPath(pathname);
}

View File

@@ -1131,7 +1131,7 @@ describe("directive behavior", () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockReset();
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.writeFile(
path.join(authDir, "auth-profiles.json"),

View File

@@ -1,6 +1,10 @@
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
import { resolveAgentConfig } from "../../agents/agent-scope.js";
import {
resolveAgentConfig,
resolveAgentDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import {
isProfileInCooldown,
resolveAuthProfileDisplayLabel,
resolveAuthStorePathForDisplay,
} from "../../agents/auth-profiles.js";
@@ -20,6 +24,7 @@ import {
buildModelAliasIndex,
type ModelAliasIndex,
modelKey,
normalizeProviderId,
resolveConfiguredModelRef,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
@@ -73,18 +78,104 @@ const maskApiKey = (value: string): string => {
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
};
type ModelAuthDetailMode = "compact" | "verbose";
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();
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 &&
@@ -93,13 +184,23 @@ const resolveAuthLabel = async (
configProfile.mode !== profile.type &&
!(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") {
return `${profileId}=${maskApiKey(profile.key)}`;
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
return `${profileId}=${maskApiKey(profile.key)}${suffix}`;
}
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({
cfg,
@@ -112,13 +213,20 @@ const resolveAuthLabel = async (
: display.startsWith(profileId)
? display.slice(profileId.length).trim()
: `(${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 {
label: labels.join(", "),
source: `auth-profiles.json: ${formatPath(
resolveAuthStorePathForDisplay(),
)}`,
source: `auth-profiles.json: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
};
}
@@ -128,13 +236,13 @@ const resolveAuthLabel = async (
envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") ||
envKey.source.toLowerCase().includes("oauth");
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);
if (customKey) {
return {
label: maskApiKey(customKey),
source: `models.json: ${formatPath(modelsPath)}`,
source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "",
};
}
return { label: "missing", source: "missing" };
@@ -151,10 +259,13 @@ 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();
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profile = store.profiles[raw];
if (!profile) {
return { error: `Auth profile "${raw}" not found.` };
@@ -363,6 +474,10 @@ export async function handleDirectiveOnly(params: {
currentReasoningLevel,
currentElevatedLevel,
} = params;
const activeAgentId = params.sessionKey
? resolveAgentIdFromSessionKey(params.sessionKey)
: resolveDefaultAgentId(params.cfg);
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
const runtimeIsSandboxed = (() => {
const sessionKey = params.sessionKey?.trim();
if (!sessionKey) return false;
@@ -384,6 +499,10 @@ export async function handleDirectiveOnly(params: {
const isModelListAlias =
modelDirective === "status" || modelDirective === "list";
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) {
const resolvedDefault = resolveConfiguredModelRef({
cfg: params.cfg,
@@ -423,9 +542,6 @@ export async function handleDirectiveOnly(params: {
if (fallbackCatalog.length === 0) {
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>();
for (const entry of fallbackCatalog) {
if (authByProvider.has(entry.provider)) continue;
@@ -433,6 +549,8 @@ export async function handleDirectiveOnly(params: {
entry.provider,
params.cfg,
modelsPath,
agentDir,
authMode,
);
authByProvider.set(entry.provider, formatAuthLabel(auth));
}
@@ -441,7 +559,8 @@ export async function handleDirectiveOnly(params: {
const lines = [
`Current: ${current}`,
`Default: ${defaultLabel}`,
`Auth file: ${formatPath(resolveAuthStorePathForDisplay())}`,
`Agent: ${activeAgentId}`,
`Auth file: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
`⚠️ Model catalog unavailable; showing configured models only.`,
];
const byProvider = new Map<string, typeof fallbackCatalog>();
@@ -469,9 +588,6 @@ export async function handleDirectiveOnly(params: {
}
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>();
for (const entry of allowedModelCatalog) {
if (authByProvider.has(entry.provider)) continue;
@@ -479,6 +595,8 @@ export async function handleDirectiveOnly(params: {
entry.provider,
params.cfg,
modelsPath,
agentDir,
authMode,
);
authByProvider.set(entry.provider, formatAuthLabel(auth));
}
@@ -487,7 +605,8 @@ export async function handleDirectiveOnly(params: {
const lines = [
`Current: ${current}`,
`Default: ${defaultLabel}`,
`Auth file: ${formatPath(resolveAuthStorePathForDisplay())}`,
`Agent: ${activeAgentId}`,
`Auth file: ${formatPath(resolveAuthStorePathForDisplay(agentDir))}`,
];
if (resetModelOverride) {
lines.push(`(previous selection reset to default)`);
@@ -684,15 +803,16 @@ export async function handleDirectiveOnly(params: {
}
modelSelection = resolved.selection;
if (modelSelection) {
if (directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
if (directives.rawModelProfile) {
const profileResolved = resolveProfileOverride({
rawProfile: directives.rawModelProfile,
provider: modelSelection.provider,
cfg: params.cfg,
agentDir,
});
if (profileResolved.error) {
return { text: profileResolved.error };
}
profileOverride = profileResolved.profileId;
}
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
@@ -933,6 +1053,7 @@ export async function persistInlineDirectives(params: {
rawProfile: directives.rawModelProfile,
provider: resolved.ref.provider,
cfg,
agentDir,
});
if (profileResolved.error) {
throw new Error(profileResolved.error);

View File

@@ -5,6 +5,9 @@ import {
modelsAliasesListCommand,
modelsAliasesRemoveCommand,
modelsAuthAddCommand,
modelsAuthOrderClearCommand,
modelsAuthOrderGetCommand,
modelsAuthOrderSetCommand,
modelsAuthPasteTokenCommand,
modelsAuthSetupTokenCommand,
modelsFallbacksAddCommand,
@@ -360,4 +363,72 @@ export function registerModelsCli(program: Command) {
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);
}
});
}

View File

@@ -8,6 +8,11 @@ export {
modelsAuthPasteTokenCommand,
modelsAuthSetupTokenCommand,
} from "./models/auth.js";
export {
modelsAuthOrderClearCommand,
modelsAuthOrderGetCommand,
modelsAuthOrderSetCommand,
} from "./models/auth-order.js";
export {
modelsFallbacksAddCommand,
modelsFallbacksClearCommand,

View 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(", ")}`);
}