From 8a0c42433af55a9ca3fcf2d1ae268c10454e3f24 Mon Sep 17 00:00:00 2001 From: "Baek, JH" Date: Mon, 26 Jan 2026 22:25:08 -0800 Subject: [PATCH] feat(auth): add ollama to onboard provider selection UI --- src/commands/auth-choice-options.ts | 14 +++++- .../auth-choice.apply.api-providers.ts | 48 +++++++++++++++++++ .../auth-choice.preferred-provider.ts | 1 + src/commands/onboard-auth.credentials.ts | 12 +++++ src/commands/onboard-auth.ts | 1 + src/commands/onboard-types.ts | 1 + 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..19ca73add 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -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 LLM (API key)", + choices: ["ollama-api-key"], + }, ]; export function buildAuthChoiceOptions(params: { @@ -177,6 +184,11 @@ export function buildAuthChoiceOptions(params: { label: "OpenCode Zen (multi-model proxy)", hint: "Claude, GPT, Gemini via opencode.ai/zen", }); + options.push({ + value: "ollama-api-key", + label: "Ollama (local LLM)", + hint: "Requires Ollama running locally", + }); options.push({ value: "minimax-api", label: "MiniMax M2.1" }); options.push({ value: "minimax-api-lightning", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..040a8707e 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -37,6 +37,7 @@ import { setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, + setOllamaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -83,6 +84,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { authChoice = "venice-api-key"; + } else if (params.opts.tokenProvider === "ollama") { + authChoice = "ollama-api-key"; } else if (params.opts.tokenProvider === "opencode") { authChoice = "opencode-zen"; } @@ -522,6 +525,51 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "ollama-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "ollama") { + await setOllamaApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Ollama runs LLMs locally on your machine.", + "Make sure Ollama is running: ollama serve", + "The API key can be any non-empty string (e.g. 'ollama').", + ].join("\n"), + "Ollama", + ); + } + const envKey = resolveEnvApiKey("ollama"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OLLAMA_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setOllamaApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Ollama API key (any non-empty string)", + initialValue: "ollama", + validate: validateApiKeyInput, + }); + await setOllamaApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "ollama:default", + provider: "ollama", + mode: "api_key", + }); + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "opencode-zen") { let hasCredential = false; if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") { diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6fe26b59a..93d628801 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -28,6 +28,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { minimax: "lmstudio", "opencode-zen": "opencode", "qwen-portal": "qwen-portal", + "ollama-api-key": "ollama", }; export function resolvePreferredProviderForAuthChoice(choice: AuthChoice): string | undefined { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 0c7dff409..8c0b1e79a 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -153,6 +153,18 @@ export async function setVercelAiGatewayApiKey(key: string, agentDir?: string) { }); } +export async function setOllamaApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "ollama:default", + credential: { + type: "api_key", + provider: "ollama", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export async function setOpencodeZenApiKey(key: string, agentDir?: string) { upsertAuthProfile({ profileId: "opencode:default", diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..64ef474b7 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -39,6 +39,7 @@ export { setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, + setOllamaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..3e5128712 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -31,6 +31,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "ollama-api-key" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full";