fix(ollama): normalize the base URL on the environment path too

The saved-profile path normalized /v1 but FromEnv did not, so
ARTEX_LLM_BASE_URL=http://host:11434 sent chat to /chat/completions and got a
404 on a LAN Ollama server. Share one helper between both entry points, and add
an env-path test with a bare host and with a full endpoint URL.

Also soften the keyless comment: a local Ollama ignores the bearer token, but a
gateway in front of it can require one, which belongs in a saved profile.
This commit is contained in:
BaaRai
2026-11-02 17:03:57 +01:00
parent 1ed154e86c
commit 1c35c9cf68
2 changed files with 52 additions and 18 deletions
+28 -18
View File
@@ -147,6 +147,23 @@ const DefaultOllamaBaseURL = "http://127.0.0.1:11434/v1"
// to the local server) and keeps the header stable.
const ollamaPlaceholderKey = "ollama"
// normalizeOllamaBaseURL maps whatever the operator typed onto the /v1 root the
// OpenAI-compatible provider expects: it strips a pasted "/chat/completions"
// suffix, an empty value becomes the local default, and a missing "/v1" is
// appended so the natural "http://host:11434" also works. Shared by the saved
// profile path (ConfigFrom) and the environment path (FromEnv): both used to
// differ, and only the profile path normalized.
func normalizeOllamaBaseURL(raw string) string {
b := strings.TrimRight(strings.TrimSuffix(strings.TrimSpace(raw), "/chat/completions"), "/")
if b == "" {
return DefaultOllamaBaseURL
}
if !strings.HasSuffix(b, "/v1") {
b += "/v1"
}
return b
}
// NeedsAPIKey reports whether this provider format requires an API key to be
// usable. Ollama (local server) is keyless; every cloud format needs one.
func (c Config) NeedsAPIKey() bool { return c.Format != FormatOllama }
@@ -224,13 +241,11 @@ func FromEnv() (Config, bool) {
}
case "ollama":
c.Format = FormatOllama
// Deliberately NOT inheriting OPENAI_API_KEY: Ollama ignores the bearer
// token, so the only thing the inheritance could do is ship a real cloud
// key to the local (or LAN) endpoint. An authenticated Ollama gateway can
// still be configured as a saved profile with an explicit key.
if c.BaseURL == "" {
c.BaseURL = DefaultOllamaBaseURL
}
// Deliberately NOT inheriting OPENAI_API_KEY: a local Ollama ignores the
// bearer token, so the only thing the inheritance could do is ship a real
// cloud key to the endpoint. A gateway that does require a key can still
// be configured as a saved profile with an explicit one.
c.BaseURL = normalizeOllamaBaseURL(c.BaseURL)
if c.Model == "" {
c.Model = "llama3.2"
}
@@ -278,16 +293,10 @@ func ConfigFrom(provider, model, baseURL, apiKey, proxy string) Config {
case "ollama":
c.Format = FormatOllama
// Ollama's OpenAI-compatible endpoint lives at /v1 and the provider
// appends "/chat/completions"; tolerate a full endpoint URL and the
// natural Ollama URL without the /v1 suffix. Never leave it empty: an
// empty BaseURL would let the SDK fall back to OpenAI's public endpoint
// with this profile's request.
c.BaseURL = strings.TrimRight(strings.TrimSuffix(strings.TrimSpace(c.BaseURL), "/chat/completions"), "/")
if c.BaseURL == "" {
c.BaseURL = DefaultOllamaBaseURL
} else if !strings.HasSuffix(c.BaseURL, "/v1") {
c.BaseURL += "/v1"
}
// appends "/chat/completions". Never leave it empty: an empty BaseURL
// would let the SDK fall back to OpenAI's public endpoint with this
// profile's request.
c.BaseURL = normalizeOllamaBaseURL(c.BaseURL)
if c.Model == "" {
c.Model = "llama3.2"
}
@@ -340,7 +349,8 @@ func (c Config) NewProvider() (llm.Provider, error) {
// Ollama speaks OpenAI's Chat Completions on the wire. Bundle the keyless
// defaults here so an Ollama config can never leak toward the SDK's env-
// driven fallbacks: BaseURL to OpenAI's public endpoint, APIKey to
// OPENAI_API_KEY.
// OPENAI_API_KEY. A local Ollama ignores the placeholder token; a gateway
// that requires one carries a real key in the profile.
if wireFormat == FormatOllama {
wireFormat = llm.FormatOpenAI
if strings.TrimSpace(baseURL) == "" {
+24
View File
@@ -231,6 +231,30 @@ func TestOllamaListModels(t *testing.T) {
}
}
// The environment path must normalize the base URL too: ARTEX_LLM_BASE_URL is
// the natural way to configure a LAN Ollama server, and it used to send chat to
// /chat/completions instead of /v1/chat/completions.
func TestOllamaFromEnvNormalizesBaseURL(t *testing.T) {
t.Setenv("ARTEX_LLM_PROVIDER", "ollama")
t.Setenv("OPENAI_API_KEY", "")
t.Setenv("ARTEX_LLM_BASE_URL", "http://box:11434")
c, ok := agent.FromEnv()
if !ok {
t.Fatal("ollama env config must be usable")
}
if c.BaseURL != "http://box:11434/v1" {
t.Fatalf("env base URL not normalized: %q", c.BaseURL)
}
t.Setenv("ARTEX_LLM_BASE_URL", "http://box:11434/v1/chat/completions")
c, ok = agent.FromEnv()
if !ok {
t.Fatal("ollama env config must be usable")
}
if c.BaseURL != "http://box:11434/v1" {
t.Fatalf("full endpoint URL not normalized: %q", c.BaseURL)
}
}
// An Ollama config resolved from the environment must not inherit OPENAI_API_KEY:
// Ollama ignores the bearer token, so inheritance would only ship the cloud key
// to the local or LAN endpoint.