From b297bae0275c99867053d4e89f833131b3f64317 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 27 Feb 2026 23:35:57 -0800 Subject: [PATCH] fix(cli): allow Ollama apiKey config set without predeclared provider (#29299) * CLI: seed Ollama provider on apiKey set * Tests: cover Ollama apiKey config set path --- src/cli/config-cli.test.ts | 18 ++++++++++++++++++ src/cli/config-cli.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index a17354497..e672a0f1f 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -141,6 +141,24 @@ describe("config cli", () => { expect(written.gateway?.port).toBe(18789); expect(written.gateway?.auth).toEqual({ mode: "token" }); }); + + it("auto-seeds a valid Ollama provider when setting only models.providers.ollama.apiKey", async () => { + const resolved: OpenClawConfig = { + gateway: { port: 18789 }, + }; + setSnapshot(resolved, resolved); + + await runConfigCommand(["config", "set", "models.providers.ollama.apiKey", '"ollama-local"']); + + expect(mockWriteConfigFile).toHaveBeenCalledTimes(1); + const written = mockWriteConfigFile.mock.calls[0]?.[0]; + expect(written.models?.providers?.ollama).toEqual({ + baseUrl: "http://127.0.0.1:11434", + api: "ollama", + models: [], + apiKey: "ollama-local", + }); + }); }); describe("config get", () => { diff --git a/src/cli/config-cli.ts b/src/cli/config-cli.ts index 3893aa1d0..39e60e4ce 100644 --- a/src/cli/config-cli.ts +++ b/src/cli/config-cli.ts @@ -16,6 +16,10 @@ type ConfigSetParseOpts = { strictJson?: boolean; }; +const OLLAMA_API_KEY_PATH: PathSegment[] = ["models", "providers", "ollama", "apiKey"]; +const OLLAMA_PROVIDER_PATH: PathSegment[] = ["models", "providers", "ollama"]; +const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434"; + function isIndexSegment(raw: string): boolean { return /^[0-9]+$/.test(raw); } @@ -242,6 +246,30 @@ function parseRequiredPath(path: string): PathSegment[] { return parsedPath; } +function pathEquals(path: PathSegment[], expected: PathSegment[]): boolean { + return ( + path.length === expected.length && path.every((segment, index) => segment === expected[index]) + ); +} + +function ensureValidOllamaProviderForApiKeySet( + root: Record, + path: PathSegment[], +): void { + if (!pathEquals(path, OLLAMA_API_KEY_PATH)) { + return; + } + const existing = getAtPath(root, OLLAMA_PROVIDER_PATH); + if (existing.found) { + return; + } + setAtPath(root, OLLAMA_PROVIDER_PATH, { + baseUrl: OLLAMA_DEFAULT_BASE_URL, + api: "ollama", + models: [], + }); +} + export async function runConfigGet(opts: { path: string; json?: boolean; runtime?: RuntimeEnv }) { const runtime = opts.runtime ?? defaultRuntime; try { @@ -345,6 +373,7 @@ export function registerConfigCli(program: Command) { // instead of snapshot.config (runtime-merged with defaults). // This prevents runtime defaults from leaking into the written config file (issue #6070) const next = structuredClone(snapshot.resolved) as Record; + ensureValidOllamaProviderForApiKeySet(next, parsedPath); setAtPath(next, parsedPath, parsedValue); await writeConfigFile(next); defaultRuntime.log(info(`Updated ${path}. Restart the gateway to apply.`));