From 61b7398cb70d20329c6cdfa335aca7a39da8a9bf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 05:24:41 +0000 Subject: [PATCH] refactor: centralize auth-choice model defaults --- src/agents/models-config.providers.ts | 62 ++++++ src/agents/models-config.ts | 69 +------ src/commands/auth-choice.test.ts | 20 +- src/commands/auth-choice.ts | 265 +++++++++++++++----------- src/discord/monitor.slash.test.ts | 1 + 5 files changed, 238 insertions(+), 179 deletions(-) diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 28aea65e17..c1b96063ec 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -1,4 +1,8 @@ import type { ClawdbotConfig } from "../config/config.js"; +import { + DEFAULT_COPILOT_API_BASE_URL, + resolveCopilotApiToken, +} from "../providers/github-copilot-token.js"; import { ensureAuthProfileStore, listProfilesForProvider, @@ -224,3 +228,61 @@ export function resolveImplicitProviders(params: { return providers; } + +export async function resolveImplicitCopilotProvider(params: { + agentDir: string; + env?: NodeJS.ProcessEnv; +}): Promise { + const env = params.env ?? process.env; + const authStore = ensureAuthProfileStore(params.agentDir); + const hasProfile = + listProfilesForProvider(authStore, "github-copilot").length > 0; + const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN; + const githubToken = (envToken ?? "").trim(); + + if (!hasProfile && !githubToken) return null; + + let selectedGithubToken = githubToken; + if (!selectedGithubToken && hasProfile) { + // Use the first available profile as a default for discovery (it will be + // re-resolved per-run by the embedded runner). + const profileId = listProfilesForProvider(authStore, "github-copilot")[0]; + const profile = profileId ? authStore.profiles[profileId] : undefined; + if (profile && profile.type === "token") { + selectedGithubToken = profile.token; + } + } + + let baseUrl = DEFAULT_COPILOT_API_BASE_URL; + if (selectedGithubToken) { + try { + const token = await resolveCopilotApiToken({ + githubToken: selectedGithubToken, + env, + }); + baseUrl = token.baseUrl; + } catch { + baseUrl = DEFAULT_COPILOT_API_BASE_URL; + } + } + + // pi-coding-agent's ModelRegistry marks a model "available" only if its + // `AuthStorage` has auth configured for that provider (via auth.json/env/etc). + // Our Copilot auth lives in Clawdbot's auth-profiles store instead, so we also + // write a runtime-only auth.json entry for pi-coding-agent to pick up. + // + // This is safe because it's (1) within Clawdbot's agent dir, (2) contains the + // GitHub token (not the exchanged Copilot token), and (3) matches existing + // patterns for OAuth-like providers in pi-coding-agent. + // Note: we deliberately do not write pi-coding-agent's `auth.json` here. + // Clawdbot uses its own auth store and exchanges tokens at runtime. + // `models list` uses Clawdbot's auth heuristics for availability. + + // We intentionally do NOT define custom models for Copilot in models.json. + // pi-coding-agent treats providers with models as replacements requiring apiKey. + // We only override baseUrl; the model list comes from pi-ai built-ins. + return { + baseUrl, + models: [], + } satisfies ProviderConfig; +} diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 28fc73723f..4f9ad05e3d 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -2,18 +2,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import { type ClawdbotConfig, loadConfig } from "../config/config.js"; -import { - DEFAULT_COPILOT_API_BASE_URL, - resolveCopilotApiToken, -} from "../providers/github-copilot-token.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; -import { - ensureAuthProfileStore, - listProfilesForProvider, -} from "./auth-profiles.js"; import { normalizeProviders, type ProviderConfig, + resolveImplicitCopilotProvider, resolveImplicitProviders, } from "./models-config.providers.js"; @@ -85,64 +78,6 @@ async function readJson(pathname: string): Promise { } } -async function maybeBuildCopilotProvider(params: { - agentDir: string; - env?: NodeJS.ProcessEnv; -}): Promise { - const env = params.env ?? process.env; - const authStore = ensureAuthProfileStore(params.agentDir); - const hasProfile = - listProfilesForProvider(authStore, "github-copilot").length > 0; - const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN; - const githubToken = (envToken ?? "").trim(); - - if (!hasProfile && !githubToken) return null; - - let selectedGithubToken = githubToken; - if (!selectedGithubToken && hasProfile) { - // Use the first available profile as a default for discovery (it will be - // re-resolved per-run by the embedded runner). - const profileId = listProfilesForProvider(authStore, "github-copilot")[0]; - const profile = profileId ? authStore.profiles[profileId] : undefined; - if (profile && profile.type === "token") { - selectedGithubToken = profile.token; - } - } - - let baseUrl = DEFAULT_COPILOT_API_BASE_URL; - if (selectedGithubToken) { - try { - const token = await resolveCopilotApiToken({ - githubToken: selectedGithubToken, - env, - }); - baseUrl = token.baseUrl; - } catch { - baseUrl = DEFAULT_COPILOT_API_BASE_URL; - } - } - - // pi-coding-agent's ModelRegistry marks a model "available" only if its - // `AuthStorage` has auth configured for that provider (via auth.json/env/etc). - // Our Copilot auth lives in Clawdbot's auth-profiles store instead, so we also - // write a runtime-only auth.json entry for pi-coding-agent to pick up. - // - // This is safe because it's (1) within Clawdbot's agent dir, (2) contains the - // GitHub token (not the exchanged Copilot token), and (3) matches existing - // patterns for OAuth-like providers in pi-coding-agent. - // Note: we deliberately do not write pi-coding-agent's `auth.json` here. - // Clawdbot uses its own auth store and exchanges tokens at runtime. - // `models list` uses Clawdbot's auth heuristics for availability. - - // We intentionally do NOT define custom models for Copilot in models.json. - // pi-coding-agent treats providers with models as replacements requiring apiKey. - // We only override baseUrl; the model list comes from pi-ai built-ins. - return { - baseUrl, - models: [], - } satisfies ProviderConfig; -} - export async function ensureClawdbotModelsJson( config?: ClawdbotConfig, agentDirOverride?: string, @@ -161,7 +96,7 @@ export async function ensureClawdbotModelsJson( implicit: implicitProviders, explicit: explicitProviders, }); - const implicitCopilot = await maybeBuildCopilotProvider({ agentDir }); + const implicitCopilot = await resolveImplicitCopilotProvider({ agentDir }); if (implicitCopilot && !providers["github-copilot"]) { providers["github-copilot"] = implicitCopilot; } diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 14da2bef6f..441d954bcd 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -6,7 +6,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; -import { applyAuthChoice } from "./auth-choice.js"; +import { + applyAuthChoice, + resolvePreferredProviderForAuthChoice, +} from "./auth-choice.js"; +import type { AuthChoice } from "./onboard-types.js"; vi.mock("../providers/github-copilot-auth.js", () => ({ githubCopilotLoginCommand: vi.fn(async () => {}), @@ -444,3 +448,17 @@ describe("applyAuthChoice", () => { }); }); }); + +describe("resolvePreferredProviderForAuthChoice", () => { + it("maps github-copilot to the provider", () => { + expect(resolvePreferredProviderForAuthChoice("github-copilot")).toBe( + "github-copilot", + ); + }); + + it("returns undefined for unknown choices", () => { + expect( + resolvePreferredProviderForAuthChoice("unknown" as AuthChoice), + ).toBeUndefined(); + }); +}); diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 4318c9cfb4..4bb6cce48f 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -120,6 +120,32 @@ function formatApiKeyPreview( return `${trimmed.slice(0, head)}…${trimmed.slice(-tail)}`; } +async function applyDefaultModelChoice(params: { + config: ClawdbotConfig; + setDefaultModel: boolean; + defaultModel: string; + applyDefaultConfig: (config: ClawdbotConfig) => ClawdbotConfig; + applyProviderConfig: (config: ClawdbotConfig) => ClawdbotConfig; + noteDefault?: string; + noteAgentModel: (model: string) => Promise; + prompter: WizardPrompter; +}): Promise<{ config: ClawdbotConfig; agentModelOverride?: string }> { + if (params.setDefaultModel) { + const next = params.applyDefaultConfig(params.config); + if (params.noteDefault) { + await params.prompter.note( + `Default model set to ${params.noteDefault}`, + "Model configured", + ); + } + return { config: next }; + } + + const next = params.applyProviderConfig(params.config); + await params.noteAgentModel(params.defaultModel); + return { config: next, agentModelOverride: params.defaultModel }; +} + export async function warnIfModelConfigLooksOff( config: ClawdbotConfig, prompter: WizardPrompter, @@ -487,16 +513,19 @@ export async function applyAuthChoice(params: { mode, }); } - if (params.setDefaultModel) { - nextConfig = applyOpenrouterConfig(nextConfig); - await params.prompter.note( - `Default model set to ${OPENROUTER_DEFAULT_MODEL_REF}`, - "Model configured", - ); - } else { - nextConfig = applyOpenrouterProviderConfig(nextConfig); - agentModelOverride = OPENROUTER_DEFAULT_MODEL_REF; - await noteAgentModel(OPENROUTER_DEFAULT_MODEL_REF); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: OPENROUTER_DEFAULT_MODEL_REF, + applyDefaultConfig: applyOpenrouterConfig, + applyProviderConfig: applyOpenrouterProviderConfig, + noteDefault: OPENROUTER_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "moonshot-api-key") { let hasCredential = false; @@ -526,12 +555,18 @@ export async function applyAuthChoice(params: { provider: "moonshot", mode: "api_key", }); - if (params.setDefaultModel) { - nextConfig = applyMoonshotConfig(nextConfig); - } else { - nextConfig = applyMoonshotProviderConfig(nextConfig); - agentModelOverride = MOONSHOT_DEFAULT_MODEL_REF; - await noteAgentModel(MOONSHOT_DEFAULT_MODEL_REF); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: MOONSHOT_DEFAULT_MODEL_REF, + applyDefaultConfig: applyMoonshotConfig, + applyProviderConfig: applyMoonshotProviderConfig, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "chutes") { const isRemote = isRemoteEnvironment(); @@ -867,33 +902,36 @@ export async function applyAuthChoice(params: { provider: "zai", mode: "api_key", }); - if (params.setDefaultModel) { - nextConfig = applyZaiConfig(nextConfig); - await params.prompter.note( - `Default model set to ${ZAI_DEFAULT_MODEL_REF}`, - "Model configured", - ); - } else { - nextConfig = { - ...nextConfig, - agents: { - ...nextConfig.agents, - defaults: { - ...nextConfig.agents?.defaults, - models: { - ...nextConfig.agents?.defaults?.models, - [ZAI_DEFAULT_MODEL_REF]: { - ...nextConfig.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF], - alias: - nextConfig.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF] - ?.alias ?? "GLM", + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: ZAI_DEFAULT_MODEL_REF, + applyDefaultConfig: applyZaiConfig, + applyProviderConfig: (config) => ({ + ...config, + agents: { + ...config.agents, + defaults: { + ...config.agents?.defaults, + models: { + ...config.agents?.defaults?.models, + [ZAI_DEFAULT_MODEL_REF]: { + ...config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF], + alias: + config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF] + ?.alias ?? "GLM", + }, }, }, }, - }, - }; - agentModelOverride = ZAI_DEFAULT_MODEL_REF; - await noteAgentModel(ZAI_DEFAULT_MODEL_REF); + }), + noteDefault: ZAI_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "synthetic-api-key") { const key = await params.prompter.text({ @@ -906,16 +944,19 @@ export async function applyAuthChoice(params: { provider: "synthetic", mode: "api_key", }); - if (params.setDefaultModel) { - nextConfig = applySyntheticConfig(nextConfig); - await params.prompter.note( - `Default model set to ${SYNTHETIC_DEFAULT_MODEL_REF}`, - "Model configured", - ); - } else { - nextConfig = applySyntheticProviderConfig(nextConfig); - agentModelOverride = SYNTHETIC_DEFAULT_MODEL_REF; - await noteAgentModel(SYNTHETIC_DEFAULT_MODEL_REF); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: SYNTHETIC_DEFAULT_MODEL_REF, + applyDefaultConfig: applySyntheticConfig, + applyProviderConfig: applySyntheticProviderConfig, + noteDefault: SYNTHETIC_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "apiKey") { let hasCredential = false; @@ -981,13 +1022,20 @@ export async function applyAuthChoice(params: { provider: "minimax", mode: "api_key", }); - if (params.setDefaultModel) { - nextConfig = applyMinimaxApiConfig(nextConfig, modelId); - } else { + { const modelRef = `minimax/${modelId}`; - nextConfig = applyMinimaxApiProviderConfig(nextConfig, modelId); - agentModelOverride = modelRef; - await noteAgentModel(modelRef); + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: modelRef, + applyDefaultConfig: (config) => applyMinimaxApiConfig(config, modelId), + applyProviderConfig: (config) => + applyMinimaxApiProviderConfig(config, modelId), + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "github-copilot") { await params.prompter.note( @@ -1045,12 +1093,18 @@ export async function applyAuthChoice(params: { ); } } else if (params.authChoice === "minimax") { - if (params.setDefaultModel) { - nextConfig = applyMinimaxConfig(nextConfig); - } else { - nextConfig = applyMinimaxProviderConfig(nextConfig); - agentModelOverride = "lmstudio/minimax-m2.1-gs32"; - await noteAgentModel("lmstudio/minimax-m2.1-gs32"); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: "lmstudio/minimax-m2.1-gs32", + applyDefaultConfig: applyMinimaxConfig, + applyProviderConfig: applyMinimaxProviderConfig, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } else if (params.authChoice === "opencode-zen") { await params.prompter.note( @@ -1088,16 +1142,19 @@ export async function applyAuthChoice(params: { provider: "opencode", mode: "api_key", }); - if (params.setDefaultModel) { - nextConfig = applyOpencodeZenConfig(nextConfig); - await params.prompter.note( - `Default model set to ${OPENCODE_ZEN_DEFAULT_MODEL}`, - "Model configured", - ); - } else { - nextConfig = applyOpencodeZenProviderConfig(nextConfig); - agentModelOverride = OPENCODE_ZEN_DEFAULT_MODEL; - await noteAgentModel(OPENCODE_ZEN_DEFAULT_MODEL); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: OPENCODE_ZEN_DEFAULT_MODEL, + applyDefaultConfig: applyOpencodeZenConfig, + applyProviderConfig: applyOpencodeZenProviderConfig, + noteDefault: OPENCODE_ZEN_DEFAULT_MODEL, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; } } @@ -1107,43 +1164,29 @@ export async function applyAuthChoice(params: { export function resolvePreferredProviderForAuthChoice( choice: AuthChoice, ): string | undefined { - switch (choice) { - case "oauth": - case "setup-token": - case "claude-cli": - case "token": - case "apiKey": - return "anthropic"; - case "openai-codex": - case "codex-cli": - return "openai-codex"; - case "chutes": - return "chutes"; - case "openai-api-key": - return "openai"; - case "openrouter-api-key": - return "openrouter"; - case "moonshot-api-key": - return "moonshot"; - case "gemini-api-key": - return "google"; - case "zai-api-key": - return "zai"; - case "antigravity": - return "google-antigravity"; - case "synthetic-api-key": - return "synthetic"; - case "github-copilot": - return "github-copilot"; - case "minimax-cloud": - case "minimax-api": - case "minimax-api-lightning": - return "minimax"; - case "minimax": - return "lmstudio"; - case "opencode-zen": - return "opencode"; - default: - return undefined; - } + return PREFERRED_PROVIDER_BY_AUTH_CHOICE[choice]; } + +const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { + oauth: "anthropic", + "setup-token": "anthropic", + "claude-cli": "anthropic", + token: "anthropic", + apiKey: "anthropic", + "openai-codex": "openai-codex", + "codex-cli": "openai-codex", + chutes: "chutes", + "openai-api-key": "openai", + "openrouter-api-key": "openrouter", + "moonshot-api-key": "moonshot", + "gemini-api-key": "google", + "zai-api-key": "zai", + antigravity: "google-antigravity", + "synthetic-api-key": "synthetic", + "github-copilot": "github-copilot", + "minimax-cloud": "minimax", + "minimax-api": "minimax", + "minimax-api-lightning": "minimax", + minimax: "lmstudio", + "opencode-zen": "opencode", +}; diff --git a/src/discord/monitor.slash.test.ts b/src/discord/monitor.slash.test.ts index 251d966946..027ab211b1 100644 --- a/src/discord/monitor.slash.test.ts +++ b/src/discord/monitor.slash.test.ts @@ -40,6 +40,7 @@ describe("discord native commands", () => { agents: { defaults: { model: "anthropic/claude-opus-4-5", + humanDelay: { mode: "off" }, workspace: "/tmp/clawd", }, },