From e73573eaeacabd6e6c9facb8f563df27c42cdc7a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 01:08:36 +0000 Subject: [PATCH] fix: clean model config typing --- src/agents/auth-profiles.ts | 64 +++++++++++++++++++------ src/agents/pi-embedded-runner.ts | 15 ++++-- src/auto-reply/reply/commands.ts | 5 +- src/commands/configure.ts | 22 +++++---- src/commands/models/fallbacks.ts | 48 ++++++++++++------- src/commands/models/image-fallbacks.ts | 50 +++++++++++-------- src/commands/models/scan.ts | 18 ++++--- src/commands/models/set-image.ts | 10 ++-- src/commands/models/set.ts | 10 ++-- src/commands/onboard-auth.ts | 9 +++- src/config/legacy.ts | 9 ++-- src/discord/monitor.tool-result.test.ts | 2 +- src/wizard/onboarding.ts | 12 +++-- 13 files changed, 184 insertions(+), 90 deletions(-) diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index f3019f755..df8b1b452 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -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); } diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 28bd4bec6..473f89175 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -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(); - let apiKeyInfo: Awaited> | 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 => { apiKeyInfo = await resolveApiKeyForCandidate(candidate); authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey); + lastProfileId = apiKeyInfo.profileId; }; const advanceAuthProfile = async (): Promise => { @@ -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 { diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index d8042911b..ad2778b42 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -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, diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 9d61541d4..e2b8b6451 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -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) + ? { + 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) + ? { + fallbacks: (next.agent.model as { fallbacks?: string[] }) + .fallbacks, + } + : undefined), primary: model, }, models: { diff --git a/src/commands/models/fallbacks.ts b/src/commands/models/fallbacks.ts index 3722fabfa..c5ac94f4d 100644 --- a/src/commands/models/fallbacks.ts +++ b/src/commands/models/fallbacks.ts @@ -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."); diff --git a/src/commands/models/image-fallbacks.ts b/src/commands/models/image-fallbacks.ts index 5fcff8bd4..25ea316ec 100644 --- a/src/commands/models/image-fallbacks.ts +++ b/src/commands/models/image-fallbacks.ts @@ -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."); diff --git a/src/commands/models/scan.ts b/src/commands/models/scan.ts index 416a220de..6a2a058e2 100644 --- a/src/commands/models/scan.ts +++ b/src/commands/models/scan.ts @@ -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] } : {}), }, diff --git a/src/commands/models/set-image.ts b/src/commands/models/set-image.ts index 46214ee9a..ed7a3e0db 100644 --- a/src/commands/models/set-image.ts +++ b/src/commands/models/set-image.ts @@ -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, diff --git a/src/commands/models/set.ts b/src/commands/models/set.ts index d8546c484..0cfc9cdc3 100644 --- a/src/commands/models/set.ts +++ b/src/commands/models/set.ts @@ -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, diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 94a272958..3da496b34 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -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) + ? { + fallbacks: (cfg.agent.model as { fallbacks?: string[] }) + .fallbacks, + } + : undefined), primary: "lmstudio/minimax-m2.1-gs32", }, models, diff --git a/src/config/legacy.ts b/src/config/legacy.ts index 10f9cfe13..1955955c3 100644 --- a/src/config/legacy.ts +++ b/src/config/legacy.ts @@ -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" diff --git a/src/discord/monitor.tool-result.test.ts b/src/discord/monitor.tool-result.test.ts index b9314b5a2..2d000c61d 100644 --- a/src/discord/monitor.tool-result.test.ts +++ b/src/discord/monitor.tool-result.test.ts @@ -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); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index b4f30e6a4..883d14614 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -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) + ? { + fallbacks: ( + nextConfig.agent.model as { fallbacks?: string[] } + ).fallbacks, + } + : undefined), primary: "google-antigravity/claude-opus-4-5-thinking", }, models: {