fix(llms): normalize scheme and port in Ollama base URL (#7206)

* fix(llms): normalize scheme and port in Ollama base URL

OLLAMA_HOST follows Ollama's own convention and may be a bare host
("0.0.0.0") or a host:port pair ("127.0.0.1:11434") rather than a full
URL. _normalize_ollama_base_url only appended "/v1", so those values
produced invalid base URLs such as "0.0.0.0/v1", and every request
failed with the misleading error "Failed to connect to OpenAI API:
Connection error." - confusing, since no OpenAI model was requested.

Fill in the missing parts the way Ollama's own client does: prepend
http:// when no scheme is present, append the default port 11434 when
none is present and the scheme is http (https implies 443), then append
the /v1 suffix the OpenAI-compatible endpoint requires.

Six of nine realistic OLLAMA_HOST forms were affected, including
127.0.0.1:11434, which is Ollama's documented default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(llms): strip only the parsed path when normalizing Ollama base URL

Stripping trailing slashes from the whole URL before parsing corrupted
inputs that carry a query or fragment. "http://ollama/?tenant=acme" kept
a "/" path and produced a doubled "//v1", and a query or fragment ending
in "/" silently lost that character.

Parse first, then rstrip only parts.path. Adds regression tests for a
root path alongside a query and for a query value ending in "/".

Reported by CodeRabbit on #7206.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com>
This commit is contained in:
Parthiban Sivakumar
2026-09-03 16:05:56 +05:30
committed by GitHub
parent 3d72c707d5
commit c90337ba5a
2 changed files with 53 additions and 9 deletions

View File

@@ -15,6 +15,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
import os
from typing import Any
from urllib.parse import urlsplit, urlunsplit
from pydantic import model_validator
@@ -91,23 +92,39 @@ OPENAI_COMPATIBLE_PROVIDERS: dict[str, ProviderConfig] = {
),
}
_OLLAMA_DEFAULT_PORT = 11434
def _normalize_ollama_base_url(base_url: str) -> str:
"""Normalize Ollama base URL to ensure it ends with /v1.
"""Normalize an Ollama base URL into a full OpenAI-compatible endpoint.
Ollama uses OLLAMA_HOST which may not include the /v1 suffix,
but the OpenAI-compatible endpoint requires it.
``OLLAMA_HOST`` follows Ollama's own convention and may be a bare host
(``0.0.0.0``), a ``host:port`` pair (``127.0.0.1:11434``), or a full URL.
Whichever parts are missing are filled in: ``http://`` when no scheme is
given, the default Ollama port when none is given and the scheme is
``http`` (``https`` implies 443), and the ``/v1`` suffix that the
OpenAI-compatible endpoint requires.
Args:
base_url: The base URL, potentially without /v1 suffix.
base_url: The base URL, potentially missing scheme, port or /v1.
Returns:
The base URL with /v1 suffix if needed.
A fully-qualified base URL ending in /v1.
"""
base_url = base_url.rstrip("/")
if not base_url.endswith("/v1"):
return f"{base_url}/v1"
return base_url
if "://" not in base_url:
base_url = f"http://{base_url}"
parts = urlsplit(base_url)
netloc = parts.netloc
if parts.scheme == "http" and parts.port is None:
netloc = f"{netloc}:{_OLLAMA_DEFAULT_PORT}"
path = parts.path.rstrip("/")
if not path.endswith("/v1"):
path = f"{path}/v1"
return urlunsplit((parts.scheme, netloc, path, parts.query, parts.fragment))
class OpenAICompatibleCompletion(OpenAICompletion):

View File

@@ -114,7 +114,34 @@ class TestNormalizeOllamaBaseUrl:
def test_handles_v1_with_trailing_slash(self):
"""Test /v1/ is normalized."""
assert _normalize_ollama_base_url("http://localhost:11434/v1/") == "http://localhost:11434/v1"
def test_bare_host_gets_scheme_and_port(self):
"""Bare host from OLLAMA_HOST gets http:// and the default port."""
assert _normalize_ollama_base_url("0.0.0.0") == "http://0.0.0.0:11434/v1"
def test_bare_localhost_gets_scheme_and_port(self):
"""Bare localhost gets http:// and the default port."""
assert _normalize_ollama_base_url("localhost") == "http://localhost:11434/v1"
def test_host_port_without_scheme_gets_scheme(self):
"""host:port without a scheme gets http:// prepended."""
assert _normalize_ollama_base_url("127.0.0.1:11434") == "http://127.0.0.1:11434/v1"
def test_lan_host_port_without_scheme(self):
"""A LAN host:port without a scheme gets http:// prepended."""
assert _normalize_ollama_base_url("192.168.1.5:11434") == "http://192.168.1.5:11434/v1"
def test_https_url_keeps_scheme_and_gets_no_default_port(self):
"""An explicit https:// URL keeps its scheme and gets no default port."""
assert _normalize_ollama_base_url("https://ollama.example.com") == "https://ollama.example.com/v1"
def test_root_path_with_query_does_not_double_slash(self):
"""A root path alongside a query yields /v1, not //v1."""
assert _normalize_ollama_base_url("http://ollama/?tenant=acme") == "http://ollama:11434/v1?tenant=acme"
def test_trailing_slash_in_query_is_preserved(self):
"""Only the path is stripped, so a query ending in / keeps that character."""
assert _normalize_ollama_base_url("http://ollama:11434/?x=a/") == "http://ollama:11434/v1?x=a/"
class TestOpenAICompatibleCompletion:
"""Tests for OpenAICompatibleCompletion class."""