fix: clean model config typing

This commit is contained in:
Peter Steinberger
2026-01-06 01:08:36 +00:00
parent b04c838c15
commit e73573eaea
13 changed files with 184 additions and 90 deletions

View File

@@ -143,10 +143,26 @@ export function loadAuthProfileStore(): AuthProfileStore {
}; };
for (const [provider, cred] of Object.entries(legacy)) { for (const [provider, cred] of Object.entries(legacy)) {
const profileId = `${provider}:default`; const profileId = `${provider}:default`;
store.profiles[profileId] = { if (cred.type === "api_key") {
...cred, store.profiles[profileId] = {
provider: cred.provider ?? (provider as OAuthProvider), type: "api_key",
}; provider: cred.provider ?? (provider as OAuthProvider),
key: cred.key,
...(cred.email ? { email: cred.email } : {}),
};
} else {
store.profiles[profileId] = {
type: "oauth",
provider: cred.provider ?? (provider as OAuthProvider),
access: cred.access,
refresh: cred.refresh,
expires: cred.expires,
...(cred.enterpriseUrl ? { enterpriseUrl: cred.enterpriseUrl } : {}),
...(cred.projectId ? { projectId: cred.projectId } : {}),
...(cred.accountId ? { accountId: cred.accountId } : {}),
...(cred.email ? { email: cred.email } : {}),
};
}
} }
return store; return store;
} }
@@ -162,17 +178,35 @@ export function ensureAuthProfileStore(): AuthProfileStore {
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath()); const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
const legacy = coerceLegacyStore(legacyRaw); const legacy = coerceLegacyStore(legacyRaw);
const store = legacy const store: AuthProfileStore = {
? { version: AUTH_STORE_VERSION,
version: AUTH_STORE_VERSION, profiles: {},
profiles: Object.fromEntries( };
Object.entries(legacy).map(([provider, cred]) => [ if (legacy) {
`${provider}:default`, for (const [provider, cred] of Object.entries(legacy)) {
{ ...cred, provider: cred.provider ?? (provider as OAuthProvider) }, const profileId = `${provider}:default`;
]), if (cred.type === "api_key") {
), store.profiles[profileId] = {
type: "api_key",
provider: cred.provider ?? (provider as OAuthProvider),
key: cred.key,
...(cred.email ? { email: cred.email } : {}),
};
} else {
store.profiles[profileId] = {
type: "oauth",
provider: cred.provider ?? (provider as OAuthProvider),
access: cred.access,
refresh: cred.refresh,
expires: cred.expires,
...(cred.enterpriseUrl ? { enterpriseUrl: cred.enterpriseUrl } : {}),
...(cred.projectId ? { projectId: cred.projectId } : {}),
...(cred.accountId ? { accountId: cred.accountId } : {}),
...(cred.email ? { email: cred.email } : {}),
};
} }
: { version: AUTH_STORE_VERSION, profiles: {} }; }
}
const mergedOAuth = mergeOAuthFileIntoStore(store); const mergedOAuth = mergeOAuthFileIntoStore(store);
const shouldWrite = legacy !== null || mergedOAuth; const shouldWrite = legacy !== null || mergedOAuth;
@@ -291,7 +325,7 @@ export function markAuthProfileGood(params: {
const { store, provider, profileId } = params; const { store, provider, profileId } = params;
const profile = store.profiles[profileId]; const profile = store.profiles[profileId];
if (!profile || profile.provider !== provider) return; if (!profile || profile.provider !== provider) return;
store.lastGood = { ...(store.lastGood ?? {}), [provider]: profileId }; store.lastGood = { ...store.lastGood, [provider]: profileId };
saveAuthProfileStore(store); saveAuthProfileStore(store);
} }

View File

