From 3fedfdcb96d6d7f8c0e3e14ce7e373a987a99527 Mon Sep 17 00:00:00 2001 From: Arnav Goel Date: Wed, 5 Aug 2026 00:33:50 +0530 Subject: [PATCH 1/2] fix: make Ollama base URL configurable via OLLAMA_BASE_URL env var Hardcoded 'http://localhost:11434' prevented deployed instances from reaching a user's local Ollama. The server resolves localhost to itself, not the client's machine. Changes: - config.py: add OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434') - ollama_services.py: use Config.OLLAMA_BASE_URL instead of hardcoded string - chat_routes.py: use Config.OLLAMA_BASE_URL for both /api/generate calls - .env.example: document OLLAMA_BASE_URL with usage example Closes #291 --- server/.env.example | 4 ++++ server/api/config.py | 1 + server/api/routes/chat_routes.py | 4 ++-- server/api/services/ollama_services.py | 7 +++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/server/.env.example b/server/.env.example index 83058852..2a694495 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,6 +10,10 @@ MAX_MESSAGES_PER_SESSION=5 # Google Authentication GOOGLE_CLIENT_ID=your-client-id +# Ollama base URL (override when Ollama runs somewhere other than localhost) +# Example for a remote or Docker-hosted instance: OLLAMA_BASE_URL="http://192.168.1.100:11434" +OLLAMA_BASE_URL="http://localhost:11434" + # Plugin Architecture Config # PLUGINS_DIRS="path/to/plugins1,path/to/plugins2" # Optional: Override the default plugins locations # ENABLED_PLUGINS="example_plugin,another_plugin" # Optional: Comma-separated list of plugins to enable \ No newline at end of file diff --git a/server/api/config.py b/server/api/config.py index 351c7aec..8771efbd 100644 --- a/server/api/config.py +++ b/server/api/config.py @@ -27,3 +27,4 @@ class Config: ENABLED_PLUGINS = os.getenv("ENABLED_PLUGINS", None) # Comma-separated list of plugin folder names MAX_MESSAGES_PER_SESSION = int(os.getenv("MAX_MESSAGES_PER_SESSION", 10)) CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "support@privgpt-studio.com") + OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") diff --git a/server/api/routes/chat_routes.py b/server/api/routes/chat_routes.py index 43fe2f12..a0b5ef83 100644 --- a/server/api/routes/chat_routes.py +++ b/server/api/routes/chat_routes.py @@ -286,7 +286,7 @@ def chat(): payload["system"] = system_prompt try: latency_ms = datetime.now() - response = requests.post("http://localhost:11434/api/generate", json=payload, timeout=60) + response = requests.post(f"{Config.OLLAMA_BASE_URL}/api/generate", json=payload, timeout=60) latency_ms = int((datetime.now() - latency_ms).total_seconds() * 1000) bot_reply = response.json().get("response", "No reply.") @@ -549,7 +549,7 @@ def generate_stream(): payload["options"]["seed"] = seed if system_prompt: payload["system"] = system_prompt - response = requests.post("http://localhost:11434/api/generate", json=payload, stream=True, timeout=60) + response = requests.post(f"{Config.OLLAMA_BASE_URL}/api/generate", json=payload, stream=True, timeout=60) response.raise_for_status() for line in response.iter_lines(): diff --git a/server/api/services/ollama_services.py b/server/api/services/ollama_services.py index 26230344..90c5a707 100644 --- a/server/api/services/ollama_services.py +++ b/server/api/services/ollama_services.py @@ -1,4 +1,7 @@ import requests +from api.config import Config + +OLLAMA_BASE_URL = Config.OLLAMA_BASE_URL def get_available_models(): """ @@ -8,7 +11,7 @@ def get_available_models(): list: Names of available local models (with full tags). """ try: - res = requests.get("http://localhost:11434/api/tags", timeout=5) + res = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5) # Return full model names including tags (e.g., "gemma3:1b" instead of just "gemma3") return sorted(m['name'] for m in res.json().get("models", [])) except: @@ -25,7 +28,7 @@ def get_model_details(model_name): dict: The JSON response from Ollama's /api/show endpoint, or None if failed. """ try: - res = requests.post("http://localhost:11434/api/show", json={"name": model_name}, timeout=5) + res = requests.post(f"{OLLAMA_BASE_URL}/api/show", json={"name": model_name}, timeout=5) if res.status_code == 200: return res.json() return None From 392f5702e49c1ca0e559e79f394b9eed5b5310a3 Mon Sep 17 00:00:00 2001 From: Arnav Goel Date: Thu, 6 Aug 2026 14:34:16 +0530 Subject: [PATCH 2/2] fix: accept per-request ollama_url to support deployed frontends Server env var OLLAMA_BASE_URL is not enough for Vercel deployments: the server's localhost is Vercel's own machine, not the user's. Each endpoint now accepts an optional ollama_url param that takes precedence over the server default, letting users pass a tunnel URL (e.g. ngrok) to reach their local Ollama from a deployed app. - GET /models?ollama_url=... - POST /model_info { ..., ollama_url: '...' } - POST /chat (form field: ollama_url) - POST /chat/stream (form field: ollama_url) OLLAMA_BASE_URL env var is still used as the fallback, so local dev and self-hosted instances need no changes. Co-Authored-By: Claude --- server/.env.example | 7 +++++-- server/api/routes/chat_routes.py | 16 +++++++++++----- server/api/routes/model_routes.py | 14 +++++++++----- server/api/services/ollama_services.py | 16 ++++++++++------ 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/server/.env.example b/server/.env.example index 2a694495..853ee50a 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,8 +10,11 @@ MAX_MESSAGES_PER_SESSION=5 # Google Authentication GOOGLE_CLIENT_ID=your-client-id -# Ollama base URL (override when Ollama runs somewhere other than localhost) -# Example for a remote or Docker-hosted instance: OLLAMA_BASE_URL="http://192.168.1.100:11434" +# Ollama base URL — server-side default when no ollama_url is sent in the request. +# For local dev this is fine. For deployed instances (e.g. Vercel), users must pass +# their own ollama_url in each API request (e.g. an ngrok tunnel to their local machine), +# because the server cannot reach a user's localhost. +# Example for a self-hosted or Docker instance: OLLAMA_BASE_URL="http://192.168.1.100:11434" OLLAMA_BASE_URL="http://localhost:11434" # Plugin Architecture Config diff --git a/server/api/routes/chat_routes.py b/server/api/routes/chat_routes.py index a0b5ef83..5e632cc7 100644 --- a/server/api/routes/chat_routes.py +++ b/server/api/routes/chat_routes.py @@ -164,11 +164,14 @@ def chat(): model_name = request.form.get("model_name", "") session_id = request.form.get("session_id", "1") session_name = request.form.get("session_name", "") - + # User-supplied Ollama URL (e.g. an ngrok tunnel) lets deployed instances + # proxy to the user's local Ollama without changing the server default. + ollama_url = request.form.get("ollama_url") or Config.OLLAMA_BASE_URL + # Plugin: before_prompt if plugin_manager: user_msg = plugin_manager.before_prompt(user_msg) - + user_timestamp = datetime.now() - timedelta(seconds=10) session_id = request.form.get("session_id", "1") @@ -286,7 +289,7 @@ def chat(): payload["system"] = system_prompt try: latency_ms = datetime.now() - response = requests.post(f"{Config.OLLAMA_BASE_URL}/api/generate", json=payload, timeout=60) + response = requests.post(f"{ollama_url}/api/generate", json=payload, timeout=60) latency_ms = int((datetime.now() - latency_ms).total_seconds() * 1000) bot_reply = response.json().get("response", "No reply.") @@ -429,11 +432,14 @@ def chat_stream(): model_name = request.form.get("model_name", "") session_id = request.form.get("session_id", "1") session_name = request.form.get("session_name", "") + # User-supplied Ollama URL (e.g. an ngrok tunnel) lets deployed instances + # proxy to the user's local Ollama without changing the server default. + ollama_url = request.form.get("ollama_url") or Config.OLLAMA_BASE_URL # Plugin: before_prompt if plugin_manager: user_msg = plugin_manager.before_prompt(user_msg) - + if is_session_locked(session_id): def error_generator_locked(): err_msg = "This chat is locked and cannot receive new messages." @@ -549,7 +555,7 @@ def generate_stream(): payload["options"]["seed"] = seed if system_prompt: payload["system"] = system_prompt - response = requests.post(f"{Config.OLLAMA_BASE_URL}/api/generate", json=payload, stream=True, timeout=60) + response = requests.post(f"{ollama_url}/api/generate", json=payload, stream=True, timeout=60) response.raise_for_status() for line in response.iter_lines(): diff --git a/server/api/routes/model_routes.py b/server/api/routes/model_routes.py index 30bdb852..8f990b64 100644 --- a/server/api/routes/model_routes.py +++ b/server/api/routes/model_routes.py @@ -8,11 +8,14 @@ def models(): """ Returns available local and cloud models. + Accepts optional query param ?ollama_url= so deployed frontends can + proxy through a user-supplied Ollama address (e.g. an ngrok tunnel). + Returns: JSON: Dictionary with local_models and cloud_models keys. """ - - local_models = get_available_models() + ollama_url = request.args.get("ollama_url") or None + local_models = get_available_models(ollama_url=ollama_url) cloud_models = ["gemini"] return jsonify({ "local_models": local_models, @@ -23,7 +26,7 @@ def models(): def model_info(): """ Returns detailed information about a specific model. - Expects JSON: { "model_name": "name", "model_type": "local"|"cloud" } + Expects JSON: { "model_name": "name", "model_type": "local"|"cloud", "ollama_url": "..." (optional) } """ data = request.json if not data: @@ -31,6 +34,7 @@ def model_info(): model_name = data.get("model_name") model_type = data.get("model_type", "local") + ollama_url = data.get("ollama_url") or None if not model_name: return jsonify({"error": "Model name is required"}), 400 @@ -51,10 +55,10 @@ def model_info(): } }) - details = get_model_details(model_name) + details = get_model_details(model_name, ollama_url=ollama_url) if details: return jsonify(details) - + return jsonify({"error": "Failed to fetch model info"}), 500 select_model_bp = Blueprint('select_model_bp', __name__) diff --git a/server/api/services/ollama_services.py b/server/api/services/ollama_services.py index 90c5a707..568eaa95 100644 --- a/server/api/services/ollama_services.py +++ b/server/api/services/ollama_services.py @@ -1,34 +1,38 @@ import requests from api.config import Config -OLLAMA_BASE_URL = Config.OLLAMA_BASE_URL - -def get_available_models(): +def get_available_models(ollama_url=None): """ Fetches list of available local models from Ollama. + Args: + ollama_url (str, optional): Custom Ollama base URL. Defaults to Config.OLLAMA_BASE_URL. + Returns: list: Names of available local models (with full tags). """ + base_url = ollama_url or Config.OLLAMA_BASE_URL try: - res = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5) + res = requests.get(f"{base_url}/api/tags", timeout=5) # Return full model names including tags (e.g., "gemma3:1b" instead of just "gemma3") return sorted(m['name'] for m in res.json().get("models", [])) except: return [] -def get_model_details(model_name): +def get_model_details(model_name, ollama_url=None): """ Fetches detailed information for a specific local model from Ollama. Args: model_name (str): The name of the model to inspect. + ollama_url (str, optional): Custom Ollama base URL. Defaults to Config.OLLAMA_BASE_URL. Returns: dict: The JSON response from Ollama's /api/show endpoint, or None if failed. """ + base_url = ollama_url or Config.OLLAMA_BASE_URL try: - res = requests.post(f"{OLLAMA_BASE_URL}/api/show", json={"name": model_name}, timeout=5) + res = requests.post(f"{base_url}/api/show", json={"name": model_name}, timeout=5) if res.status_code == 200: return res.json() return None