feat(onboard): add Ollama provider support

This commit is contained in:
Bruce MacDonald 2026-01-29 15:03:01 -08:00
parent cb4b3f74b5
commit e8216d78c7
9 changed files with 160 additions and 1 deletions

View File

@ -20,7 +20,8 @@ export type AuthChoiceGroupId =
| "minimax"
| "synthetic"
| "venice"
| "qwen";
| "qwen"
| "ollama";
export type AuthChoiceGroup = {
value: AuthChoiceGroupId;
@ -113,6 +114,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: {
@ -183,6 +190,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" });
}

View File

@ -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<ApplyAuthChoiceResult | null> {
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 <model>` to download models, then `moltbot models list` to see them.",
].join("\n"),
"Setup complete",
);
return { config: nextConfig };
}

View File

@ -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) {

View File

@ -28,6 +28,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
minimax: "lmstudio",
"opencode-zen": "opencode",
"qwen-portal": "qwen-portal",
ollama: "ollama",
};
export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined {

View File

@ -25,6 +25,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: MoltbotConfig): MoltbotConfig {
@ -411,6 +413,53 @@ export function applyVeniceConfig(cfg: MoltbotConfig): MoltbotConfig {
};
}
/**
* 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: MoltbotConfig,
params: {

View File

@ -164,3 +164,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),
});
}

View File

@ -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,

View File

@ -9,6 +9,8 @@ export {
applyKimiCodeProviderConfig,
applyMoonshotConfig,
applyMoonshotProviderConfig,
applyOllamaConfig,
applyOllamaProviderConfig,
applyOpenrouterConfig,
applyOpenrouterProviderConfig,
applySyntheticConfig,
@ -39,6 +41,7 @@ export {
setKimiCodeApiKey,
setMinimaxApiKey,
setMoonshotApiKey,
setOllamaApiKey,
setOpencodeZenApiKey,
setOpenrouterApiKey,
setSyntheticApiKey,
@ -64,4 +67,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";

View File

@ -31,6 +31,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";
@ -71,6 +72,7 @@ export type OnboardOptions = {
syntheticApiKey?: string;
veniceApiKey?: string;
opencodeZenApiKey?: string;
ollamaBaseUrl?: string;
gatewayPort?: number;
gatewayBind?: GatewayBind;
gatewayAuth?: GatewayAuthChoice;