@@ -82,6 +82,12 @@ export type EmbeddedPiRunMeta = {
aborted?: boolean; aborted?: boolean;
}; };
type ApiKeyInfo = {
apiKey: string;
profileId?: string;
source: string;
};
export type EmbeddedPiRunResult = { export type EmbeddedPiRunResult = {
payloads?: Array<{ payloads?: Array<{
text?: string; text?: string;
@@ -396,8 +402,8 @@ export async function runEmbeddedPiAgent(params: {
const initialThinkLevel = params.thinkLevel ?? "off"; const initialThinkLevel = params.thinkLevel ?? "off";
let thinkLevel = initialThinkLevel; let thinkLevel = initialThinkLevel;
const attemptedThinking = new Set<ThinkLevel>(); const attemptedThinking = new Set<ThinkLevel>();
let apiKeyInfo: Awaited<ReturnType<typeof getApiKeyForModel>> | null = let apiKeyInfo: ApiKeyInfo | null = null;
null; let lastProfileId: string | undefined;
const resolveApiKeyForCandidate = async (candidate?: string) => { const resolveApiKeyForCandidate = async (candidate?: string) => {
return getApiKeyForModel({ return getApiKeyForModel({
@@ -411,6 +417,7 @@ export async function runEmbeddedPiAgent(params: {
const applyApiKeyInfo = async (candidate?: string): Promise<void> => { const applyApiKeyInfo = async (candidate?: string): Promise<void> => {
apiKeyInfo = await resolveApiKeyForCandidate(candidate); apiKeyInfo = await resolveApiKeyForCandidate(candidate);
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey); authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
lastProfileId = apiKeyInfo.profileId;
}; };
const advanceAuthProfile = async (): Promise<boolean> => { const advanceAuthProfile = async (): Promise<boolean> => {
@@ -802,11 +809,11 @@ export async function runEmbeddedPiAgent(params: {
log.debug( log.debug(
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`, `embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
); );
if (apiKeyInfo?.profileId) { if (lastProfileId) {
markAuthProfileGood({ markAuthProfileGood({
store: authStore, store: authStore,
provider, provider,
profileId: apiKeyInfo.profileId, profileId: lastProfileId,
}); });
} }
return { return {

View File

@@ -337,7 +337,10 @@ export async function handleCommands(params: {
const statusText = buildStatusMessage({ const statusText = buildStatusMessage({
agent: { agent: {
...cfg.agent, ...cfg.agent,
model, model: {
...cfg.agent?.model,
primary: model,
},
contextTokens, contextTokens,
thinkingDefault: cfg.agent?.thinkingDefault, thinkingDefault: cfg.agent?.thinkingDefault,
verboseDefault: cfg.agent?.verboseDefault, verboseDefault: cfg.agent?.verboseDefault,

View File

@@ -333,10 +333,13 @@ async function promptAuthConfig(
agent: { agent: {
...next.agent, ...next.agent,
model: { model: {
...((next.agent?.model as { ...(next.agent?.model &&
primary?: string; "fallbacks" in (next.agent.model as Record<string, unknown>)
fallbacks?: string[]; ? {
}) ?? {}), fallbacks: (next.agent.model as { fallbacks?: string[] })
.fallbacks,
}
: undefined),
primary: "google-antigravity/claude-opus-4-5-thinking", primary: "google-antigravity/claude-opus-4-5-thinking",
}, },
models: { models: {
@@ -392,10 +395,13 @@ async function promptAuthConfig(
agent: { agent: {
...next.agent, ...next.agent,
model: { model: {
...((next.agent?.model as { ...(next.agent?.model &&
primary?: string; "fallbacks" in (next.agent.model as Record<string, unknown>)
fallbacks?: string[]; ? {
}) ?? {}), fallbacks: (next.agent.model as { fallbacks?: string[] })
.fallbacks,
}
: undefined),
primary: model, primary: model,
}, },
models: { models: {

View File

@@ -64,15 +64,18 @@ export async function modelsFallbacksAddCommand(
if (existingKeys.includes(targetKey)) return cfg; if (existingKeys.includes(targetKey)) return cfg;
const existingModel = cfg.agent?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
model: { model: {
...((cfg.agent?.model as { ...(existingModel?.primary
primary?: string; ? { primary: existingModel.primary }
fallbacks?: string[]; : undefined),
}) ?? {}),
fallbacks: [...existing, targetKey], fallbacks: [...existing, targetKey],
}, },
models: nextModels, models: nextModels,
@@ -115,15 +118,18 @@ export async function modelsFallbacksRemoveCommand(
throw new Error(`Fallback not found: ${targetKey}`); throw new Error(`Fallback not found: ${targetKey}`);
} }
const existingModel = cfg.agent?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
model: { model: {
...((cfg.agent?.model as { ...(existingModel?.primary
primary?: string; ? { primary: existingModel.primary }
fallbacks?: string[]; : undefined),
}) ?? {}),
fallbacks: filtered, fallbacks: filtered,
}, },
}, },
@@ -137,17 +143,23 @@ export async function modelsFallbacksRemoveCommand(
} }
export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) { export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
await updateConfig((cfg) => ({ await updateConfig((cfg) => {
...cfg, const existingModel = cfg.agent?.model as
agent: { | { primary?: string; fallbacks?: string[] }
...cfg.agent, | undefined;
model: { return {
...((cfg.agent?.model as { primary?: string; fallbacks?: string[] }) ?? ...cfg,
{}), agent: {
fallbacks: [], ...cfg.agent,
model: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
fallbacks: [],
},
}, },
}, };
})); });
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log("Fallback list cleared."); runtime.log("Fallback list cleared.");

View File

@@ -64,15 +64,18 @@ export async function modelsImageFallbacksAddCommand(
if (existingKeys.includes(targetKey)) return cfg; if (existingKeys.includes(targetKey)) return cfg;
const existingModel = cfg.agent?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
imageModel: { imageModel: {
...((cfg.agent?.imageModel as { ...(existingModel?.primary
primary?: string; ? { primary: existingModel.primary }
fallbacks?: string[]; : undefined),
}) ?? {}),
fallbacks: [...existing, targetKey], fallbacks: [...existing, targetKey],
}, },
models: nextModels, models: nextModels,
@@ -115,15 +118,18 @@ export async function modelsImageFallbacksRemoveCommand(
throw new Error(`Image fallback not found: ${targetKey}`); throw new Error(`Image fallback not found: ${targetKey}`);
} }
const existingModel = cfg.agent?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
imageModel: { imageModel: {
...((cfg.agent?.imageModel as { ...(existingModel?.primary
primary?: string; ? { primary: existingModel.primary }
fallbacks?: string[]; : undefined),
}) ?? {}),
fallbacks: filtered, fallbacks: filtered,
}, },
}, },
@@ -137,19 +143,23 @@ export async function modelsImageFallbacksRemoveCommand(
} }
export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) { export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) {
await updateConfig((cfg) => ({ await updateConfig((cfg) => {
...cfg, const existingModel = cfg.agent?.imageModel as
agent: { | { primary?: string; fallbacks?: string[] }
...cfg.agent, | undefined;
imageModel: { return {
...((cfg.agent?.imageModel as { ...cfg,
primary?: string; agent: {
fallbacks?: string[]; ...cfg.agent,
}) ?? {}), imageModel: {
fallbacks: [], ...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
fallbacks: [],
},
}, },
}, };
})); });
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`); runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log("Image fallback list cleared."); runtime.log("Image fallback list cleared.");

View File

@@ -275,22 +275,28 @@ export async function modelsScanCommand(
for (const entry of selectedImages) { for (const entry of selectedImages) {
if (!nextModels[entry]) nextModels[entry] = {}; if (!nextModels[entry]) nextModels[entry] = {};
} }
const existingImageModel = cfg.agent?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
const nextImageModel = const nextImageModel =
selectedImages.length > 0 selectedImages.length > 0
? { ? {
...((cfg.agent?.imageModel as { ...(existingImageModel?.primary
primary?: string; ? { primary: existingImageModel.primary }
fallbacks?: string[]; : undefined),
}) ?? {}),
fallbacks: selectedImages, fallbacks: selectedImages,
...(opts.setImage ? { primary: selectedImages[0] } : {}), ...(opts.setImage ? { primary: selectedImages[0] } : {}),
} }
: cfg.agent?.imageModel; : cfg.agent?.imageModel;
const existingModel = cfg.agent?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
const agent = { const agent = {
...cfg.agent, ...cfg.agent,
model: { model: {
...((cfg.agent?.model as { primary?: string; fallbacks?: string[] }) ?? ...(existingModel?.primary
{}), ? { primary: existingModel.primary }
: undefined),
fallbacks: selected, fallbacks: selected,
...(opts.setDefault ? { primary: selected[0] } : {}), ...(opts.setDefault ? { primary: selected[0] } : {}),
}, },

View File

@@ -11,15 +11,17 @@ export async function modelsSetImageCommand(
const key = `${resolved.provider}/${resolved.model}`; const key = `${resolved.provider}/${resolved.model}`;
const nextModels = { ...cfg.agent?.models }; const nextModels = { ...cfg.agent?.models };
if (!nextModels[key]) nextModels[key] = {}; if (!nextModels[key]) nextModels[key] = {};
const existingModel = cfg.agent?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
imageModel: { imageModel: {
...((cfg.agent?.imageModel as { ...(existingModel?.fallbacks
primary?: string; ? { fallbacks: existingModel.fallbacks }
fallbacks?: string[]; : undefined),
}) ?? {}),
primary: key, primary: key,
}, },
models: nextModels, models: nextModels,

View File

@@ -8,15 +8,17 @@ export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) {
const key = `${resolved.provider}/${resolved.model}`; const key = `${resolved.provider}/${resolved.model}`;
const nextModels = { ...cfg.agent?.models }; const nextModels = { ...cfg.agent?.models };
if (!nextModels[key]) nextModels[key] = {}; if (!nextModels[key]) nextModels[key] = {};
const existingModel = cfg.agent?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
return { return {
...cfg, ...cfg,
agent: { agent: {
...cfg.agent, ...cfg.agent,
model: { model: {
...((cfg.agent?.model as { ...(existingModel?.fallbacks
primary?: string; ? { fallbacks: existingModel.fallbacks }
fallbacks?: string[]; : undefined),
}) ?? {}),
primary: key, primary: key,
}, },
models: nextModels, models: nextModels,

View File

@@ -94,8 +94,13 @@ export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig {
agent: { agent: {
...cfg.agent, ...cfg.agent,
model: { model: {
...((cfg.agent?.model as { primary?: string; fallbacks?: string[] }) ?? ...(cfg.agent?.model &&
{}), "fallbacks" in (cfg.agent.model as Record<string, unknown>)
? {
fallbacks: (cfg.agent.model as { fallbacks?: string[] })
.fallbacks,
}
: undefined),
primary: "lmstudio/minimax-m2.1-gs32", primary: "lmstudio/minimax-m2.1-gs32",
}, },
models, models,

View File

@@ -244,7 +244,8 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
: {}; : {};
const ensureModel = (rawKey?: string) => { const ensureModel = (rawKey?: string) => {
const key = String(rawKey ?? "").trim(); if (typeof rawKey !== "string") return;
const key = rawKey.trim();
if (!key) return; if (!key) return;
if (!models[key]) models[key] = {}; if (!models[key]) models[key] = {};
}; };
@@ -255,11 +256,13 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
for (const key of legacyModelFallbacks) ensureModel(key); for (const key of legacyModelFallbacks) ensureModel(key);
for (const key of legacyImageModelFallbacks) ensureModel(key); for (const key of legacyImageModelFallbacks) ensureModel(key);
for (const target of Object.values(legacyAliases)) { for (const target of Object.values(legacyAliases)) {
ensureModel(String(target ?? "")); if (typeof target !== "string") continue;
ensureModel(target);
} }
for (const [alias, targetRaw] of Object.entries(legacyAliases)) { for (const [alias, targetRaw] of Object.entries(legacyAliases)) {
const target = String(targetRaw ?? "").trim(); if (typeof targetRaw !== "string") continue;
const target = targetRaw.trim();
if (!target) continue; if (!target) continue;
const entry = const entry =
models[target] && typeof models[target] === "object" models[target] && typeof models[target] === "object"

View File

@@ -49,7 +49,7 @@ vi.mock("discord.js", () => {
} }
emit(event: string, ...args: unknown[]) { emit(event: string, ...args: unknown[]) {
for (const handler of handlers.get(event) ?? []) { for (const handler of handlers.get(event) ?? []) {
void Promise.resolve(handler(...args)); Promise.resolve(handler(...args)).catch(() => {});
} }
} }
login = vi.fn().mockResolvedValue(undefined); login = vi.fn().mockResolvedValue(undefined);

View File

@@ -337,10 +337,14 @@ export async function runOnboardingWizard(
agent: { agent: {
...nextConfig.agent, ...nextConfig.agent,
model: { model: {
...((nextConfig.agent?.model as { ...(nextConfig.agent?.model &&
primary?: string; "fallbacks" in (nextConfig.agent.model as Record<string, unknown>)
fallbacks?: string[]; ? {
}) ?? {}), fallbacks: (
nextConfig.agent.model as { fallbacks?: string[] }
).fallbacks,
}
: undefined),
primary: "google-antigravity/claude-opus-4-5-thinking", primary: "google-antigravity/claude-opus-4-5-thinking",
}, },
models: { models: {