@@ -63,17 +66,31 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
// For all other pages, render with Langflow-styled navigation and task menu
return (
-
+
-
-
+
+
+
+
+ {settings?.edited && (
+
+
+
+ )}
{children}
diff --git a/pyproject.toml b/pyproject.toml
index f2f66ebe..36031ae4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ dependencies = [
"textual-fspicker>=0.6.0",
"structlog>=25.4.0",
"docling-serve==1.5.0",
+ "docling-core==2.48.1",
"easyocr>=1.7.1"
]
diff --git a/scripts/run_openrag_with_prereqs.sh b/scripts/run_openrag_with_prereqs.sh
new file mode 100755
index 00000000..d620256e
--- /dev/null
+++ b/scripts/run_openrag_with_prereqs.sh
@@ -0,0 +1,345 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+say() { printf "%s\n" "$*" >&2; }
+hr() { say "----------------------------------------"; }
+
+ask_yes_no() {
+ local prompt="${1:-Continue?} [Y/n] "
+ read -r -p "$prompt" ans || true
+ case "${ans:-Y}" in [Yy]|[Yy][Ee][Ss]|"") return 0 ;; *) return 1 ;; esac
+}
+
+# --- Platform detection ------------------------------------------------------
+uname_s="$(uname -s 2>/dev/null || echo unknown)"
+is_wsl=false
+if [ -f /proc/version ]; then grep -qiE 'microsoft|wsl' /proc/version && is_wsl=true || true; fi
+
+case "$uname_s" in
+ Darwin) PLATFORM="macOS" ;;
+ Linux) PLATFORM="$($is_wsl && echo WSL || echo Linux)" ;;
+ CYGWIN*|MINGW*|MSYS*) PLATFORM="Windows" ;;
+ *) PLATFORM="Unknown" ;;
+esac
+
+if [ "$PLATFORM" = "Windows" ]; then
+ say ">>> Native Windows shell detected. Please run this inside WSL (Ubuntu, etc.)."
+ exit 1
+fi
+
+# --- Minimal sudo (used only when necessary) --------------------------------
+SUDO="sudo"; $SUDO -n true >/dev/null 2>&1 || SUDO="sudo" # may prompt later only if needed
+
+# --- PATH probe for common bins (no sudo) -----------------------------------
+ensure_path_has_common_bins() {
+ local add=()
+ [ -d /opt/homebrew/bin ] && add+=("/opt/homebrew/bin")
+ [ -d /usr/local/bin ] && add+=("/usr/local/bin")
+ [ -d "/Applications/Docker.app/Contents/Resources/bin" ] && add+=("/Applications/Docker.app/Contents/Resources/bin")
+ [ -d "$HOME/.docker/cli-plugins" ] && add+=("$HOME/.docker/cli-plugins")
+ for p in "${add[@]}"; do case ":$PATH:" in *":$p:"*) ;; *) PATH="$p:$PATH" ;; esac; done
+ export PATH
+}
+ensure_path_has_common_bins
+
+# --- Helpers ----------------------------------------------------------------
+has_cmd() { command -v "$1" >/dev/null 2>&1; }
+docker_cli_path() { command -v docker 2>/dev/null || true; }
+podman_cli_path() { command -v podman 2>/dev/null || true; }
+
+docker_daemon_ready() { docker info >/dev/null 2>&1; } # no sudo; fails if socket perms/daemon issue
+compose_v2_ready() { docker compose version >/dev/null 2>&1; }
+compose_v1_ready() { command -v docker-compose >/dev/null 2>&1; }
+podman_ready() { podman info >/dev/null 2>&1; } # macOS may need podman machine
+
+docker_is_podman() {
+ # True if `docker` is Podman (podman-docker shim or alias)
+ if ! has_cmd docker; then return 1; fi
+
+ # 1) Text outputs
+ local out=""
+ out+="$(docker --version 2>&1 || true)\n"
+ out+="$(docker -v 2>&1 || true)\n"
+ out+="$(docker help 2>&1 | head -n 2 || true)\n"
+ if printf "%b" "$out" | grep -qiE '\bpodman\b'; then
+ return 0
+ fi
+
+ # 2) Symlink target / alternatives
+ local p t
+ p="$(command -v docker)"
+ if has_cmd readlink; then
+ t="$(readlink -f "$p" 2>/dev/null || readlink "$p" 2>/dev/null || echo "$p")"
+ printf "%s" "$t" | grep -qi 'podman' && return 0
+ fi
+ if [ -L /etc/alternatives/docker ]; then
+ t="$(readlink -f /etc/alternatives/docker 2>/dev/null || true)"
+ printf "%s" "$t" | grep -qi 'podman' && return 0
+ fi
+
+ # 3) Fallback: package id (rpm/dpkg), best effort (ignore errors)
+ if has_cmd rpm; then
+ rpm -qf "$p" 2>/dev/null | grep -qi 'podman' && return 0
+ fi
+ if has_cmd dpkg-query; then
+ dpkg-query -S "$p" 2>/dev/null | grep -qi 'podman' && return 0
+ fi
+
+ return 1
+}
+
+# --- uv install (optional) --------------------------------------------------
+install_uv() {
+ if has_cmd uv; then
+ say ">>> uv present: $(uv --version 2>/dev/null || echo ok)"
+ return
+ fi
+ if ! ask_yes_no "uv not found. Install uv now?"; then return; fi
+ if ! has_cmd curl; then say ">>> curl is required to install uv. Please install curl and re-run."; exit 1; fi
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+}
+
+# --- Docker: install if missing (never reinstall) ---------------------------
+install_docker_if_missing() {
+ if has_cmd docker; then
+ say ">>> Docker CLI detected at: $(docker_cli_path)"
+ say ">>> Version: $(docker --version 2>/dev/null || echo 'unknown')"
+ return
+ fi
+ say ">>> Docker CLI not found."
+ if ! ask_yes_no "Install Docker now?"; then return; fi
+
+ case "$PLATFORM" in
+ macOS)
+ if has_cmd brew; then
+ brew install --cask docker
+ say ">>> Starting Docker Desktop..."
+ open -gj -a Docker || true
+ else
+ say ">>> Homebrew not found. Install from https://brew.sh then: brew install --cask docker"
+ exit 1
+ fi
+ ;;
+ Linux|WSL)
+ if ! has_cmd curl; then say ">>> Need curl to install Docker. Install curl and re-run."; exit 1; fi
+ curl -fsSL https://get.docker.com | $SUDO sh
+ # Do NOT assume docker group exists everywhere; creation is distro-dependent
+ if getent group docker >/dev/null 2>&1; then
+ $SUDO usermod -aG docker "$USER" || true
+ fi
+ ;;
+ *)
+ say ">>> Unsupported platform for automated Docker install."
+ ;;
+ esac
+}
+
+# --- Docker daemon start/wait (sudo only if starting service) ---------------
+start_docker_daemon_if_needed() {
+ if docker_daemon_ready; then
+ say ">>> Docker daemon is ready."
+ return 0
+ fi
+
+ say ">>> Docker CLI found but daemon not reachable."
+ case "$PLATFORM" in
+ macOS)
+ say ">>> Attempting to start Docker Desktop..."
+ open -gj -a Docker || true
+ ;;
+ Linux|WSL)
+ say ">>> Attempting to start docker service (may prompt for sudo)..."
+ $SUDO systemctl start docker >/dev/null 2>&1 || $SUDO service docker start >/dev/null 2>&1 || true
+ ;;
+ esac
+
+ for i in {1..60}; do
+ docker_daemon_ready && { say ">>> Docker daemon is ready."; return 0; }
+ sleep 2
+ done
+
+ say ">>> Still not reachable. If Linux: check 'systemctl status docker' and group membership."
+ say ">>> If macOS: open Docker.app and wait for 'Docker Desktop is running'."
+ return 1
+}
+
+# --- Docker group activation (safe: only if group exists) -------------------
+activate_docker_group_now() {
+ [ "$PLATFORM" = "Linux" ] || [ "$PLATFORM" = "WSL" ] || return 0
+ has_cmd docker || return 0
+
+ # only act if the docker group actually exists
+ if ! getent group docker >/dev/null 2>&1; then
+ return 0
+ fi
+
+ # If user already in group, nothing to do
+ if id -nG "$USER" 2>/dev/null | grep -qw docker; then return 0; fi
+
+ # Re-enter with sg if available
+ if has_cmd sg; then
+ if [ -z "${REENTERED_WITH_DOCKER_GROUP:-}" ]; then
+ say ">>> Re-entering shell with 'docker' group active for this run..."
+ export REENTERED_WITH_DOCKER_GROUP=1
+ exec sg docker -c "REENTERED_WITH_DOCKER_GROUP=1 bash \"$0\""
+ fi
+ else
+ say ">>> You were likely added to 'docker' group. Open a new shell or run: newgrp docker"
+ fi
+}
+
+# --- Compose detection/offer (no reinstall) ---------------------------------
+check_or_offer_compose() {
+ if compose_v2_ready; then
+ say ">>> Docker Compose v2 available (docker compose)."
+ return 0
+ fi
+ if compose_v1_ready; then
+ say ">>> docker-compose (v1) available: $(docker-compose --version 2>/dev/null || echo ok)"
+ return 0
+ fi
+
+ say ">>> Docker Compose not found."
+ if ! ask_yes_no "Install Docker Compose plugin (v2)?"; then
+ say ">>> Skipping Compose install."
+ return 1
+ fi
+
+ case "$PLATFORM" in
+ macOS)
+ say ">>> On macOS, Docker Desktop bundles Compose v2. Starting Desktop…"
+ open -gj -a Docker || true
+ ;;
+ Linux|WSL)
+ if has_cmd apt-get; then $SUDO apt-get update -y && $SUDO apt-get install -y docker-compose-plugin || true
+ elif has_cmd dnf; then $SUDO dnf install -y docker-compose-plugin || true
+ elif has_cmd yum; then $SUDO yum install -y docker-compose-plugin || true
+ elif has_cmd zypper; then $SUDO zypper install -y docker-compose docker-compose-plugin || true
+ elif has_cmd pacman; then $SUDO pacman -Sy --noconfirm docker-compose || true
+ else
+ say ">>> Please install Compose via your distro's instructions."
+ fi
+ ;;
+ esac
+
+ if compose_v2_ready || compose_v1_ready; then
+ say ">>> Compose is now available."
+ else
+ say ">>> Could not verify Compose installation automatically."
+ fi
+}
+
+# --- Podman: install if missing (never reinstall) ---------------------------
+install_podman_if_missing() {
+ if has_cmd podman; then
+ say ">>> Podman CLI detected at: $(podman_cli_path)"
+ say ">>> Version: $(podman --version 2>/dev/null || echo 'unknown')"
+ return
+ fi
+ say ">>> Podman CLI not found."
+ if ! ask_yes_no "Install Podman now?"; then return; fi
+
+ case "$PLATFORM" in
+ macOS)
+ if has_cmd brew; then
+ brew install podman
+ else
+ say ">>> Install Homebrew first (https://brew.sh) then: brew install podman"
+ exit 1
+ fi
+ ;;
+ Linux|WSL)
+ if has_cmd apt-get; then $SUDO apt-get update -y && $SUDO apt-get install -y podman
+ elif has_cmd dnf; then $SUDO dnf install -y podman
+ elif has_cmd yum; then $SUDO yum install -y podman
+ elif has_cmd zypper; then $SUDO zypper install -y podman
+ elif has_cmd pacman; then $SUDO pacman -Sy --noconfirm podman
+ else
+ say ">>> Please install 'podman' via your distro."
+ fi
+ ;;
+ esac
+}
+
+ensure_podman_ready() {
+ if [ "$PLATFORM" = "macOS" ]; then
+ if ! podman machine list 2>/dev/null | grep -q running; then
+ say ">>> Starting Podman machine (macOS)…"
+ podman machine start || true
+ for i in {1..30}; do podman_ready && break || sleep 2; done
+ fi
+ fi
+ if podman_ready; then
+ say ">>> Podman is ready."
+ return 0
+ else
+ say ">>> Podman CLI present but not ready (try 'podman machine start' on macOS)."
+ return 1
+ fi
+}
+
+# --- Runtime auto-detect (prefer no prompt) ---------------------------------
+hr
+say "Platform: $PLATFORM"
+hr
+
+# uv (optional)
+if has_cmd uv; then say ">>> uv present: $(uv --version 2>/dev/null || echo ok)"; else install_uv; fi
+
+RUNTIME=""
+if docker_is_podman; then
+ say ">>> Detected podman-docker shim: using Podman runtime."
+ RUNTIME="Podman"
+elif has_cmd docker; then
+ say ">>> Docker CLI detected."
+ RUNTIME="Docker"
+elif has_cmd podman; then
+ say ">>> Podman CLI detected."
+ RUNTIME="Podman"
+fi
+
+if [ -z "$RUNTIME" ]; then
+ say "Choose container runtime:"
+ PS3="Select [1-2]: "
+ select rt in "Docker" "Podman"; do
+ case "$REPLY" in 1|2) RUNTIME="$rt"; break ;; *) say "Invalid choice";; esac
+ done
+fi
+
+say "Selected runtime: $RUNTIME"
+hr
+
+# --- Execute runtime path ----------------------------------------------------
+if [ "$RUNTIME" = "Docker" ]; then
+ install_docker_if_missing # no reinstall if present
+ activate_docker_group_now # safe: only if group exists and user not in it
+ start_docker_daemon_if_needed # sudo only to start service on Linux/WSL
+ check_or_offer_compose # offer to install Compose only if missing
+else
+ install_podman_if_missing # no reinstall if present
+ ensure_podman_ready
+ # Optional: podman-compose for compose-like UX
+ if ! command -v podman-compose >/dev/null 2>&1; then
+ if ask_yes_no "Install podman-compose (optional)?"; then
+ if has_cmd brew; then brew install podman-compose
+ elif has_cmd apt-get; then $SUDO apt-get update -y && $SUDO apt-get install -y podman-compose || pip3 install --user podman-compose || true
+ elif has_cmd dnf; then $SUDO dnf install -y podman-compose || true
+ elif has_cmd yum; then $SUDO yum install -y podman-compose || true
+ elif has_cmd zypper; then $SUDO zypper install -y podman-compose || true
+ elif has_cmd pacman; then $SUDO pacman -Sy --noconfirm podman-compose || true
+ else say ">>> Please install podman-compose via your distro."; fi
+ fi
+ fi
+fi
+
+hr
+say "Environment ready — launching: uvx openrag"
+hr
+
+if ! has_cmd uv; then
+ say ">>> 'uv' not on PATH. Add the installer’s bin dir to PATH, then run: uvx openrag"
+ exit 1
+fi
+
+exec uvx openrag
+
diff --git a/src/api/provider_health.py b/src/api/provider_health.py
new file mode 100644
index 00000000..f4aa386e
--- /dev/null
+++ b/src/api/provider_health.py
@@ -0,0 +1,117 @@
+"""Provider health check endpoint."""
+
+from starlette.responses import JSONResponse
+from utils.logging_config import get_logger
+from config.settings import get_openrag_config
+from api.provider_validation import validate_provider_setup
+
+logger = get_logger(__name__)
+
+
+async def check_provider_health(request):
+ """
+ Check if the configured provider is healthy and properly validated.
+
+ Query parameters:
+ provider (optional): Provider to check ('openai', 'ollama', 'watsonx').
+ If not provided, checks the currently configured provider.
+
+ Returns:
+ 200: Provider is healthy and validated
+ 400: Invalid provider specified
+ 503: Provider validation failed
+ """
+ try:
+ # Get optional provider from query params
+ query_params = dict(request.query_params)
+ check_provider = query_params.get("provider")
+
+ # Get current config
+ current_config = get_openrag_config()
+
+ # Determine which provider to check
+ if check_provider:
+ provider = check_provider.lower()
+ else:
+ provider = current_config.provider.model_provider
+
+ # Validate provider name
+ valid_providers = ["openai", "ollama", "watsonx"]
+ if provider not in valid_providers:
+ return JSONResponse(
+ {
+ "status": "error",
+ "message": f"Invalid provider: {provider}. Must be one of: {', '.join(valid_providers)}",
+ "provider": provider,
+ },
+ status_code=400,
+ )
+
+ # Get provider configuration
+ if check_provider:
+ # If checking a specific provider, we may not have all config
+ # So we'll try to use what's available or fail gracefully
+ if provider == current_config.provider.model_provider:
+ # Use current config if checking current provider
+ api_key = current_config.provider.api_key
+ endpoint = current_config.provider.endpoint
+ project_id = current_config.provider.project_id
+ llm_model = current_config.agent.llm_model
+ embedding_model = current_config.knowledge.embedding_model
+ else:
+ # For other providers, we can't validate without config
+ return JSONResponse(
+ {
+ "status": "error",
+ "message": f"Cannot validate {provider} - not currently configured. Please configure it first.",
+ "provider": provider,
+ },
+ status_code=400,
+ )
+ else:
+ # Check current provider
+ api_key = current_config.provider.api_key
+ endpoint = current_config.provider.endpoint
+ project_id = current_config.provider.project_id
+ llm_model = current_config.agent.llm_model
+ embedding_model = current_config.knowledge.embedding_model
+
+ logger.info(f"Checking health for provider: {provider}")
+
+ # Validate provider setup
+ await validate_provider_setup(
+ provider=provider,
+ api_key=api_key,
+ embedding_model=embedding_model,
+ llm_model=llm_model,
+ endpoint=endpoint,
+ project_id=project_id,
+ )
+
+ return JSONResponse(
+ {
+ "status": "healthy",
+ "message": "Properly configured and validated",
+ "provider": provider,
+ "details": {
+ "llm_model": llm_model,
+ "embedding_model": embedding_model,
+ "endpoint": endpoint if provider in ["ollama", "watsonx"] else None,
+ },
+ },
+ status_code=200,
+ )
+
+ except Exception as e:
+ error_message = str(e)
+ logger.error(f"Provider health check failed for {provider}: {error_message}")
+
+ return JSONResponse(
+ {
+ "status": "unhealthy",
+ "message": error_message,
+ "provider": provider,
+ },
+ status_code=503,
+ )
+
diff --git a/src/api/settings.py b/src/api/settings.py
index dc9a177c..cdbf873d 100644
--- a/src/api/settings.py
+++ b/src/api/settings.py
@@ -15,6 +15,7 @@ from config.settings import (
get_openrag_config,
config_manager,
)
+from api.provider_validation import validate_provider_setup
logger = get_logger(__name__)
@@ -201,7 +202,115 @@ async def update_settings(request, session_manager):
status_code=400,
)
+ # Validate types early before modifying config
+ if "embedding_model" in body:
+ if (
+ not isinstance(body["embedding_model"], str)
+ or not body["embedding_model"].strip()
+ ):
+ return JSONResponse(
+ {"error": "embedding_model must be a non-empty string"},
+ status_code=400,
+ )
+
+ if "table_structure" in body:
+ if not isinstance(body["table_structure"], bool):
+ return JSONResponse(
+ {"error": "table_structure must be a boolean"}, status_code=400
+ )
+
+ if "ocr" in body:
+ if not isinstance(body["ocr"], bool):
+ return JSONResponse(
+ {"error": "ocr must be a boolean"}, status_code=400
+ )
+
+ if "picture_descriptions" in body:
+ if not isinstance(body["picture_descriptions"], bool):
+ return JSONResponse(
+ {"error": "picture_descriptions must be a boolean"}, status_code=400
+ )
+
+ if "chunk_size" in body:
+ if not isinstance(body["chunk_size"], int) or body["chunk_size"] <= 0:
+ return JSONResponse(
+ {"error": "chunk_size must be a positive integer"}, status_code=400
+ )
+
+ if "chunk_overlap" in body:
+ if not isinstance(body["chunk_overlap"], int) or body["chunk_overlap"] < 0:
+ return JSONResponse(
+ {"error": "chunk_overlap must be a non-negative integer"},
+ status_code=400,
+ )
+
+ if "model_provider" in body:
+ if (
+ not isinstance(body["model_provider"], str)
+ or not body["model_provider"].strip()
+ ):
+ return JSONResponse(
+ {"error": "model_provider must be a non-empty string"},
+ status_code=400,
+ )
+
+ if "api_key" in body:
+ if not isinstance(body["api_key"], str):
+ return JSONResponse(
+ {"error": "api_key must be a string"}, status_code=400
+ )
+
+ if "endpoint" in body:
+ if not isinstance(body["endpoint"], str) or not body["endpoint"].strip():
+ return JSONResponse(
+ {"error": "endpoint must be a non-empty string"}, status_code=400
+ )
+
+ if "project_id" in body:
+ if (
+ not isinstance(body["project_id"], str)
+ or not body["project_id"].strip()
+ ):
+ return JSONResponse(
+ {"error": "project_id must be a non-empty string"}, status_code=400
+ )
+
+ # Validate provider setup if provider-related fields are being updated
+ # Do this BEFORE modifying any config
+ provider_fields = ["model_provider", "api_key", "endpoint", "project_id", "llm_model", "embedding_model"]
+ should_validate = any(field in body for field in provider_fields)
+
+ if should_validate:
+ try:
+ logger.info("Running provider validation before modifying config")
+
+ provider = body.get("model_provider", current_config.provider.model_provider)
+ api_key = body.get("api_key") if "api_key" in body and body["api_key"].strip() else current_config.provider.api_key
+ endpoint = body.get("endpoint", current_config.provider.endpoint)
+ project_id = body.get("project_id", current_config.provider.project_id)
+ llm_model = body.get("llm_model", current_config.agent.llm_model)
+ embedding_model = body.get("embedding_model", current_config.knowledge.embedding_model)
+
+ await validate_provider_setup(
+ provider=provider,
+ api_key=api_key,
+ embedding_model=embedding_model,
+ llm_model=llm_model,
+ endpoint=endpoint,
+ project_id=project_id,
+ )
+
+ logger.info(f"Provider validation successful for {provider}")
+
+ except Exception as e:
+ logger.error(f"Provider validation failed: {str(e)}")
+ return JSONResponse(
+ {"error": f"{str(e)}"},
+ status_code=400
+ )
+
# Update configuration
+ # Only reached if validation passed or wasn't needed
config_updated = False
# Update agent settings
@@ -240,14 +349,6 @@ async def update_settings(request, session_manager):
# Update knowledge settings
if "embedding_model" in body:
- if (
- not isinstance(body["embedding_model"], str)
- or not body["embedding_model"].strip()
- ):
- return JSONResponse(
- {"error": "embedding_model must be a non-empty string"},
- status_code=400,
- )
new_embedding_model = body["embedding_model"].strip()
current_config.knowledge.embedding_model = new_embedding_model
config_updated = True
@@ -297,10 +398,6 @@ async def update_settings(request, session_manager):
# The config will still be saved
if "table_structure" in body:
- if not isinstance(body["table_structure"], bool):
- return JSONResponse(
- {"error": "table_structure must be a boolean"}, status_code=400
- )
current_config.knowledge.table_structure = body["table_structure"]
config_updated = True
@@ -318,10 +415,6 @@ async def update_settings(request, session_manager):
logger.error(f"Failed to update docling settings in flow: {str(e)}")
if "ocr" in body:
- if not isinstance(body["ocr"], bool):
- return JSONResponse(
- {"error": "ocr must be a boolean"}, status_code=400
- )
current_config.knowledge.ocr = body["ocr"]
config_updated = True
@@ -339,10 +432,6 @@ async def update_settings(request, session_manager):
logger.error(f"Failed to update docling settings in flow: {str(e)}")
if "picture_descriptions" in body:
- if not isinstance(body["picture_descriptions"], bool):
- return JSONResponse(
- {"error": "picture_descriptions must be a boolean"}, status_code=400
- )
current_config.knowledge.picture_descriptions = body["picture_descriptions"]
config_updated = True
@@ -360,10 +449,6 @@ async def update_settings(request, session_manager):
logger.error(f"Failed to update docling settings in flow: {str(e)}")
if "chunk_size" in body:
- if not isinstance(body["chunk_size"], int) or body["chunk_size"] <= 0:
- return JSONResponse(
- {"error": "chunk_size must be a positive integer"}, status_code=400
- )
current_config.knowledge.chunk_size = body["chunk_size"]
config_updated = True
@@ -380,11 +465,6 @@ async def update_settings(request, session_manager):
# The config will still be saved
if "chunk_overlap" in body:
- if not isinstance(body["chunk_overlap"], int) or body["chunk_overlap"] < 0:
- return JSONResponse(
- {"error": "chunk_overlap must be a non-negative integer"},
- status_code=400,
- )
current_config.knowledge.chunk_overlap = body["chunk_overlap"]
config_updated = True
@@ -404,43 +484,20 @@ async def update_settings(request, session_manager):
# Update provider settings
if "model_provider" in body:
- if (
- not isinstance(body["model_provider"], str)
- or not body["model_provider"].strip()
- ):
- return JSONResponse(
- {"error": "model_provider must be a non-empty string"},
- status_code=400,
- )
current_config.provider.model_provider = body["model_provider"].strip()
config_updated = True
if "api_key" in body:
- if not isinstance(body["api_key"], str):
- return JSONResponse(
- {"error": "api_key must be a string"}, status_code=400
- )
# Only update if non-empty string (empty string means keep current value)
if body["api_key"].strip():
current_config.provider.api_key = body["api_key"]
config_updated = True
if "endpoint" in body:
- if not isinstance(body["endpoint"], str) or not body["endpoint"].strip():
- return JSONResponse(
- {"error": "endpoint must be a non-empty string"}, status_code=400
- )
current_config.provider.endpoint = body["endpoint"].strip()
config_updated = True
if "project_id" in body:
- if (
- not isinstance(body["project_id"], str)
- or not body["project_id"].strip()
- ):
- return JSONResponse(
- {"error": "project_id must be a non-empty string"}, status_code=400
- )
current_config.provider.project_id = body["project_id"].strip()
config_updated = True
diff --git a/src/main.py b/src/main.py
index 08d2de33..2a85c739 100644
--- a/src/main.py
+++ b/src/main.py
@@ -39,6 +39,7 @@ from api import (
models,
nudges,
oidc,
+ provider_health,
router,
search,
settings,
@@ -986,6 +987,14 @@ async def create_app():
),
methods=["POST"],
),
+ # Provider health check endpoint
+ Route(
+ "/provider/health",
+ require_auth(services["session_manager"])(
+ provider_health.check_provider_health
+ ),
+ methods=["GET"],
+ ),
# Models endpoints
Route(
"/models/openai",
diff --git a/uv.lock b/uv.lock
index 7f86be10..977e447c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2361,6 +2361,7 @@ dependencies = [
{ name = "cryptography" },
{ name = "docling", extra = ["ocrmac"], marker = "sys_platform == 'darwin'" },
{ name = "docling", extra = ["vlm"] },
+ { name = "docling-core" },
{ name = "docling-serve" },
{ name = "easyocr" },
{ name = "google-api-python-client" },
@@ -2399,6 +2400,7 @@ requires-dist = [
{ name = "cryptography", specifier = ">=45.0.6" },
{ name = "docling", extras = ["ocrmac", "vlm"], marker = "sys_platform == 'darwin'", specifier = "==2.41.0" },
{ name = "docling", extras = ["vlm"], marker = "sys_platform != 'darwin'", specifier = "==2.41.0" },
+ { name = "docling-core", specifier = "==2.48.1" },
{ name = "docling-serve", specifier = "==1.5.0" },
{ name = "easyocr", specifier = ">=1.7.1" },
{ name = "google-api-python-client", specifier = ">=2.143.0" },