diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..7ec9c0c0e 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -21,7 +21,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "ollama"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -120,6 +121,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["opencode-zen"], }, + { + value: "ollama", + label: "Ollama", + hint: "Local + cloud models", + choices: ["ollama"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -194,6 +201,11 @@ export function buildAuthChoiceOptions(params: { label: "MiniMax M2.1 Lightning", hint: "Faster, higher output cost", }); + options.push({ + value: "ollama", + label: "Ollama", + hint: "Sign-in is handled by Ollama when required", + }); if (params.includeSkip) { options.push({ value: "skip", label: "Skip for now" }); } diff --git a/src/commands/auth-choice.apply.ollama.ts b/src/commands/auth-choice.apply.ollama.ts new file mode 100644 index 000000000..7860a6469 --- /dev/null +++ b/src/commands/auth-choice.apply.ollama.ts @@ -0,0 +1,73 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { + applyAuthProfileConfig, + applyOllamaProviderConfig, + OLLAMA_BASE_URL, + OLLAMA_DEFAULT_API_KEY, + setOllamaApiKey, +} from "./onboard-auth.js"; + +export async function applyAuthChoiceOllama( + params: ApplyAuthChoiceParams, +): Promise { + if (params.authChoice !== "ollama") { + return null; + } + + let nextConfig = params.config; + + await params.prompter.note( + [ + "Ollama runs models locally or in the cloud.", + "Make sure Ollama is installed and running: https://ollama.com", + "Default server: http://127.0.0.1:11434", + ].join("\n"), + "Ollama", + ); + + const useDefault = await params.prompter.confirm({ + message: `Use default Ollama server (${OLLAMA_BASE_URL.replace("/v1", "")})?`, + initialValue: true, + }); + + let baseUrl = OLLAMA_BASE_URL; + if (!useDefault) { + const customUrl = await params.prompter.text({ + message: "Enter Ollama server URL (e.g., http://192.168.1.100:11434)", + validate: (value) => { + if (!value?.trim()) return "URL is required"; + try { + new URL(value.trim()); + return undefined; + } catch { + return "Invalid URL format"; + } + }, + }); + // Append /v1 if not present for OpenAI-compatible endpoint + const trimmedUrl = String(customUrl).trim().replace(/\/+$/, ""); + baseUrl = trimmedUrl.endsWith("/v1") ? trimmedUrl : `${trimmedUrl}/v1`; + } + + // Store the placeholder API key to enable provider discovery + await setOllamaApiKey(OLLAMA_DEFAULT_API_KEY, params.agentDir); + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "ollama:default", + provider: "ollama", + mode: "api_key", + }); + + nextConfig = applyOllamaProviderConfig(nextConfig, { baseUrl }); + + await params.prompter.note( + [ + "Ollama configured successfully.", + "Models will be discovered automatically from your Ollama server.", + "Use `ollama pull ` to download models, then `moltbot models list` to see them.", + ].join("\n"), + "Setup complete", + ); + + return { config: nextConfig }; +} diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index c36a3981a..47f45b332 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -9,6 +9,7 @@ import { applyAuthChoiceGoogleAntigravity } from "./auth-choice.apply.google-ant import { applyAuthChoiceGoogleGeminiCli } from "./auth-choice.apply.google-gemini-cli.js"; import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js"; import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js"; +import { applyAuthChoiceOllama } from "./auth-choice.apply.ollama.js"; import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; import { applyAuthChoiceQwenPortal } from "./auth-choice.apply.qwen-portal.js"; import type { AuthChoice } from "./onboard-types.js"; @@ -46,6 +47,7 @@ export async function applyAuthChoice( applyAuthChoiceGoogleGeminiCli, applyAuthChoiceCopilotProxy, applyAuthChoiceQwenPortal, + applyAuthChoiceOllama, ]; for (const handler of handlers) { diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index a4d831c92..d9033a12e 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -29,6 +29,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { minimax: "lmstudio", "opencode-zen": "opencode", "qwen-portal": "qwen-portal", + ollama: "ollama", }; export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index c94eeb51b..8eb9a2b56 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -27,6 +27,8 @@ import { MOONSHOT_BASE_URL, MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_REF, + OLLAMA_BASE_URL, + OLLAMA_DEFAULT_API_KEY, } from "./onboard-auth.models.js"; export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig { @@ -484,6 +486,53 @@ export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { }; } +/** + * Apply Ollama provider configuration without changing the default model. + * Sets up the provider with dynamic model discovery; models are populated at runtime. + */ +export function applyOllamaProviderConfig( + cfg: MoltbotConfig, + params?: { baseUrl?: string }, +): MoltbotConfig { + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.ollama; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim() || OLLAMA_DEFAULT_API_KEY; + const baseUrl = params?.baseUrl ?? OLLAMA_BASE_URL; + + providers.ollama = { + ...existingProviderRest, + baseUrl, + api: "openai-completions", + apiKey: normalizedApiKey, + // Models are discovered dynamically via Ollama's /api/tags endpoint + models: [], + }; + + return { + ...cfg, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Ollama provider configuration. Since Ollama discovers models dynamically, + * this just configures the provider; model selection happens separately. + */ +export function applyOllamaConfig( + cfg: MoltbotConfig, + params?: { baseUrl?: string }, +): MoltbotConfig { + return applyOllamaProviderConfig(cfg, params); +} + export function applyAuthProfileConfig( cfg: OpenClawConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fbf6dbfb9..95c50c6c7 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -177,3 +177,15 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setOllamaApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "ollama:default", + credential: { + type: "api_key", + provider: "ollama", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index de5a4edaa..61bce4b50 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -20,6 +20,9 @@ export const KIMI_CODE_MAX_TOKENS = 32768; export const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const; export const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const; +export const OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1"; +export const OLLAMA_DEFAULT_API_KEY = "ollama"; + // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. export const MINIMAX_API_COST = { input: 15, diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..05ce5f625 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -9,6 +9,8 @@ export { applyKimiCodeProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, + applyOllamaConfig, + applyOllamaProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, applySyntheticConfig, @@ -41,6 +43,7 @@ export { setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, + setOllamaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -68,4 +71,6 @@ export { MOONSHOT_BASE_URL, MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_REF, + OLLAMA_BASE_URL, + OLLAMA_DEFAULT_API_KEY, } from "./onboard-auth.models.js"; diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..bafcc24b5 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -32,6 +32,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "ollama" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -73,6 +74,7 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + ollamaBaseUrl?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice;