Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataclasses import dataclass, field
import os
from typing import Any
from urllib.parse import urlsplit, urlunsplit

from pydantic import model_validator

Expand Down Expand Up @@ -91,23 +92,39 @@ class 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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,34 @@ def test_strips_trailing_slash(self):
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."""
Expand Down
Loading