diff --git a/server/.env.example b/server/.env.example index 83058852..853ee50a 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,6 +10,13 @@ MAX_MESSAGES_PER_SESSION=5 # Google Authentication GOOGLE_CLIENT_ID=your-client-id +# 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 # 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..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("http://localhost:11434/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("http://localhost:11434/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 26230344..568eaa95 100644 --- a/server/api/services/ollama_services.py +++ b/server/api/services/ollama_services.py @@ -1,31 +1,38 @@ import requests +from api.config import Config -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("http://localhost:11434/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("http://localhost:11434/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