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)) {
const profileId = `${provider}:default`;
store.profiles[profileId] = {
...cred,
provider: cred.provider ?? (provider as OAuthProvider),
};
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 } : {}),
};
}
}
return store;
}
@@ -162,17 +178,35 @@ export function ensureAuthProfileStore(): AuthProfileStore {
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
const legacy = coerceLegacyStore(legacyRaw);
const store = legacy
? {
version: AUTH_STORE_VERSION,
profiles: Object.fromEntries(
Object.entries(legacy).map(([provider, cred]) => [
`${provider}:default`,
{ ...cred, provider: cred.provider ?? (provider as OAuthProvider) },
]),
),
const store: AuthProfileStore = {
version: AUTH_STORE_VERSION,
profiles: {},
};
if (legacy) {
for (const [provider, cred] of Object.entries(legacy)) {
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 shouldWrite = legacy !== null || mergedOAuth;
@@ -291,7 +325,7 @@ export function markAuthProfileGood(params: {
const { store, provider, profileId } = params;
const profile = store.profiles[profileId];
if (!profile || profile.provider !== provider) return;
store.lastGood = { ...(store.lastGood ?? {}), [provider]: profileId };
store.lastGood = { ...store.lastGood, [provider]: profileId };
saveAuthProfileStore(store);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,8 +94,13 @@ export function applyMinimaxConfig(cfg: ClawdbotConfig): ClawdbotConfig {
agent: {
...cfg.agent,
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",
},
models,

View File

@@ -244,7 +244,8 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
: {};
const ensureModel = (rawKey?: string) => {
const key = String(rawKey ?? "").trim();
if (typeof rawKey !== "string") return;
const key = rawKey.trim();
if (!key) return;
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 legacyImageModelFallbacks) ensureModel(key);
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)) {
const target = String(targetRaw ?? "").trim();
if (typeof targetRaw !== "string") continue;
const target = targetRaw.trim();
if (!target) continue;
const entry =
models[target] && typeof models[target] === "object"

View File

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

View File

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