mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 12:18:59 +00:00
feat(providers): add Ollama local and Ollama Cloud provider support (#113)
* feat(providers): add Ollama local and Ollama Cloud provider support Adds two new provider variants: ollama — local/self-hosted Ollama instance - Gated on providers.ollama.host in config (or GOCLAW_OLLAMA_HOST env) - No API key required; Ollama's OpenAI-compat endpoint accepts any Bearer value - Defaults to http://localhost:11434/v1 (configurable for LAN/remote hosts) - Default model: llama3.3 ollama-cloud — Ollama Cloud (managed remote inference) - Gated on providers.ollama_cloud.api_key (or GOCLAW_OLLAMA_CLOUD_API_KEY env) - Bearer token from ollama.com/settings/keys - Default base URL: https://ollama.com/v1 (overridable via api_base) - Default model: llama3.3 Both variants use the existing NewOpenAIProvider (OpenAI-compat) — no new provider struct needed. Both are registered from config file and DB (via llm_providers table with ProviderOllama / ProviderOllamaCloud types). OllamaCloud.APIKey follows all existing secret handling patterns: MaskedCopy, StripSecrets, StripMaskedSecrets. * feat(providers): wire Ollama into web UI and fix DB registration - Add ollama + ollama_cloud to PROVIDER_TYPES constants (dropdowns) - Fix setup wizard: skip API key requirement for Ollama local (isOllama) - Fix bootstrap status: recognize Ollama local as no-API-key provider - Add ollama_cloud to config-page KNOWN_PROVIDERS list - Fix gateway_providers.go: move ProviderOllama before APIKey=='' guard so DB-registered local Ollama providers actually register at startup (same pattern as ClaudeCLI, which also needs no API key)
This commit is contained in:
1 parent
456e594b8f
commit
d874266e87
9 files changed
+68
-8
No files matched your search
@@ -116,6 +116,24 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) {
|
||||
slog.Info("registered provider", "name", "zai-coding")
|
||||
}
|
||||
|
||||
// Local / self-hosted Ollama — gated on Host, no API key required.
|
||||
// Ollama's OpenAI-compat endpoint accepts any non-empty Bearer value.
|
||||
if cfg.Providers.Ollama.Host != "" {
|
||||
host := cfg.Providers.Ollama.Host
|
||||
registry.Register(providers.NewOpenAIProvider("ollama", "ollama", host+"/v1", "llama3.3"))
|
||||
slog.Info("registered provider", "name", "ollama")
|
||||
}
|
||||
|
||||
// Ollama Cloud — API key required (generate at ollama.com/settings/keys).
|
||||
if cfg.Providers.OllamaCloud.APIKey != "" {
|
||||
base := cfg.Providers.OllamaCloud.APIBase
|
||||
if base == "" {
|
||||
base = "https://ollama.com/v1"
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider("ollama-cloud", cfg.Providers.OllamaCloud.APIKey, base, "llama3.3"))
|
||||
slog.Info("registered provider", "name", "ollama-cloud")
|
||||
}
|
||||
|
||||
// Claude CLI provider (subscription-based, no API key needed)
|
||||
if cfg.Providers.ClaudeCLI.CLIPath != "" {
|
||||
cliPath := cfg.Providers.ClaudeCLI.CLIPath
|
||||
@@ -243,6 +261,17 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
slog.Info("registered provider from DB", "name", p.Name)
|
||||
continue
|
||||
}
|
||||
// Local Ollama requires no API key — handle before the key guard (same pattern as ClaudeCLI).
|
||||
if p.ProviderType == store.ProviderOllama {
|
||||
host := p.APIBase
|
||||
if host == "" {
|
||||
host = "http://localhost:11434"
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider(p.Name, "ollama", host+"/v1", "llama3.3"))
|
||||
slog.Info("registered provider from DB", "name", p.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if p.APIKey == "" {
|
||||
continue
|
||||
}
|
||||
@@ -273,6 +302,12 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi
|
||||
base = "https://api.z.ai/api/coding/paas/v4"
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5"))
|
||||
case store.ProviderOllamaCloud:
|
||||
base := p.APIBase
|
||||
if base == "" {
|
||||
base = "https://ollama.com/v1"
|
||||
}
|
||||
registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "llama3.3"))
|
||||
case store.ProviderSuno:
|
||||
// Suno is a media-only provider (music gen). Register as OpenAI-compat
|
||||
// so credentialProvider interface works for API key/base extraction.
|
||||
|
||||
@@ -194,9 +194,17 @@ type ProvidersConfig struct {
|
||||
Perplexity ProviderConfig `json:"perplexity"`
|
||||
DashScope ProviderConfig `json:"dashscope"`
|
||||
Bailian ProviderConfig `json:"bailian"`
|
||||
Zai ProviderConfig `json:"zai"`
|
||||
ZaiCoding ProviderConfig `json:"zai_coding"`
|
||||
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
|
||||
Zai ProviderConfig `json:"zai"`
|
||||
ZaiCoding ProviderConfig `json:"zai_coding"`
|
||||
Ollama OllamaConfig `json:"ollama"` // local Ollama instance (no API key needed)
|
||||
OllamaCloud ProviderConfig `json:"ollama_cloud"` // Ollama Cloud (API key required)
|
||||
ClaudeCLI ClaudeCLIConfig `json:"claude_cli"`
|
||||
}
|
||||
|
||||
// OllamaConfig configures a local (or self-hosted) Ollama instance.
|
||||
// No API key is required — Ollama accepts any Bearer token value.
|
||||
type OllamaConfig struct {
|
||||
Host string `json:"host"` // Ollama server base URL, e.g. http://localhost:11434
|
||||
}
|
||||
|
||||
// ClaudeCLIConfig configures the Claude CLI provider (uses subscription, not API key).
|
||||
@@ -230,6 +238,8 @@ func (c *Config) HasAnyProvider() bool {
|
||||
p.Bailian.APIKey != "" ||
|
||||
p.Zai.APIKey != "" ||
|
||||
p.ZaiCoding.APIKey != "" ||
|
||||
p.Ollama.Host != "" ||
|
||||
p.OllamaCloud.APIKey != "" ||
|
||||
p.ClaudeCLI.CLIPath != ""
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ func (c *Config) applyEnvOverrides() {
|
||||
envStr("GOCLAW_BAILIAN_API_KEY", &c.Providers.Bailian.APIKey)
|
||||
envStr("GOCLAW_ZAI_API_KEY", &c.Providers.Zai.APIKey)
|
||||
envStr("GOCLAW_ZAI_CODING_API_KEY", &c.Providers.ZaiCoding.APIKey)
|
||||
envStr("GOCLAW_OLLAMA_HOST", &c.Providers.Ollama.Host)
|
||||
envStr("GOCLAW_OLLAMA_CLOUD_API_KEY", &c.Providers.OllamaCloud.APIKey)
|
||||
envStr("GOCLAW_OLLAMA_CLOUD_API_BASE", &c.Providers.OllamaCloud.APIBase)
|
||||
envStr("GOCLAW_GATEWAY_TOKEN", &c.Gateway.Token)
|
||||
envStr("GOCLAW_TELEGRAM_TOKEN", &c.Channels.Telegram.Token)
|
||||
envStr("GOCLAW_DISCORD_TOKEN", &c.Channels.Discord.Token)
|
||||
|
||||
@@ -36,6 +36,7 @@ func (c *Config) MaskedCopy() *Config {
|
||||
maskNonEmpty(&cp.Providers.Bailian.APIKey)
|
||||
maskNonEmpty(&cp.Providers.Zai.APIKey)
|
||||
maskNonEmpty(&cp.Providers.ZaiCoding.APIKey)
|
||||
maskNonEmpty(&cp.Providers.OllamaCloud.APIKey)
|
||||
|
||||
// Mask gateway token
|
||||
maskNonEmpty(&cp.Gateway.Token)
|
||||
@@ -85,6 +86,7 @@ func (c *Config) StripSecrets() {
|
||||
c.Providers.Bailian.APIKey = ""
|
||||
c.Providers.Zai.APIKey = ""
|
||||
c.Providers.ZaiCoding.APIKey = ""
|
||||
c.Providers.OllamaCloud.APIKey = ""
|
||||
|
||||
// Gateway token
|
||||
c.Gateway.Token = ""
|
||||
@@ -139,6 +141,7 @@ func (c *Config) StripMaskedSecrets() {
|
||||
stripIfMasked(&c.Providers.Bailian.APIKey)
|
||||
stripIfMasked(&c.Providers.Zai.APIKey)
|
||||
stripIfMasked(&c.Providers.ZaiCoding.APIKey)
|
||||
stripIfMasked(&c.Providers.OllamaCloud.APIKey)
|
||||
|
||||
// Gateway token
|
||||
stripIfMasked(&c.Gateway.Token)
|
||||
|
||||
@@ -28,6 +28,8 @@ const (
|
||||
ProviderYesScale = "yescale"
|
||||
ProviderZai = "zai"
|
||||
ProviderZaiCoding = "zai_coding"
|
||||
ProviderOllama = "ollama" // local or self-hosted Ollama (no API key)
|
||||
ProviderOllamaCloud = "ollama_cloud" // Ollama Cloud (Bearer token required)
|
||||
)
|
||||
|
||||
// ValidProviderTypes lists all accepted provider_type values.
|
||||
@@ -51,6 +53,8 @@ var ValidProviderTypes = map[string]bool{
|
||||
ProviderYesScale: true,
|
||||
ProviderZai: true,
|
||||
ProviderZaiCoding: true,
|
||||
ProviderOllama: true,
|
||||
ProviderOllamaCloud: true,
|
||||
}
|
||||
|
||||
// LLMProviderData represents an LLM provider configuration.
|
||||
|
||||
@@ -23,5 +23,7 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [
|
||||
{ value: "yescale", label: "YesScale", apiBase: "https://api.yescale.one/v1", placeholder: "" },
|
||||
{ value: "zai", label: "Z.ai API", apiBase: "https://api.z.ai/api/paas/v4", placeholder: "" },
|
||||
{ value: "zai_coding", label: "Z.ai Coding Plan", apiBase: "https://api.z.ai/api/coding/paas/v4", placeholder: "" },
|
||||
{ value: "ollama", label: "Ollama (Local)", apiBase: "http://localhost:11434/v1", placeholder: "" },
|
||||
{ value: "ollama_cloud", label: "Ollama Cloud", apiBase: "https://ollama.com/v1", placeholder: "" },
|
||||
{ value: "claude_cli", label: "Claude CLI (Local)", apiBase: "", placeholder: "" },
|
||||
];
|
||||
@@ -25,6 +25,7 @@ const KNOWN_PROVIDERS = [
|
||||
{ key: "minimax", label: "MiniMax", envKey: "GOCLAW_MINIMAX_API_KEY" },
|
||||
{ key: "cohere", label: "Cohere", envKey: "GOCLAW_COHERE_API_KEY" },
|
||||
{ key: "perplexity", label: "Perplexity", envKey: "GOCLAW_PERPLEXITY_API_KEY" },
|
||||
{ key: "ollama_cloud", label: "Ollama Cloud", envKey: "GOCLAW_OLLAMA_CLOUD_API_KEY" },
|
||||
];
|
||||
|
||||
function isSecret(val: unknown): boolean {
|
||||
|
||||
@@ -17,9 +17,9 @@ export function useBootstrapStatus() {
|
||||
if (loading) return { needsSetup: false, currentStep: "complete" as SetupStep };
|
||||
|
||||
// A provider is "configured" if enabled + has an API key set (masked as "***")
|
||||
// Claude CLI and ChatGPT OAuth don't require API keys — check type instead
|
||||
// Claude CLI, ChatGPT OAuth, and local Ollama don't require API keys — check type instead
|
||||
const hasProvider = providers.some((p) => p.enabled &&
|
||||
(p.api_key === "***" || p.provider_type === "claude_cli" || p.provider_type === "chatgpt_oauth"));
|
||||
(p.api_key === "***" || p.provider_type === "claude_cli" || p.provider_type === "chatgpt_oauth" || p.provider_type === "ollama"));
|
||||
const hasAgent = agents.length > 0;
|
||||
|
||||
if (!hasProvider) return { needsSetup: true, currentStep: 1 as SetupStep };
|
||||
|
||||
@@ -35,6 +35,8 @@ export function StepProvider({ onComplete }: StepProviderProps) {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const isCLI = providerType === "claude_cli";
|
||||
// Local Ollama uses no API key — the server accepts any non-empty Bearer value internally
|
||||
const isOllama = providerType === "ollama";
|
||||
|
||||
const handleTypeChange = (value: string) => {
|
||||
setProviderType(value);
|
||||
@@ -53,7 +55,7 @@ export function StepProvider({ onComplete }: StepProviderProps) {
|
||||
);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!isCLI && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
|
||||
if (!isCLI && !isOllama && !apiKey.trim()) { setError(t("provider.errors.apiKeyRequired")); return; }
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -61,7 +63,7 @@ export function StepProvider({ onComplete }: StepProviderProps) {
|
||||
name: name.trim(),
|
||||
provider_type: providerType,
|
||||
api_base: apiBase.trim() || undefined,
|
||||
api_key: isCLI ? undefined : apiKey.trim(),
|
||||
api_key: isCLI || isOllama ? undefined : apiKey.trim(),
|
||||
enabled: true,
|
||||
}) as ProviderData;
|
||||
onComplete(provider);
|
||||
@@ -143,7 +145,7 @@ export function StepProvider({ onComplete }: StepProviderProps) {
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleCreate} disabled={loading || (!isCLI && !apiKey.trim())}>
|
||||
<Button onClick={handleCreate} disabled={loading || (!isCLI && !isOllama && !apiKey.trim())}>
|
||||
{loading ? t("provider.creating") : t("provider.create")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user