Skip to content
Merged
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
19 changes: 18 additions & 1 deletion app/ai_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ def rewrite_ai_url_for_runtime(url: str) -> str:
return rewritten


def openai_compat_base_url(url: str, endpoint: str = "/chat/completions") -> str:
"""Return the OpenAI-compatible *base* URL (scheme+host+optional /v1).

Users often paste the full chat URL (…/v1/chat/completions) into the
Settings base-URL field. Built-in providers then append ``endpoint``
again and Ollama returns 404. Strip a trailing copy of ``endpoint``.
"""
if not url:
return url
base = url.rstrip("/")
ep = "/" + (endpoint or "").lstrip("/")
if ep not in ("", "/") and base.lower().endswith(ep.lower()):
base = base[: -len(ep)].rstrip("/")
return base


def dummy_key_for_local(api_key: Optional[str]) -> str:
"""Ollama accepts any Bearer token; the proxy still sends Authorization."""
key = (api_key or "").strip()
Expand Down Expand Up @@ -2541,7 +2557,8 @@ def call_ai_provider(provider: str,
base = api_url.rstrip('/')
full_url = f"{base}/models/{model}:generateContent"
else:
full_url = api_url.rstrip('/') + '/' + endpoint.lstrip('/')
base = openai_compat_base_url(api_url, endpoint)
full_url = base.rstrip('/') + '/' + endpoint.lstrip('/')

return _execute_ai_request(full_url, headers, ai_request_payload)

Expand Down
3 changes: 2 additions & 1 deletion app/app_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,8 @@ def ai_api_call():
else:
full_url = f"{api_url.rstrip('/')}/models/{model}:generateContent"
else:
full_url = api_url.rstrip('/') + '/' + endpoint.lstrip('/')
base = ai_analyzer.openai_compat_base_url(api_url, endpoint)
full_url = base.rstrip('/') + '/' + endpoint.lstrip('/')
logger.debug("Full URL: %s", full_url)

# Prepare headers
Expand Down
2 changes: 1 addition & 1 deletion app/assets/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const AI_PROVIDERS = [
defaultUrl: 'http://localhost:11434/v1',
apiKeyOptional: true,
allowCustomModel: true,
helpHtml: 'Talks to Ollama, LM Studio, vLLM, or llama.cpp on <em>your laptop</em> (not inside Docker). From Docker Desktop, <code>localhost</code> is rewritten to <code>host.docker.internal</code>. Ollama default port is <code>11434</code>; LM Studio is usually <code>1234</code>.',
helpHtml: 'Use the <strong>base URL only</strong> — <code>http://localhost:11434/v1</code>. Do <em>not</em> add <code>/chat/completions</code> (the app appends it). From Docker Desktop, <code>localhost</code> is rewritten to <code>host.docker.internal</code>. Ollama port <code>11434</code>; LM Studio is usually <code>1234</code>.',
models: [
{ id: 'llama3.2', name: 'llama3.2' },
{ id: 'llama3.1', name: 'llama3.1' },
Expand Down
13 changes: 13 additions & 0 deletions tests/python/test_local_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ai_analyzer import (
dummy_key_for_local,
is_local_openai_compat,
openai_compat_base_url,
rewrite_ai_url_for_runtime,
running_in_docker,
)
Expand Down Expand Up @@ -40,6 +41,18 @@ def test_rewrite_localhost_inside_docker(monkeypatch):
)


def test_openai_compat_base_url_strips_chat_completions():
assert openai_compat_base_url("http://localhost:11434/v1") == "http://localhost:11434/v1"
assert (
openai_compat_base_url("http://host.docker.internal:11434/v1/chat/completions")
== "http://host.docker.internal:11434/v1"
)
assert (
openai_compat_base_url("http://localhost:11434/v1/chat/completions/")
== "http://localhost:11434/v1"
)


def test_running_in_docker_env(monkeypatch):
monkeypatch.setenv("CBQA_IN_DOCKER", "true")
assert running_in_docker() is True
Expand Down
Loading