mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
fix: normalize ollama openai compat base url
This commit is contained in:
@@ -13,6 +13,7 @@ from .providers.catalog import (
|
||||
DEFAULT_MODEL_BY_PROVIDER,
|
||||
DEFAULT_PROVIDER,
|
||||
get_provider_info,
|
||||
normalize_provider_base_url,
|
||||
)
|
||||
from .providers.keys import get_api_key_for_provider
|
||||
from .runtime_config import get_effective_config
|
||||
@@ -41,11 +42,13 @@ def get_effective_llm_base_url(provider: str) -> str:
|
||||
current_provider = str(effective.get("provider") or "").lower()
|
||||
configured_base_url = str(effective.get("base_url") or "").strip()
|
||||
if configured_base_url and str(provider).lower() == current_provider:
|
||||
return configured_base_url
|
||||
# IMPORTANT: persisted Ollama configs may still store the historical root URL
|
||||
# without `/v1`; normalize at the shared facade so all consumers stay aligned.
|
||||
return normalize_provider_base_url(provider, configured_base_url)
|
||||
|
||||
info = get_provider_info(provider)
|
||||
if info:
|
||||
return info.base_url
|
||||
return normalize_provider_base_url(provider, info.base_url)
|
||||
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from .providers.catalog import (
|
||||
DEFAULT_PROVIDER,
|
||||
ProviderType,
|
||||
get_provider_info,
|
||||
normalize_provider_base_url,
|
||||
)
|
||||
from .providers.keys import get_api_key_for_provider, mask_api_key, requires_api_key
|
||||
from .structured_logging import (
|
||||
@@ -121,6 +122,12 @@ class LLMClient:
|
||||
info = get_provider_info(self.provider)
|
||||
if info:
|
||||
self.base_url = info.base_url
|
||||
# IMPORTANT: test-connection and normal OpenAI-compatible completions must
|
||||
# share the same Ollama `/v1` normalization path as model discovery. Do not
|
||||
# bypass this with provider-specific ad hoc request URL assembly.
|
||||
self.base_url = normalize_provider_base_url(
|
||||
self.provider, str(self.base_url or "")
|
||||
)
|
||||
|
||||
# R57: Strict Precedence (Arg > Config > Default)
|
||||
# CRITICAL: Only inherit config['model'] if the effective provider matches config['provider'].
|
||||
|
||||
@@ -159,7 +159,11 @@ def resolve_model_list_target(
|
||||
if info.api_type != catalog.ProviderType.OPENAI_COMPAT:
|
||||
raise TypeError("Model list is only supported for OpenAI-compatible providers.")
|
||||
|
||||
base_url = runtime_base_url if runtime_base_url else info.base_url
|
||||
raw_base_url = runtime_base_url if runtime_base_url else info.base_url
|
||||
# IMPORTANT: model discovery is one of the primary Ollama debug hotspots.
|
||||
# Keep provider-aware base URL normalization here so legacy root URLs still
|
||||
# resolve to the OpenAI-compatible `/v1/models` endpoint instead of 404.
|
||||
base_url = catalog.normalize_provider_base_url(provider, raw_base_url)
|
||||
if not base_url:
|
||||
raise ValueError(f"No base URL configured for provider '{provider}'.")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ R73: Provider drift governance — alias/deprecation metadata and resolution tra
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
class ProviderType(Enum):
|
||||
@@ -81,7 +81,7 @@ PROVIDER_CATALOG: Dict[str, ProviderInfo] = {
|
||||
),
|
||||
"ollama": ProviderInfo(
|
||||
name="Ollama (Local)",
|
||||
base_url="http://127.0.0.1:11434",
|
||||
base_url="http://127.0.0.1:11434/v1",
|
||||
api_type=ProviderType.OPENAI_COMPAT,
|
||||
supports_vision=True,
|
||||
env_key_name=None, # Local, no key needed
|
||||
@@ -302,6 +302,33 @@ def normalize_provider_id(provider: str) -> str:
|
||||
return PROVIDER_ALIASES.get(p, p)
|
||||
|
||||
|
||||
def normalize_provider_base_url(provider: str, base_url: str) -> str:
|
||||
"""
|
||||
Normalize provider-specific base URL compatibility seams.
|
||||
|
||||
Currently used to keep Ollama's OpenAI-compatible endpoint path aligned to
|
||||
`/v1` even when older persisted configs still store the historical root URL.
|
||||
"""
|
||||
value = str(base_url or "").strip()
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
if normalize_provider_id(str(provider or "")) != "ollama":
|
||||
return value
|
||||
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
except Exception:
|
||||
return value
|
||||
|
||||
# IMPORTANT: old Ollama configs may still store the root OpenAI-compat host.
|
||||
# Normalize only the empty-path form to `/v1`; do not rewrite custom subpaths.
|
||||
if parsed.path not in ("", "/"):
|
||||
return value
|
||||
|
||||
return urlunparse(parsed._replace(path="/v1"))
|
||||
|
||||
|
||||
def normalize_model_id(model: str) -> str:
|
||||
"""Normalize model ID (resolve aliases)."""
|
||||
m = model.lower().strip()
|
||||
|
||||
@@ -48,6 +48,24 @@ class EffectiveConfigFacadeTests(unittest.TestCase):
|
||||
"https://custom-openai.example/v1",
|
||||
)
|
||||
|
||||
@patch("services.effective_config.get_effective_config")
|
||||
def test_ollama_root_base_url_is_normalized_from_effective_config(
|
||||
self, mock_get_effective
|
||||
):
|
||||
mock_get_effective.return_value = (
|
||||
{
|
||||
"provider": "ollama",
|
||||
"model": "llama3.2",
|
||||
"base_url": "http://127.0.0.1:11434",
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
effective_config.get_effective_llm_base_url("ollama"),
|
||||
"http://127.0.0.1:11434/v1",
|
||||
)
|
||||
|
||||
@patch("config.get_effective_llm_api_key", return_value="sk-effective")
|
||||
def test_config_get_api_key_uses_effective_facade(self, mock_get_key):
|
||||
self.assertEqual(config_module.get_api_key(), "sk-effective")
|
||||
|
||||
@@ -157,6 +157,42 @@ class TestLLMClientPluginIntegration(unittest.TestCase):
|
||||
# Should succeed without plugins
|
||||
self.assertEqual(result["text"], "test")
|
||||
|
||||
@patch("services.llm_client.PLUGINS_AVAILABLE", False)
|
||||
@patch("services.runtime_config.get_effective_config")
|
||||
@patch("services.effective_config.get_effective_config")
|
||||
@patch("services.llm_client.requires_api_key", return_value=False)
|
||||
@patch("services.llm_client.get_api_key_for_provider", return_value=None)
|
||||
@patch("services.llm_client.openai_compat.make_request")
|
||||
def test_ollama_root_base_url_is_normalized_before_openai_compat_request(
|
||||
self,
|
||||
mock_request,
|
||||
_mock_key,
|
||||
_mock_requires_key,
|
||||
mock_effective_config_facade,
|
||||
mock_runtime_config,
|
||||
):
|
||||
config_payload = (
|
||||
{
|
||||
"provider": "ollama",
|
||||
"model": "llama3.2",
|
||||
"base_url": "http://127.0.0.1:11434",
|
||||
"timeout_sec": 120,
|
||||
"max_retries": 3,
|
||||
},
|
||||
None,
|
||||
)
|
||||
mock_runtime_config.return_value = config_payload
|
||||
mock_effective_config_facade.return_value = config_payload
|
||||
mock_request.return_value = {"text": "test", "raw": {}}
|
||||
|
||||
client = LLMClient()
|
||||
client.complete(system="test", user_message="test")
|
||||
|
||||
self.assertEqual(
|
||||
mock_request.call_args.kwargs["base_url"],
|
||||
"http://127.0.0.1:11434/v1",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -25,6 +25,24 @@ class LlmModelListServiceTests(unittest.TestCase):
|
||||
("tenant-a", "custom", "https://custom.example/v1"),
|
||||
)
|
||||
|
||||
@patch("services.providers.keys.requires_api_key", return_value=False)
|
||||
@patch("services.providers.keys.get_api_key_for_provider", return_value=None)
|
||||
def test_resolve_target_normalizes_legacy_ollama_root_url(
|
||||
self, _mock_key, _mock_requires_key
|
||||
):
|
||||
target = resolve_model_list_target(
|
||||
provider_override="ollama",
|
||||
effective={"provider": "ollama", "base_url": "http://127.0.0.1:11434"},
|
||||
tenant_id="default",
|
||||
)
|
||||
|
||||
self.assertEqual(target.provider, "ollama")
|
||||
self.assertEqual(target.base_url, "http://127.0.0.1:11434/v1")
|
||||
self.assertEqual(
|
||||
target.cache_key,
|
||||
("ollama", "http://127.0.0.1:11434/v1"),
|
||||
)
|
||||
|
||||
@patch("services.safe_io.safe_request_json")
|
||||
def test_fetch_remote_model_list_builds_auth_header(self, mock_safe_request):
|
||||
mock_safe_request.return_value = {"data": [{"id": "gpt-4o-mini"}]}
|
||||
|
||||
@@ -21,6 +21,7 @@ from services.providers.catalog import (
|
||||
is_local_provider,
|
||||
is_loopback_host,
|
||||
list_providers,
|
||||
normalize_provider_base_url,
|
||||
)
|
||||
from services.providers.keys import (
|
||||
get_all_configured_keys,
|
||||
@@ -95,6 +96,22 @@ class TestProviderCatalog(unittest.TestCase):
|
||||
self.assertTrue(is_local_provider("lmstudio"))
|
||||
self.assertFalse(is_local_provider("openai"))
|
||||
|
||||
def test_ollama_default_base_url_uses_v1_openai_compat_prefix(self):
|
||||
self.assertEqual(
|
||||
PROVIDER_CATALOG["ollama"].base_url,
|
||||
"http://127.0.0.1:11434/v1",
|
||||
)
|
||||
|
||||
def test_ollama_root_base_url_normalizes_to_v1(self):
|
||||
self.assertEqual(
|
||||
normalize_provider_base_url("ollama", "http://127.0.0.1:11434"),
|
||||
"http://127.0.0.1:11434/v1",
|
||||
)
|
||||
self.assertEqual(
|
||||
normalize_provider_base_url("ollama", "http://127.0.0.1:11434/v1"),
|
||||
"http://127.0.0.1:11434/v1",
|
||||
)
|
||||
|
||||
def test_loopback_helpers(self):
|
||||
self.assertTrue(is_loopback_host("localhost"))
|
||||
self.assertTrue(is_loopback_host("127.0.0.1"))
|
||||
|
||||
@@ -56,7 +56,7 @@ class TestR123RealBackendModelListLane(AioHTTPTestCase):
|
||||
|
||||
class ModelsHandler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path in ("/models", "/v1/models"):
|
||||
if self.path == "/v1/models":
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload_bytes)))
|
||||
|
||||
Reference in New Issue
Block a user