feat!: redesign model config + auth profiles
This commit is contained in:
@@ -1,19 +1,28 @@
|
||||
export function extractModelDirective(body?: string): {
|
||||
cleaned: string;
|
||||
rawModel?: string;
|
||||
rawProfile?: string;
|
||||
hasDirective: boolean;
|
||||
} {
|
||||
if (!body) return { cleaned: "", hasDirective: false };
|
||||
const match = body.match(
|
||||
/(?:^|\s)\/model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:-]+(?:\/[A-Za-z0-9_.:-]+)?)?/i,
|
||||
/(?:^|\s)\/model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i,
|
||||
);
|
||||
const rawModel = match?.[1]?.trim();
|
||||
const raw = match?.[1]?.trim();
|
||||
let rawModel = raw;
|
||||
let rawProfile: string | undefined;
|
||||
if (raw?.includes("@")) {
|
||||
const parts = raw.split("@");
|
||||
rawModel = parts[0]?.trim();
|
||||
rawProfile = parts.slice(1).join("@").trim() || undefined;
|
||||
}
|
||||
const cleaned = match
|
||||
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
|
||||
: body.trim();
|
||||
return {
|
||||
cleaned,
|
||||
rawModel,
|
||||
rawProfile,
|
||||
hasDirective: !!match,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,11 +37,24 @@ vi.mock("../agents/model-catalog.js", () => ({
|
||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-reply-"));
|
||||
const previousHome = process.env.HOME;
|
||||
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
process.env.HOME = base;
|
||||
process.env.CLAWDBOT_STATE_DIR = path.join(base, ".clawdbot");
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(base, ".clawdbot", "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
try {
|
||||
return await fn(base);
|
||||
} finally {
|
||||
process.env.HOME = previousHome;
|
||||
if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR;
|
||||
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR;
|
||||
else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||
if (previousPiAgentDir === undefined)
|
||||
delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
await fs.rm(base, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -566,9 +579,12 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
@@ -593,9 +609,12 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
@@ -620,9 +639,12 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["anthropic/claude-opus-4-5", "openai/gpt-4.1-mini"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
@@ -646,9 +668,11 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["anthropic/claude-opus-4-5"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
@@ -671,9 +695,12 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["openai/gpt-4.1-mini"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
@@ -699,11 +726,11 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["openai/gpt-4.1-mini", "anthropic/claude-opus-4-5"],
|
||||
modelAliases: {
|
||||
Opus: "anthropic/claude-opus-4-5",
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -721,6 +748,55 @@ describe("directive parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("stores auth profile overrides on /model directive", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||
const storePath = path.join(home, "sessions.json");
|
||||
const authDir = path.join(home, ".clawdbot", "agent");
|
||||
await fs.mkdir(authDir, { recursive: true, mode: 0o700 });
|
||||
await fs.writeFile(
|
||||
path.join(authDir, "auth-profiles.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:work": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-test-1234567890",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Auth profile set to anthropic:work");
|
||||
const store = loadSessionStore(storePath);
|
||||
const entry = store.main;
|
||||
expect(entry.authProfileOverride).toBe("anthropic:work");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("queues a system event when switching models", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
drainSystemEvents();
|
||||
@@ -732,11 +808,11 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "openai/gpt-4.1-mini",
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["openai/gpt-4.1-mini", "anthropic/claude-opus-4-5"],
|
||||
modelAliases: {
|
||||
Opus: "anthropic/claude-opus-4-5",
|
||||
models: {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
},
|
||||
},
|
||||
session: { store: storePath },
|
||||
@@ -771,9 +847,12 @@ describe("directive parsing", () => {
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
model: { primary: "anthropic/claude-opus-4-5" },
|
||||
workspace: path.join(home, "clawd"),
|
||||
allowedModels: ["openai/gpt-4.1-mini"],
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": {},
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
|
||||
@@ -361,7 +361,9 @@ export async function getReplyFromConfig(
|
||||
: `Model switched to ${label}.`;
|
||||
const isModelListAlias =
|
||||
directives.hasModelDirective &&
|
||||
directives.rawModelDirective?.trim().toLowerCase() === "status";
|
||||
["status", "list"].includes(
|
||||
directives.rawModelDirective?.trim().toLowerCase() ?? "",
|
||||
);
|
||||
const effectiveModelDirective = isModelListAlias
|
||||
? undefined
|
||||
: directives.rawModelDirective;
|
||||
@@ -376,6 +378,7 @@ export async function getReplyFromConfig(
|
||||
})
|
||||
) {
|
||||
const directiveReply = await handleDirectiveOnly({
|
||||
cfg,
|
||||
directives,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
@@ -401,6 +404,7 @@ export async function getReplyFromConfig(
|
||||
const persisted = await persistInlineDirectives({
|
||||
directives,
|
||||
effectiveModelDirective,
|
||||
cfg,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -634,6 +638,7 @@ export async function getReplyFromConfig(
|
||||
resolvedQueue.mode === "followup" ||
|
||||
resolvedQueue.mode === "collect" ||
|
||||
resolvedQueue.mode === "steer-backlog";
|
||||
const authProfileId = sessionEntry?.authProfileOverride;
|
||||
const followupRun = {
|
||||
prompt: queuedBody,
|
||||
summaryLine: baseBodyTrimmedRaw,
|
||||
@@ -648,6 +653,7 @@ export async function getReplyFromConfig(
|
||||
skillsSnapshot,
|
||||
provider,
|
||||
model,
|
||||
authProfileId,
|
||||
thinkLevel: resolvedThinkLevel,
|
||||
verboseLevel: resolvedVerboseLevel,
|
||||
elevatedLevel: resolvedElevatedLevel,
|
||||
|
||||
@@ -195,6 +195,7 @@ export async function runReplyAgent(params: {
|
||||
enforceFinalTag: followupRun.run.enforceFinalTag,
|
||||
provider,
|
||||
model,
|
||||
authProfileId: followupRun.run.authProfileId,
|
||||
thinkLevel: followupRun.run.thinkLevel,
|
||||
verboseLevel: followupRun.run.verboseLevel,
|
||||
bashElevated: followupRun.run.bashElevated,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
import { getEnvApiKey } from "@mariozechner/pi-ai";
|
||||
import { discoverAuthStorage } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
getCustomProviderApiKey,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveOAuthPath } from "../../config/paths.js";
|
||||
import {
|
||||
type SessionEntry,
|
||||
type SessionScope,
|
||||
@@ -42,55 +44,32 @@ export type CommandContext = {
|
||||
to?: string;
|
||||
};
|
||||
|
||||
function hasOAuthCredentials(provider: string): boolean {
|
||||
try {
|
||||
const oauthPath = resolveOAuthPath();
|
||||
if (!fs.existsSync(oauthPath)) return false;
|
||||
const raw = fs.readFileSync(oauthPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const entry = parsed?.[provider] as
|
||||
| {
|
||||
refresh?: string;
|
||||
refresh_token?: string;
|
||||
refreshToken?: string;
|
||||
access?: string;
|
||||
access_token?: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
| undefined;
|
||||
if (!entry) return false;
|
||||
const refresh =
|
||||
entry.refresh ?? entry.refresh_token ?? entry.refreshToken ?? "";
|
||||
const access =
|
||||
entry.access ?? entry.access_token ?? entry.accessToken ?? "";
|
||||
return Boolean(refresh.trim() && access.trim());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveModelAuthLabel(provider?: string): string | undefined {
|
||||
function resolveModelAuthLabel(
|
||||
provider?: string,
|
||||
cfg?: ClawdbotConfig,
|
||||
): string | undefined {
|
||||
const resolved = provider?.trim();
|
||||
if (!resolved) return undefined;
|
||||
|
||||
try {
|
||||
const authStorage = discoverAuthStorage(resolveClawdbotAgentDir());
|
||||
const stored = authStorage.get(resolved);
|
||||
if (stored?.type === "oauth") return "oauth";
|
||||
if (stored?.type === "api_key") return "api-key";
|
||||
} catch {
|
||||
// ignore auth storage errors
|
||||
const store = ensureAuthProfileStore();
|
||||
const profiles = listProfilesForProvider(store, resolved);
|
||||
if (profiles.length > 0) {
|
||||
const modes = new Set(
|
||||
profiles
|
||||
.map((id) => store.profiles[id]?.type)
|
||||
.filter((mode): mode is "api_key" | "oauth" => Boolean(mode)),
|
||||
);
|
||||
if (modes.has("oauth") && modes.has("api_key")) return "mixed";
|
||||
if (modes.has("oauth")) return "oauth";
|
||||
if (modes.has("api_key")) return "api-key";
|
||||
}
|
||||
|
||||
if (resolved === "anthropic") {
|
||||
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
||||
if (oauthEnv?.trim()) return "oauth";
|
||||
const envKey = resolveEnvApiKey(resolved);
|
||||
if (envKey?.apiKey) {
|
||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||
}
|
||||
|
||||
if (hasOAuthCredentials(resolved)) return "oauth";
|
||||
|
||||
const envKey = getEnvApiKey(resolved);
|
||||
if (envKey?.trim()) return "api-key";
|
||||
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
@@ -374,7 +353,7 @@ export async function handleCommands(params: {
|
||||
resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
|
||||
resolvedVerbose: resolvedVerboseLevel,
|
||||
resolvedElevated: resolvedElevatedLevel,
|
||||
modelAuth: resolveModelAuthLabel(provider),
|
||||
modelAuth: resolveModelAuthLabel(provider, cfg),
|
||||
webLinked,
|
||||
webAuthAgeMs,
|
||||
heartbeatSeconds,
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { getEnvApiKey } from "@mariozechner/pi-ai";
|
||||
import { discoverAuthStorage } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
|
||||
import {
|
||||
resolveAuthProfileDisplayLabel,
|
||||
resolveAuthStorePathForDisplay,
|
||||
} from "../../agents/auth-profiles.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import {
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_PROVIDER,
|
||||
} from "../../agents/defaults.js";
|
||||
import { hydrateAuthStorage } from "../../agents/model-auth.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
getCustomProviderApiKey,
|
||||
resolveAuthProfileOrder,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
@@ -53,43 +60,63 @@ const maskApiKey = (value: string): string => {
|
||||
|
||||
const resolveAuthLabel = async (
|
||||
provider: string,
|
||||
authStorage: ReturnType<typeof discoverAuthStorage>,
|
||||
authPaths: { authPath: string; modelsPath: string },
|
||||
cfg: ClawdbotConfig,
|
||||
modelsPath: string,
|
||||
): Promise<{ label: string; source: string }> => {
|
||||
const formatPath = (value: string) => shortenHomePath(value);
|
||||
const stored = authStorage.get(provider);
|
||||
if (stored?.type === "oauth") {
|
||||
const email = stored.email?.trim();
|
||||
const store = ensureAuthProfileStore();
|
||||
const order = resolveAuthProfileOrder({ cfg, store, provider });
|
||||
if (order.length > 0) {
|
||||
const labels = order.map((profileId) => {
|
||||
const profile = store.profiles[profileId];
|
||||
const configProfile = cfg.auth?.profiles?.[profileId];
|
||||
if (
|
||||
!profile ||
|
||||
(configProfile?.provider &&
|
||||
configProfile.provider !== profile.provider) ||
|
||||
(configProfile?.mode && configProfile.mode !== profile.type)
|
||||
) {
|
||||
return `${profileId}=missing`;
|
||||
}
|
||||
if (profile.type === "api_key") {
|
||||
return `${profileId}=${maskApiKey(profile.key)}`;
|
||||
}
|
||||
const display = resolveAuthProfileDisplayLabel({
|
||||
cfg,
|
||||
store,
|
||||
profileId,
|
||||
});
|
||||
const suffix =
|
||||
display === profileId
|
||||
? ""
|
||||
: display.startsWith(profileId)
|
||||
? display.slice(profileId.length).trim()
|
||||
: `(${display})`;
|
||||
return `${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`;
|
||||
});
|
||||
return {
|
||||
label: email ? `OAuth ${email}` : "OAuth (unknown)",
|
||||
source: `auth.json: ${formatPath(authPaths.authPath)}`,
|
||||
label: labels.join(", "),
|
||||
source: `auth-profiles.json: ${formatPath(
|
||||
resolveAuthStorePathForDisplay(),
|
||||
)}`,
|
||||
};
|
||||
}
|
||||
if (stored?.type === "api_key") {
|
||||
|
||||
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: envKey.source };
|
||||
}
|
||||
const customKey = getCustomProviderApiKey(cfg, provider);
|
||||
if (customKey) {
|
||||
return {
|
||||
label: maskApiKey(stored.key),
|
||||
source: `auth.json: ${formatPath(authPaths.authPath)}`,
|
||||
label: maskApiKey(customKey),
|
||||
source: `models.json: ${formatPath(modelsPath)}`,
|
||||
};
|
||||
}
|
||||
const envKey = getEnvApiKey(provider);
|
||||
if (envKey) return { label: maskApiKey(envKey), source: "env" };
|
||||
if (provider === "anthropic") {
|
||||
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN?.trim();
|
||||
if (oauthEnv) {
|
||||
return { label: "OAuth (env)", source: "env: ANTHROPIC_OAUTH_TOKEN" };
|
||||
}
|
||||
}
|
||||
try {
|
||||
const key = await authStorage.getApiKey(provider);
|
||||
if (key) {
|
||||
return {
|
||||
label: maskApiKey(key),
|
||||
source: `models.json: ${formatPath(authPaths.modelsPath)}`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore missing auth
|
||||
}
|
||||
return { label: "missing", source: "missing" };
|
||||
};
|
||||
|
||||
@@ -100,6 +127,26 @@ const formatAuthLabel = (auth: { label: string; source: string }) => {
|
||||
return `${auth.label} (${auth.source})`;
|
||||
};
|
||||
|
||||
const resolveProfileOverride = (params: {
|
||||
rawProfile?: string;
|
||||
provider: string;
|
||||
cfg: ClawdbotConfig;
|
||||
}): { profileId?: string; error?: string } => {
|
||||
const raw = params.rawProfile?.trim();
|
||||
if (!raw) return {};
|
||||
const store = ensureAuthProfileStore();
|
||||
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 };
|
||||
};
|
||||
|
||||
export type InlineDirectives = {
|
||||
cleaned: string;
|
||||
hasThinkDirective: boolean;
|
||||
@@ -114,6 +161,7 @@ export type InlineDirectives = {
|
||||
hasStatusDirective: boolean;
|
||||
hasModelDirective: boolean;
|
||||
rawModelDirective?: string;
|
||||
rawModelProfile?: string;
|
||||
hasQueueDirective: boolean;
|
||||
queueMode?: QueueMode;
|
||||
queueReset: boolean;
|
||||
@@ -151,6 +199,7 @@ export function parseInlineDirectives(body: string): InlineDirectives {
|
||||
const {
|
||||
cleaned: modelCleaned,
|
||||
rawModel,
|
||||
rawProfile,
|
||||
hasDirective: hasModelDirective,
|
||||
} = extractModelDirective(statusCleaned);
|
||||
const {
|
||||
@@ -182,6 +231,7 @@ export function parseInlineDirectives(body: string): InlineDirectives {
|
||||
hasStatusDirective,
|
||||
hasModelDirective,
|
||||
rawModelDirective: rawModel,
|
||||
rawModelProfile: rawProfile,
|
||||
hasQueueDirective,
|
||||
queueMode,
|
||||
queueReset,
|
||||
@@ -218,6 +268,7 @@ export function isDirectiveOnly(params: {
|
||||
}
|
||||
|
||||
export async function handleDirectiveOnly(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
directives: InlineDirectives;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
@@ -265,19 +316,14 @@ export async function handleDirectiveOnly(params: {
|
||||
return { text: "No models available." };
|
||||
}
|
||||
const agentDir = resolveClawdbotAgentDir();
|
||||
const authStorage = discoverAuthStorage(agentDir);
|
||||
const authPaths = {
|
||||
authPath: `${agentDir}/auth.json`,
|
||||
modelsPath: `${agentDir}/models.json`,
|
||||
};
|
||||
hydrateAuthStorage(authStorage);
|
||||
const modelsPath = `${agentDir}/models.json`;
|
||||
const authByProvider = new Map<string, string>();
|
||||
for (const entry of allowedModelCatalog) {
|
||||
if (authByProvider.has(entry.provider)) continue;
|
||||
const auth = await resolveAuthLabel(
|
||||
entry.provider,
|
||||
authStorage,
|
||||
authPaths,
|
||||
params.cfg,
|
||||
modelsPath,
|
||||
);
|
||||
authByProvider.set(entry.provider, formatAuthLabel(auth));
|
||||
}
|
||||
@@ -306,6 +352,9 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
return { text: lines.join("\n") };
|
||||
}
|
||||
if (directives.rawModelProfile && !modelDirective) {
|
||||
throw new Error("Auth profile override requires a model selection.");
|
||||
}
|
||||
}
|
||||
|
||||
if (directives.hasThinkDirective && !directives.thinkLevel) {
|
||||
@@ -378,6 +427,7 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
|
||||
let modelSelection: ModelDirectiveSelection | undefined;
|
||||
let profileOverride: string | undefined;
|
||||
if (directives.hasModelDirective && directives.rawModelDirective) {
|
||||
const resolved = resolveModelDirectiveSelection({
|
||||
raw: directives.rawModelDirective,
|
||||
@@ -391,6 +441,17 @@ 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 };
|
||||
}
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
const nextLabel = `${modelSelection.provider}/${modelSelection.model}`;
|
||||
if (nextLabel !== initialModelLabel) {
|
||||
enqueueSystemEvent(
|
||||
@@ -402,6 +463,9 @@ export async function handleDirectiveOnly(params: {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (directives.rawModelProfile && !modelSelection) {
|
||||
return { text: "Auth profile override requires a model selection." };
|
||||
}
|
||||
|
||||
if (sessionEntry && sessionStore && sessionKey) {
|
||||
if (directives.hasThinkDirective && directives.thinkLevel) {
|
||||
@@ -424,6 +488,11 @@ export async function handleDirectiveOnly(params: {
|
||||
sessionEntry.providerOverride = modelSelection.provider;
|
||||
sessionEntry.modelOverride = modelSelection.model;
|
||||
}
|
||||
if (profileOverride) {
|
||||
sessionEntry.authProfileOverride = profileOverride;
|
||||
} else if (directives.hasModelDirective) {
|
||||
delete sessionEntry.authProfileOverride;
|
||||
}
|
||||
}
|
||||
if (directives.hasQueueDirective && directives.queueReset) {
|
||||
delete sessionEntry.queueMode;
|
||||
@@ -481,6 +550,9 @@ export async function handleDirectiveOnly(params: {
|
||||
? `Model reset to default (${labelWithAlias}).`
|
||||
: `Model set to ${labelWithAlias}.`,
|
||||
);
|
||||
if (profileOverride) {
|
||||
parts.push(`Auth profile set to ${profileOverride}.`);
|
||||
}
|
||||
}
|
||||
if (directives.hasQueueDirective && directives.queueMode) {
|
||||
parts.push(`${SYSTEM_MARK} Queue mode set to ${directives.queueMode}.`);
|
||||
@@ -508,6 +580,7 @@ export async function handleDirectiveOnly(params: {
|
||||
export async function persistInlineDirectives(params: {
|
||||
directives: InlineDirectives;
|
||||
effectiveModelDirective?: string;
|
||||
cfg: ClawdbotConfig;
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
@@ -526,6 +599,7 @@ export async function persistInlineDirectives(params: {
|
||||
}): Promise<{ provider: string; model: string; contextTokens: number }> {
|
||||
const {
|
||||
directives,
|
||||
cfg,
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -586,6 +660,18 @@ export async function persistInlineDirectives(params: {
|
||||
if (resolved) {
|
||||
const key = modelKey(resolved.ref.provider, resolved.ref.model);
|
||||
if (allowedModelKeys.size === 0 || allowedModelKeys.has(key)) {
|
||||
let profileOverride: string | undefined;
|
||||
if (directives.rawModelProfile) {
|
||||
const profileResolved = resolveProfileOverride({
|
||||
rawProfile: directives.rawModelProfile,
|
||||
provider: resolved.ref.provider,
|
||||
cfg,
|
||||
});
|
||||
if (profileResolved.error) {
|
||||
throw new Error(profileResolved.error);
|
||||
}
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
const isDefault =
|
||||
resolved.ref.provider === defaultProvider &&
|
||||
resolved.ref.model === defaultModel;
|
||||
@@ -596,6 +682,11 @@ export async function persistInlineDirectives(params: {
|
||||
sessionEntry.providerOverride = resolved.ref.provider;
|
||||
sessionEntry.modelOverride = resolved.ref.model;
|
||||
}
|
||||
if (profileOverride) {
|
||||
sessionEntry.authProfileOverride = profileOverride;
|
||||
} else if (directives.hasModelDirective) {
|
||||
delete sessionEntry.authProfileOverride;
|
||||
}
|
||||
provider = resolved.ref.provider;
|
||||
model = resolved.ref.model;
|
||||
const nextLabel = `${provider}/${model}`;
|
||||
|
||||
@@ -84,6 +84,7 @@ export function createFollowupRunner(params: {
|
||||
enforceFinalTag: queued.run.enforceFinalTag,
|
||||
provider,
|
||||
model,
|
||||
authProfileId: queued.run.authProfileId,
|
||||
thinkLevel: queued.run.thinkLevel,
|
||||
verboseLevel: queued.run.verboseLevel,
|
||||
bashElevated: queued.run.bashElevated,
|
||||
|
||||
@@ -57,7 +57,8 @@ export async function createModelSelectionState(params: {
|
||||
let provider = params.provider;
|
||||
let model = params.model;
|
||||
|
||||
const hasAllowlist = (agentCfg?.allowedModels?.length ?? 0) > 0;
|
||||
const hasAllowlist =
|
||||
agentCfg?.models && Object.keys(agentCfg.models).length > 0;
|
||||
const hasStoredOverride = Boolean(
|
||||
sessionEntry?.modelOverride || sessionEntry?.providerOverride,
|
||||
);
|
||||
@@ -110,6 +111,27 @@ export async function createModelSelectionState(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sessionEntry &&
|
||||
sessionStore &&
|
||||
sessionKey &&
|
||||
sessionEntry.authProfileOverride
|
||||
) {
|
||||
const { ensureAuthProfileStore } = await import(
|
||||
"../../agents/auth-profiles.js"
|
||||
);
|
||||
const store = ensureAuthProfileStore();
|
||||
const profile = store.profiles[sessionEntry.authProfileOverride];
|
||||
if (!profile || profile.provider !== provider) {
|
||||
delete sessionEntry.authProfileOverride;
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
if (storePath) {
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let defaultThinkingLevel: ThinkLevel | undefined;
|
||||
const resolveDefaultThinkingLevel = async () => {
|
||||
if (defaultThinkingLevel) return defaultThinkingLevel;
|
||||
|
||||
@@ -32,6 +32,7 @@ export type FollowupRun = {
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
provider: string;
|
||||
model: string;
|
||||
authProfileId?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
elevatedLevel?: ElevatedLevel;
|
||||
|
||||
Reference in New Issue
Block a user