diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6d540f5a5..4a350b81dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ Docs: https://docs.openclaw.ai - CLI: add `openclaw logs --local-time` to display log timestamps in local timezone. (#13818) Thanks @xialonglee. - Config: avoid redacting `maxTokens`-like fields during config snapshot redaction, preventing round-trip validation failures in `/config`. (#14006) Thanks @constansino. +### Fixes + +- Ollama: use configured `models.providers.ollama.baseUrl` for model discovery and normalize `/v1` endpoints to the native Ollama API root. (#14131) Thanks @shtse8. + ## 2026.2.9 ### Added diff --git a/src/agents/models-config.providers.ollama.test.ts b/src/agents/models-config.providers.ollama.test.ts index e1730464ca2..3b9624a8eb6 100644 --- a/src/agents/models-config.providers.ollama.test.ts +++ b/src/agents/models-config.providers.ollama.test.ts @@ -2,7 +2,27 @@ import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { resolveImplicitProviders } from "./models-config.providers.js"; +import { resolveImplicitProviders, resolveOllamaApiBase } from "./models-config.providers.js"; + +describe("resolveOllamaApiBase", () => { + it("returns default localhost base when no configured URL is provided", () => { + expect(resolveOllamaApiBase()).toBe("http://127.0.0.1:11434"); + }); + + it("strips /v1 suffix from OpenAI-compatible URLs", () => { + expect(resolveOllamaApiBase("http://ollama-host:11434/v1")).toBe("http://ollama-host:11434"); + expect(resolveOllamaApiBase("http://ollama-host:11434/V1")).toBe("http://ollama-host:11434"); + }); + + it("keeps URLs without /v1 unchanged", () => { + expect(resolveOllamaApiBase("http://ollama-host:11434")).toBe("http://ollama-host:11434"); + }); + + it("handles trailing slash before canonicalizing", () => { + expect(resolveOllamaApiBase("http://ollama-host:11434/v1/")).toBe("http://ollama-host:11434"); + expect(resolveOllamaApiBase("http://ollama-host:11434/")).toBe("http://ollama-host:11434"); + }); +}); describe("Ollama provider", () => { it("should not include ollama when no API key is configured", async () => { @@ -33,6 +53,28 @@ describe("Ollama provider", () => { } }); + it("should preserve explicit ollama baseUrl on implicit provider injection", async () => { + const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); + process.env.OLLAMA_API_KEY = "test-key"; + + try { + const providers = await resolveImplicitProviders({ + agentDir, + explicitProviders: { + ollama: { + baseUrl: "http://192.168.20.14:11434/v1", + api: "openai-completions", + models: [], + }, + }, + }); + + expect(providers?.ollama?.baseUrl).toBe("http://192.168.20.14:11434/v1"); + } finally { + delete process.env.OLLAMA_API_KEY; + } + }); + it("should have correct model structure with streaming disabled (unit test)", () => { // This test directly verifies the model configuration structure // since discoverOllamaModels() returns empty array in test mode diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 6253b2d2d4f..a4725c5a230 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -119,7 +119,7 @@ interface OllamaTagsResponse { * The native Ollama API lives at the root (e.g. `/api/tags`), so we * strip the `/v1` suffix when present. */ -function resolveOllamaApiBase(configuredBaseUrl?: string): string { +export function resolveOllamaApiBase(configuredBaseUrl?: string): string { if (!configuredBaseUrl) { return OLLAMA_API_BASE_URL; }