diff --git a/.env.demo b/.env.demo
index 073a8425..12f9ebf8 100644
--- a/.env.demo
+++ b/.env.demo
@@ -119,16 +119,44 @@ AUTHENTIK_CONFIG_URL=.cognitiveservices.azure.com/"
-AZURE_AI_KEY=
+# --- Anthropic Claude (AI_PROVIDER=anthropic) ---
+# ANTHROPIC_API_KEY=sk-ant-...
+# AI_MODEL=claude-3-5-sonnet-20241022
+# --- Google Gemini (AI_PROVIDER=gemini) ---
+# GEMINI_API_KEY=AIza...
+# AI_MODEL=gemini-1.5-pro
+
+# --- Ollama local LLMs (AI_PROVIDER=ollama) ---
+# OLLAMA_BASE_URL=http://localhost:11434
+# AI_MODEL=llama3.2
+
+# --- OpenRouter (AI_PROVIDER=openrouter) ---
+# OPENROUTER_API_KEY=sk-or-...
+# AI_MODEL=anthropic/claude-3.5-sonnet
+
+# --- Portkey AI Gateway (AI_PROVIDER=portkey) ---
+# PORTKEY_API_KEY=pk-...
+# PORTKEY_VIRTUAL_KEY=vk-... # optional – routes to provider credentials in Portkey vault
+# PORTKEY_CONFIG=pc-... # optional – saved Config ID for fallbacks / load balancing
+
+# --- Azure OpenAI (AI_PROVIDER=azure) ---
+# OPENAI_API_KEY=
+# OPENAI_BASE_URL=https://my-resource.openai.azure.com
+# AZURE_OPENAI_API_VERSION=2024-02-01
+# AI_MODEL=gpt-4o # deployment name in Azure
+
+# Azure Document Intelligence (OCR – separate from AI provider above)
# **Email Settings**
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
diff --git a/README.md b/README.md
index 4037623f..a149a4e8 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
DocuElevate automates the handling, extraction, and processing of documents using a variety of services, including:
-- **OpenAI** for metadata extraction and text refinement.
+- **AI Provider** (pluggable – OpenAI, Anthropic, Gemini, Ollama, OpenRouter, Portkey, and more) for metadata extraction and text refinement.
- **Dropbox**, **Nextcloud**, and **Google Drive** for file storage and uploads.
- **Paperless NGX** for document indexing and management.
- **Azure Document Intelligence** for OCR on PDFs.
@@ -86,7 +86,7 @@ Documents enter DocuElevate through four possible channels:
Every document goes through the following steps:
1. **PDF Conversion**: Non-PDF files are converted to PDF format using Gotenberg
2. **OCR Processing**: Azure Document Intelligence extracts text from images/scans
-3. **Metadata Extraction**: OpenAI analyzes document content to identify:
+3. **Metadata Extraction**: The configured AI provider analyzes document content to identify:
- Document type (invoice, receipt, contract, etc.)
- Key entities (dates, names, amounts, account numbers)
- Important data points specific to the document type
@@ -116,8 +116,8 @@ Users can choose to send documents to any combination of these destinations thro
- Manual uploads (via API or UI) to Dropbox, Nextcloud, Google Drive, or Paperless
- **OCR Processing (Azure)**:
- Extract text from scanned PDFs using Azure Document Intelligence
-- **Metadata Extraction (OpenAI)**:
- - Use GPT to classify, label, or otherwise enrich the text with structured metadata
+- **Metadata Extraction (AI Provider)**:
+ - Use any supported AI provider (OpenAI, Anthropic, Gemini, Ollama, etc.) to classify, label, or otherwise enrich the text with structured metadata
- **PDF Conversion (Gotenberg)**:
- Convert non-PDF attachments (e.g., Word docs, images) into PDFs
- **Document Management (Paperless NGX)**:
@@ -214,7 +214,8 @@ The following is a summary of the licenses used by our direct dependencies:
| Uvicorn | BSD |
| SQLAlchemy | MIT |
| Pydantic | MIT |
-| OpenAI | MIT |
+| openai | MIT |
+| litellm | MIT |
| pypdf | BSD |
| Requests | Apache 2.0 |
| puremagic | MIT |
diff --git a/app/api/openai.py b/app/api/openai.py
index 83f7155f..07859e50 100644
--- a/app/api/openai.py
+++ b/app/api/openai.py
@@ -1,5 +1,9 @@
"""
-OpenAI API endpoints
+AI provider and OpenAI API endpoints.
+
+Exposes two endpoints:
+- GET /api/ai/test – tests the currently configured AI provider (generic, provider-agnostic)
+- GET /api/openai/test – backward-compatible alias that tests the OpenAI API specifically
"""
import logging
@@ -146,3 +150,55 @@ async def test_openai_connection(request: Request):
except Exception as e:
logger.exception("Unexpected error testing OpenAI connection")
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
+
+
+@router.get("/ai/test")
+@require_login
+async def test_ai_provider_connection(request: Request):
+ """
+ Test the currently configured AI provider connection.
+
+ Uses ``get_ai_provider()`` to instantiate the active provider and sends a
+ minimal chat completion to verify that the credentials and endpoint are
+ reachable. Works for all supported providers (OpenAI, Azure, Anthropic,
+ Gemini, Ollama, OpenRouter, Portkey, LiteLLM).
+ """
+ from app.utils.ai_provider import get_ai_provider
+
+ provider_name = settings.ai_provider
+ model = settings.ai_model or settings.openai_model
+
+ logger.info(f"Testing AI provider connection: provider={provider_name}, model={model}")
+
+ try:
+ provider = get_ai_provider()
+ response = provider.chat_completion(
+ messages=[{"role": "user", "content": "Reply with the single word: ok"}],
+ model=model,
+ temperature=0,
+ max_tokens=5,
+ )
+ logger.info(f"AI provider test successful: provider={provider_name}")
+ return {
+ "status": "success",
+ "message": f"AI provider '{provider_name}' is reachable and responding",
+ "provider": provider_name,
+ "model": model,
+ "response_preview": (response or "")[:50],
+ }
+ except ValueError as e:
+ # Configuration errors (missing keys, unknown provider)
+ logger.warning(f"AI provider configuration error: {e}")
+ return {
+ "status": "error",
+ "message": str(e),
+ "provider": provider_name,
+ }
+ except Exception as e:
+ detail = _get_exception_chain_detail(e)
+ logger.error(f"AI provider test failed for '{provider_name}': {detail}", exc_info=True)
+ return {
+ "status": "error",
+ "message": f"Connection failed: {detail}",
+ "provider": provider_name,
+ }
diff --git a/app/config.py b/app/config.py
index 0def557c..b331d8a1 100644
--- a/app/config.py
+++ b/app/config.py
@@ -15,6 +15,35 @@ class Settings(BaseSettings):
openai_api_key: str
openai_base_url: str = "https://api.openai.com/v1" # Default to OpenAI's endpoint
openai_model: str = "gpt-4o-mini" # Default model
+
+ # AI provider abstraction layer
+ # Supported values: openai, azure, anthropic, gemini, ollama, openrouter, litellm
+ ai_provider: str = "openai"
+ # Override model for any provider; falls back to openai_model when not set
+ ai_model: Optional[str] = None
+
+ # Anthropic Claude settings (used when ai_provider="anthropic")
+ anthropic_api_key: Optional[str] = None
+
+ # Google Gemini settings (used when ai_provider="gemini")
+ gemini_api_key: Optional[str] = None
+
+ # Ollama local LLM settings (used when ai_provider="ollama")
+ ollama_base_url: str = "http://localhost:11434"
+
+ # OpenRouter settings (used when ai_provider="openrouter")
+ openrouter_api_key: Optional[str] = None
+ openrouter_base_url: str = "https://openrouter.ai/api/v1"
+
+ # Portkey AI gateway settings (used when ai_provider="portkey")
+ # See https://portkey.ai for setup instructions
+ portkey_api_key: Optional[str] = None
+ portkey_virtual_key: Optional[str] = None # Routes to a specific provider via Portkey vault
+ portkey_config: Optional[str] = None # Portkey Config ID for advanced routing rules
+ portkey_base_url: str = "https://api.portkey.ai/v1"
+
+ # Azure OpenAI API version (used when ai_provider="azure")
+ azure_openai_api_version: str = "2024-02-01"
workdir: str
debug: bool = False # Default to False
diff --git a/app/tasks/check_credentials.py b/app/tasks/check_credentials.py
index df3c2a2b..698973b9 100644
--- a/app/tasks/check_credentials.py
+++ b/app/tasks/check_credentials.py
@@ -11,7 +11,7 @@ from app.api.google_drive import test_google_drive_token
from app.api.onedrive import test_onedrive_token
# Import the test functions from API routes
-from app.api.openai import test_openai_connection
+from app.api.openai import test_ai_provider_connection, test_openai_connection
from app.celery_app import celery
from app.config import settings
@@ -75,8 +75,17 @@ def unwrap_decorated_function(func):
# Create synchronous versions of the test functions that bypass authentication
+def sync_test_ai_provider_connection():
+ """Synchronous wrapper for the AI provider test function that bypasses auth."""
+ inner_func = unwrap_decorated_function(test_ai_provider_connection)
+ request = MockRequest()
+ if inspect.iscoroutinefunction(inner_func):
+ return asyncio.run(inner_func(request))
+ return inner_func(request)
+
+
def sync_test_openai_connection():
- """Synchronous wrapper for the OpenAI test function that bypasses auth"""
+ """Synchronous wrapper for the OpenAI test function that bypasses auth."""
# Get the original function without the @require_login decorator
inner_func = unwrap_decorated_function(test_openai_connection)
request = MockRequest()
@@ -139,10 +148,10 @@ def check_credentials():
# Define services with their test functions and configuration status
services = [
{
- "name": "OpenAI",
- "check_func": sync_test_openai_connection,
- "configured": provider_status.get("OpenAI", {}).get("configured", False),
- "config_issues": [], # OpenAI isn't in storage_configs
+ "name": "AI Provider",
+ "check_func": sync_test_ai_provider_connection,
+ "configured": provider_status.get("AI Provider", {}).get("configured", False),
+ "config_issues": [],
},
{
"name": "Azure Document Intelligence",
diff --git a/app/tasks/extract_metadata_with_gpt.py b/app/tasks/extract_metadata_with_gpt.py
index c6fd33f9..162a75f3 100644
--- a/app/tasks/extract_metadata_with_gpt.py
+++ b/app/tasks/extract_metadata_with_gpt.py
@@ -5,8 +5,6 @@ import logging
import os
import re
-import openai
-
# Import the shared Celery instance
from app.celery_app import celery
from app.config import settings
@@ -15,17 +13,10 @@ from app.models import FileRecord
from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf
from app.tasks.retry_config import BaseTaskWithRetry
from app.utils import log_task_progress
+from app.utils.ai_provider import get_ai_provider
logger = logging.getLogger(__name__)
-# Initialize OpenAI client dynamically with better error handling
-try:
- client = openai.OpenAI(api_key=settings.openai_api_key, base_url=settings.openai_base_url)
- logger.info("OpenAI client initialized successfully")
-except Exception as e:
- logger.error(f"Failed to initialize OpenAI client: {e}")
- client = None
-
def extract_json_from_text(text):
"""
@@ -114,23 +105,24 @@ def extract_metadata_with_gpt(self, filename: str, cleaned_text: str, file_id: i
try:
logger.info(f"[{task_id}] Sending classification request for {filename}...")
- log_task_progress(task_id, "call_openai", "in_progress", "Calling OpenAI API", file_id=file_id)
- completion = client.chat.completions.create(
- model=settings.openai_model,
+ log_task_progress(task_id, "call_ai_provider", "in_progress", "Calling AI provider API", file_id=file_id)
+ provider = get_ai_provider()
+ model = settings.ai_model or settings.openai_model
+ content = provider.chat_completion(
messages=[
{"role": "system", "content": "You are an intelligent document classifier."},
{"role": "user", "content": prompt},
],
+ model=model,
temperature=0,
)
- content = completion.choices[0].message.content
logger.info(f"[{task_id}] Raw classification response for {filename}: {content[:200]}...")
log_task_progress(
task_id,
- "call_openai",
+ "call_ai_provider",
"success",
- "Received OpenAI response",
+ "Received AI provider response",
file_id=file_id,
detail=f"Raw classification response:\n{content}",
)
@@ -187,13 +179,13 @@ def extract_metadata_with_gpt(self, filename: str, cleaned_text: str, file_id: i
return {"s3_file": os.path.basename(filename), "metadata": metadata}
except Exception as e:
- logger.exception(f"[{task_id}] OpenAI classification failed for {filename}: {e}")
+ logger.exception(f"[{task_id}] AI provider classification failed for {filename}: {e}")
log_task_progress(
task_id,
"extract_metadata_with_gpt",
"failure",
f"Exception: {str(e)}",
file_id=file_id,
- detail=f"OpenAI classification failed for {filename}.\nException: {str(e)}",
+ detail=f"AI provider classification failed for {filename}.\nException: {str(e)}",
)
return {}
diff --git a/app/tasks/refine_text_with_gpt.py b/app/tasks/refine_text_with_gpt.py
index d67f4c94..62b74c14 100644
--- a/app/tasks/refine_text_with_gpt.py
+++ b/app/tasks/refine_text_with_gpt.py
@@ -2,32 +2,29 @@
import logging
-import openai
-
# Import the shared Celery instance
from app.celery_app import celery
from app.config import settings
from app.tasks.retry_config import BaseTaskWithRetry
from app.utils import log_task_progress
+from app.utils.ai_provider import get_ai_provider
logger = logging.getLogger(__name__)
-# Initialize OpenAI client dynamically
-client = openai.OpenAI(api_key=settings.openai_api_key, base_url=settings.openai_base_url)
-
@celery.task(base=BaseTaskWithRetry, bind=True)
def refine_text_with_gpt(self, filename: str, raw_text: str):
- """Uses OpenAI to clean and refine OCR text."""
+ """Uses the configured AI provider to clean and refine OCR text."""
task_id = self.request.id
logger.info(f"[{task_id}] Starting OCR text refinement for: {filename}")
log_task_progress(task_id, "refine_text_with_gpt", "in_progress", f"Refining OCR text for {filename}")
try:
- log_task_progress(task_id, "call_openai", "in_progress", "Calling OpenAI for text refinement")
+ log_task_progress(task_id, "call_ai_provider", "in_progress", "Calling AI provider for text refinement")
- response = client.chat.completions.create(
- model=settings.openai_model,
+ provider = get_ai_provider()
+ model = settings.ai_model or settings.openai_model
+ cleaned_text = provider.chat_completion(
messages=[
{
"role": "system",
@@ -38,16 +35,15 @@ def refine_text_with_gpt(self, filename: str, raw_text: str):
},
{"role": "user", "content": raw_text},
],
+ model=model,
)
- cleaned_text = response.choices[0].message.content
-
logger.info(f"[{task_id}] Text refinement complete for {filename}: {len(cleaned_text)} characters")
log_task_progress(
task_id,
- "call_openai",
+ "call_ai_provider",
"success",
- "Received refined text from OpenAI",
+ "Received refined text from AI provider",
detail=f"Input: {len(raw_text)} chars → Output: {len(cleaned_text)} chars",
)
diff --git a/app/utils/ai_provider.py b/app/utils/ai_provider.py
new file mode 100644
index 00000000..fa9353b0
--- /dev/null
+++ b/app/utils/ai_provider.py
@@ -0,0 +1,452 @@
+#!/usr/bin/env python3
+"""AI provider abstraction layer for DocuElevate.
+
+This module provides a pluggable abstraction for various AI model providers,
+allowing the platform to work with OpenAI, Azure OpenAI, Anthropic Claude,
+Google Gemini, Ollama (local LLMs), OpenRouter, Portkey, and any
+LiteLLM-compatible provider without being locked to a single vendor.
+
+Provider selection is controlled by the ``AI_PROVIDER`` environment variable.
+See the Configuration Guide for full details on each provider's settings.
+"""
+
+import logging
+from abc import ABC, abstractmethod
+from typing import Any, Dict, List, Optional
+
+from app.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+def _require_text_content(content: Optional[str]) -> str:
+ """Raise a clear error if the AI response contains no text content.
+
+ This can happen when the model returns a tool/function call instead of
+ a plain text message. All DocuElevate prompts expect a plain-text or
+ JSON response, so ``None`` content is always an unexpected condition.
+
+ Args:
+ content: The ``message.content`` value from the completion response.
+
+ Returns:
+ The original string, guaranteed non-None.
+
+ Raises:
+ ValueError: If *content* is ``None``.
+ """
+ if content is None:
+ raise ValueError(
+ "AI provider returned a response with no text content (content=None). "
+ "This may occur when the model generates a tool call instead of a plain text reply. "
+ "Ensure the model and prompt are configured for text/JSON output."
+ )
+ return content
+
+
+class AIProvider(ABC):
+ """Abstract base class for AI chat completion providers.
+
+ All concrete providers must implement :meth:`chat_completion`, which
+ accepts a list of chat messages and returns the model's response as a
+ plain string. The interface intentionally mirrors the OpenAI Chat
+ Completions API so that callers need no provider-specific knowledge.
+ """
+
+ @abstractmethod
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ """Get a chat completion from the AI provider.
+
+ Args:
+ messages: List of message dicts with ``role`` and ``content`` keys.
+ model: Model name/identifier to use (provider-specific format).
+ temperature: Sampling temperature (0–1). Default: 0 (deterministic).
+ **kwargs: Additional provider-specific arguments passed through.
+
+ Returns:
+ The model's response as a plain string.
+
+ Raises:
+ Exception: If the underlying API call fails.
+ """
+
+
+class OpenAIProvider(AIProvider):
+ """OpenAI provider using the ``openai`` Python SDK.
+
+ Also works as a drop-in for any OpenAI-compatible API endpoint, including
+ LocalAI and LM Studio. Ollama and OpenRouter have dedicated providers with
+ sensible defaults, but this provider works for them too when a custom
+ ``base_url`` is supplied.
+ """
+
+ def __init__(self, api_key: str, base_url: Optional[str] = None) -> None:
+ import openai
+
+ self._client = openai.OpenAI(
+ api_key=api_key,
+ base_url=base_url or "https://api.openai.com/v1",
+ )
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ completion = self._client.chat.completions.create(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ **kwargs,
+ )
+ _content = completion.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class AzureOpenAIProvider(AIProvider):
+ """Azure OpenAI provider using the ``openai`` Python SDK's Azure client."""
+
+ def __init__(self, api_key: str, azure_endpoint: str, api_version: str = "2024-02-01") -> None:
+ import openai
+
+ self._client = openai.AzureOpenAI(
+ api_key=api_key,
+ azure_endpoint=azure_endpoint,
+ api_version=api_version,
+ )
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ completion = self._client.chat.completions.create(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ **kwargs,
+ )
+ _content = completion.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class AnthropicProvider(AIProvider):
+ """Anthropic Claude provider routed via LiteLLM.
+
+ Requires ``litellm`` to be installed. Model names should be in Anthropic
+ format (e.g. ``claude-3-5-sonnet-20241022``); the ``anthropic/`` prefix is
+ added automatically when absent.
+ """
+
+ def __init__(self, api_key: str) -> None:
+ self._api_key = api_key
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ import litellm
+
+ model_name = model if model.startswith("anthropic/") else f"anthropic/{model}"
+ response = litellm.completion(
+ model=model_name,
+ messages=messages,
+ temperature=temperature,
+ api_key=self._api_key,
+ **kwargs,
+ )
+ _content = response.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class GeminiProvider(AIProvider):
+ """Google Gemini provider routed via LiteLLM.
+
+ Requires ``litellm`` to be installed. Model names should be in Gemini
+ format (e.g. ``gemini-1.5-pro``); the ``gemini/`` prefix is added
+ automatically when absent.
+ """
+
+ def __init__(self, api_key: str) -> None:
+ self._api_key = api_key
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ import litellm
+
+ model_name = model if model.startswith("gemini/") else f"gemini/{model}"
+ response = litellm.completion(
+ model=model_name,
+ messages=messages,
+ temperature=temperature,
+ api_key=self._api_key,
+ **kwargs,
+ )
+ _content = response.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class OllamaProvider(AIProvider):
+ """Ollama local LLM provider via its OpenAI-compatible REST API.
+
+ Ollama exposes an OpenAI-compatible endpoint at ``/v1``. Any model
+ pulled into your Ollama instance (e.g. ``llama3.2``, ``qwen2.5``,
+ ``phi3``) can be used directly by name.
+
+ For CPU-only deployments the recommended models are:
+
+ * ``llama3.2`` (3B) – good balance of speed and quality
+ * ``qwen2.5`` (3B/7B) – excellent at structured JSON output
+ * ``phi3`` (3.8B) – strong reasoning, fast on CPU
+
+ See https://ollama.com for installation and model management.
+ """
+
+ def __init__(self, base_url: str = "http://localhost:11434") -> None:
+ import openai
+
+ self._client = openai.OpenAI(
+ api_key="ollama", # Ollama does not require a real API key
+ base_url=f"{base_url.rstrip('/')}/v1",
+ )
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ completion = self._client.chat.completions.create(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ **kwargs,
+ )
+ _content = completion.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class OpenRouterProvider(AIProvider):
+ """OpenRouter AI aggregator (https://openrouter.ai).
+
+ OpenRouter provides access to 100+ models from OpenAI, Anthropic, Google,
+ Meta, Mistral, and many others through a single OpenAI-compatible endpoint.
+ Model names use the ``provider/model`` format (e.g.
+ ``anthropic/claude-3.5-sonnet``, ``google/gemini-pro``).
+ """
+
+ def __init__(self, api_key: str, base_url: str = "https://openrouter.ai/api/v1") -> None:
+ import openai
+
+ self._client = openai.OpenAI(
+ api_key=api_key,
+ base_url=base_url,
+ )
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ completion = self._client.chat.completions.create(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ **kwargs,
+ )
+ _content = completion.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class PortkeyProvider(AIProvider):
+ """Portkey AI gateway (https://portkey.ai).
+
+ Portkey is an AI gateway that provides observability, caching, automatic
+ retries, fallbacks, and load balancing across 200+ LLMs via a single
+ OpenAI-compatible endpoint.
+
+ Required settings:
+ ``PORTKEY_API_KEY`` – your Portkey account API key.
+
+ Optional settings:
+ ``PORTKEY_VIRTUAL_KEY`` – a Portkey *Virtual Key* that maps to the
+ credentials of a specific provider stored in your Portkey vault.
+ When set, you do not need to expose the underlying provider's API key
+ in your environment.
+
+ ``PORTKEY_CONFIG`` – a saved Portkey *Config* ID (e.g.
+ ``pc-my-config-abc123``) that applies advanced routing rules such as
+ fallbacks and load balancing.
+
+ ``PORTKEY_BASE_URL`` – override the gateway endpoint.
+ Default: ``https://api.portkey.ai/v1``.
+
+ The model name should match what the underlying provider expects (e.g.
+ ``gpt-4o`` for OpenAI, ``claude-3-5-sonnet-20241022`` for Anthropic via a
+ virtual key).
+ """
+
+ def __init__(
+ self,
+ api_key: str,
+ virtual_key: Optional[str] = None,
+ config: Optional[str] = None,
+ base_url: str = "https://api.portkey.ai/v1",
+ ) -> None:
+ import openai
+
+ portkey_headers: Dict[str, str] = {"x-portkey-api-key": api_key}
+ if virtual_key:
+ portkey_headers["x-portkey-virtual-key"] = virtual_key
+ if config:
+ portkey_headers["x-portkey-config"] = config
+
+ self._client = openai.OpenAI(
+ api_key=api_key,
+ base_url=base_url,
+ default_headers=portkey_headers,
+ )
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ completion = self._client.chat.completions.create(
+ model=model,
+ messages=messages,
+ temperature=temperature,
+ **kwargs,
+ )
+ _content = completion.choices[0].message.content
+ return _require_text_content(_content)
+
+
+class LiteLLMProvider(AIProvider):
+ """LiteLLM provider – unified interface for 100+ LLMs.
+
+ LiteLLM (https://litellm.ai) translates calls to a single interface that
+ supports OpenAI, Azure, Anthropic, Gemini, Cohere, Ollama, and many more.
+ Use the LiteLLM model-string format ``provider/model`` (e.g.
+ ``openai/gpt-4o``, ``anthropic/claude-3-5-sonnet-20241022``,
+ ``ollama/llama3.2``).
+
+ This provider is useful when you want LiteLLM to handle all routing and
+ need features like automatic retries, fallbacks, or cost tracking.
+ """
+
+ def __init__(self, api_key: Optional[str] = None, api_base: Optional[str] = None) -> None:
+ self._api_key = api_key
+ self._api_base = api_base
+
+ def chat_completion(
+ self,
+ messages: List[Dict[str, str]],
+ model: str,
+ temperature: float = 0,
+ **kwargs: Any,
+ ) -> str:
+ import litellm
+
+ completion_kwargs: Dict[str, Any] = {
+ "model": model,
+ "messages": messages,
+ "temperature": temperature,
+ }
+ if self._api_key:
+ completion_kwargs["api_key"] = self._api_key
+ if self._api_base:
+ completion_kwargs["api_base"] = self._api_base
+ completion_kwargs.update(kwargs)
+ response = litellm.completion(**completion_kwargs)
+ _content = response.choices[0].message.content
+ return _require_text_content(_content)
+
+
+def get_ai_provider() -> AIProvider:
+ """Factory function that creates and returns the configured AI provider.
+
+ Reads ``settings.ai_provider`` (set via the ``AI_PROVIDER`` environment
+ variable) to select the provider implementation. Provider-specific
+ credentials and URLs are read from their corresponding settings fields.
+
+ Returns:
+ An :class:`AIProvider` instance ready to serve chat completions.
+
+ Raises:
+ ValueError: If the configured provider name is not recognised.
+ ValueError: If required credentials for the selected provider are absent.
+ """
+ provider = settings.ai_provider.lower()
+ logger.debug(f"Creating AI provider: {provider}")
+
+ if provider == "openai":
+ return OpenAIProvider(
+ api_key=settings.openai_api_key,
+ base_url=settings.openai_base_url,
+ )
+ elif provider == "azure":
+ return AzureOpenAIProvider(
+ api_key=settings.openai_api_key,
+ azure_endpoint=settings.openai_base_url,
+ api_version=settings.azure_openai_api_version,
+ )
+ elif provider == "anthropic":
+ if not settings.anthropic_api_key:
+ raise ValueError("ANTHROPIC_API_KEY must be set when AI_PROVIDER='anthropic'")
+ return AnthropicProvider(api_key=settings.anthropic_api_key)
+ elif provider == "gemini":
+ if not settings.gemini_api_key:
+ raise ValueError("GEMINI_API_KEY must be set when AI_PROVIDER='gemini'")
+ return GeminiProvider(api_key=settings.gemini_api_key)
+ elif provider == "ollama":
+ return OllamaProvider(base_url=settings.ollama_base_url)
+ elif provider == "openrouter":
+ if not settings.openrouter_api_key:
+ raise ValueError("OPENROUTER_API_KEY must be set when AI_PROVIDER='openrouter'")
+ return OpenRouterProvider(
+ api_key=settings.openrouter_api_key,
+ base_url=settings.openrouter_base_url,
+ )
+ elif provider == "portkey":
+ if not settings.portkey_api_key:
+ raise ValueError("PORTKEY_API_KEY must be set when AI_PROVIDER='portkey'")
+ return PortkeyProvider(
+ api_key=settings.portkey_api_key,
+ virtual_key=settings.portkey_virtual_key,
+ config=settings.portkey_config,
+ base_url=settings.portkey_base_url,
+ )
+ elif provider == "litellm":
+ return LiteLLMProvider(
+ api_key=settings.openai_api_key or None,
+ api_base=settings.openai_base_url if settings.openai_base_url != "https://api.openai.com/v1" else None,
+ )
+ else:
+ raise ValueError(
+ f"Unknown AI provider: '{provider}'. "
+ "Supported providers: openai, azure, anthropic, gemini, ollama, openrouter, portkey, litellm"
+ )
diff --git a/app/utils/config_validator/providers.py b/app/utils/config_validator/providers.py
index 3f37117d..5880ee91 100644
--- a/app/utils/config_validator/providers.py
+++ b/app/utils/config_validator/providers.py
@@ -57,20 +57,69 @@ def get_provider_status() -> dict[str, dict[str, object]]:
"test_endpoint": "/api/diagnostic/test-notification",
}
- # Add AI services first
- providers["OpenAI"] = {
- "name": "OpenAI",
- "icon": "fa-brands fa-openai",
- "configured": bool(
- getattr(settings, "openai_api_key", None) and str(getattr(settings, "openai_api_key", "")).startswith("sk-")
- ),
+ # Add AI provider (dynamic – reflects whichever provider is configured)
+ ai_provider_name = getattr(settings, "ai_provider", "openai").lower()
+ model = getattr(settings, "ai_model", None) or getattr(settings, "openai_model", "gpt-4o-mini")
+
+ # Determine whether the active provider has its required credentials set
+ def _ai_configured() -> bool:
+ if ai_provider_name in ("openai", "azure", "litellm"):
+ return bool(getattr(settings, "openai_api_key", None))
+ elif ai_provider_name == "anthropic":
+ return bool(getattr(settings, "anthropic_api_key", None))
+ elif ai_provider_name == "gemini":
+ return bool(getattr(settings, "gemini_api_key", None))
+ elif ai_provider_name == "ollama":
+ return bool(getattr(settings, "ollama_base_url", None))
+ elif ai_provider_name == "openrouter":
+ return bool(getattr(settings, "openrouter_api_key", None))
+ elif ai_provider_name == "portkey":
+ return bool(getattr(settings, "portkey_api_key", None))
+ return False
+
+ # Build provider-specific detail rows
+ def _ai_details() -> dict:
+ base = {"provider": ai_provider_name, "model": model}
+ if ai_provider_name in ("openai", "azure", "litellm"):
+ base["api_key"] = mask_sensitive_value(getattr(settings, "openai_api_key", None))
+ base["base_url"] = getattr(settings, "openai_base_url", "https://api.openai.com/v1")
+ elif ai_provider_name == "anthropic":
+ base["api_key"] = mask_sensitive_value(getattr(settings, "anthropic_api_key", None))
+ elif ai_provider_name == "gemini":
+ base["api_key"] = mask_sensitive_value(getattr(settings, "gemini_api_key", None))
+ elif ai_provider_name == "ollama":
+ base["base_url"] = getattr(settings, "ollama_base_url", "http://localhost:11434")
+ elif ai_provider_name == "openrouter":
+ base["api_key"] = mask_sensitive_value(getattr(settings, "openrouter_api_key", None))
+ base["base_url"] = getattr(settings, "openrouter_base_url", "https://openrouter.ai/api/v1")
+ elif ai_provider_name == "portkey":
+ base["api_key"] = mask_sensitive_value(getattr(settings, "portkey_api_key", None))
+ if getattr(settings, "portkey_virtual_key", None):
+ base["virtual_key"] = mask_sensitive_value(settings.portkey_virtual_key)
+ if getattr(settings, "portkey_config", None):
+ base["config"] = settings.portkey_config
+ return base
+
+ _PROVIDER_ICONS = {
+ "openai": "fa-brands fa-openai",
+ "azure": "fa-brands fa-microsoft",
+ "anthropic": "fa-solid fa-robot",
+ "gemini": "fa-brands fa-google",
+ "ollama": "fa-solid fa-server",
+ "openrouter": "fa-solid fa-route",
+ "portkey": "fa-solid fa-key",
+ "litellm": "fa-solid fa-layer-group",
+ }
+
+ providers["AI Provider"] = {
+ "name": "AI Provider",
+ "icon": _PROVIDER_ICONS.get(ai_provider_name, "fa-solid fa-microchip"),
+ "configured": _ai_configured(),
"enabled": True,
- "description": "AI-powered document analysis and metadata extraction",
- "details": {
- "api_key": mask_sensitive_value(getattr(settings, "openai_api_key", None)),
- "base_url": getattr(settings, "openai_base_url", "https://api.openai.com/v1"),
- "model": getattr(settings, "openai_model", "gpt-4"),
- },
+ "description": f"AI-powered metadata extraction and OCR refinement ({ai_provider_name})",
+ "details": _ai_details(),
+ "testable": True,
+ "test_endpoint": "/api/ai/test",
}
providers["Azure AI"] = {
diff --git a/app/utils/config_validator/settings_display.py b/app/utils/config_validator/settings_display.py
index ee2a06ba..44015461 100644
--- a/app/utils/config_validator/settings_display.py
+++ b/app/utils/config_validator/settings_display.py
@@ -170,9 +170,21 @@ def get_settings_for_display(show_values: bool = False) -> dict[str, list[dict[s
"s3_acl",
],
"AI Services": [
+ "ai_provider",
+ "ai_model",
"openai_api_key",
"openai_base_url",
"openai_model",
+ "anthropic_api_key",
+ "gemini_api_key",
+ "ollama_base_url",
+ "openrouter_api_key",
+ "openrouter_base_url",
+ "portkey_api_key",
+ "portkey_virtual_key",
+ "portkey_config",
+ "portkey_base_url",
+ "azure_openai_api_version",
"azure_ai_key",
"azure_endpoint",
"azure_region",
diff --git a/app/utils/settings_service.py b/app/utils/settings_service.py
index fb813bbb..5365d7d7 100644
--- a/app/utils/settings_service.py
+++ b/app/utils/settings_service.py
@@ -136,15 +136,15 @@ SETTING_METADATA = {
# AI Services
"openai_api_key": {
"category": "AI Services",
- "description": "OpenAI API key for metadata extraction",
+ "description": "API key for the AI provider (required for OpenAI, Azure, LiteLLM; unused for Ollama)",
"type": "string",
"sensitive": True,
- "required": True,
+ "required": False,
"restart_required": False,
},
"openai_base_url": {
"category": "AI Services",
- "description": "OpenAI API base URL (default: https://api.openai.com/v1)",
+ "description": "API base URL – override for Azure endpoints, local proxies, or LiteLLM (default: https://api.openai.com/v1)",
"type": "string",
"sensitive": False,
"required": False,
@@ -152,34 +152,132 @@ SETTING_METADATA = {
},
"openai_model": {
"category": "AI Services",
- "description": "OpenAI model to use (e.g., gpt-4o-mini)",
+ "description": "Fallback model name used when AI_MODEL is not set (e.g. gpt-4o-mini)",
"type": "string",
"sensitive": False,
"required": False,
"restart_required": False,
},
- "azure_ai_key": {
+ "ai_provider": {
"category": "AI Services",
- "description": "Azure AI key for document intelligence",
+ "description": "Active AI provider for metadata extraction and OCR refinement",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ "options": ["openai", "azure", "anthropic", "gemini", "ollama", "openrouter", "portkey", "litellm"],
+ },
+ "ai_model": {
+ "category": "AI Services",
+ "description": "Model name for the selected provider (overrides OPENAI_MODEL). E.g. gpt-4o, claude-3-5-sonnet-20241022, gemini-1.5-pro, llama3.2",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "anthropic_api_key": {
+ "category": "AI Services",
+ "description": "Anthropic API key (required when AI_PROVIDER=anthropic)",
"type": "string",
"sensitive": True,
- "required": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "gemini_api_key": {
+ "category": "AI Services",
+ "description": "Google AI Studio API key (required when AI_PROVIDER=gemini)",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "ollama_base_url": {
+ "category": "AI Services",
+ "description": "Ollama server URL for local LLM inference (used when AI_PROVIDER=ollama)",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "openrouter_api_key": {
+ "category": "AI Services",
+ "description": "OpenRouter API key (required when AI_PROVIDER=openrouter)",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "openrouter_base_url": {
+ "category": "AI Services",
+ "description": "OpenRouter gateway URL (default: https://openrouter.ai/api/v1)",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "portkey_api_key": {
+ "category": "AI Services",
+ "description": "Portkey account API key (required when AI_PROVIDER=portkey)",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "portkey_virtual_key": {
+ "category": "AI Services",
+ "description": "Portkey Virtual Key – routes to provider credentials stored in the Portkey vault (optional)",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
+ "restart_required": False,
+ },
+ "portkey_config": {
+ "category": "AI Services",
+ "description": "Portkey Config ID for advanced routing, fallbacks, and load balancing (optional, e.g. pc-my-config-abc123)",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "portkey_base_url": {
+ "category": "AI Services",
+ "description": "Portkey gateway URL (default: https://api.portkey.ai/v1; override for self-hosted deployments)",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ "azure_openai_api_version": {
+ "category": "AI Services",
+ "description": "Azure OpenAI API version string (used when AI_PROVIDER=azure, default: 2024-02-01)",
+ "type": "string",
+ "sensitive": False,
+ "required": False,
+ "restart_required": False,
+ },
+ # Azure Document Intelligence (OCR) – separate from the AI provider above
+ "azure_ai_key": {
+ "category": "AI Services",
+ "description": "Azure Document Intelligence API key for OCR processing",
+ "type": "string",
+ "sensitive": True,
+ "required": False,
"restart_required": False,
},
"azure_region": {
"category": "AI Services",
- "description": "Azure region for AI services",
+ "description": "Azure region for Document Intelligence services (e.g., eastus)",
"type": "string",
"sensitive": False,
- "required": True,
+ "required": False,
"restart_required": False,
},
"azure_endpoint": {
"category": "AI Services",
- "description": "Azure AI endpoint URL",
+ "description": "Azure Document Intelligence endpoint URL",
"type": "string",
"sensitive": False,
- "required": True,
+ "required": False,
"restart_required": False,
},
# Storage Providers - Dropbox
diff --git a/app/utils/setup_wizard.py b/app/utils/setup_wizard.py
index 6adba83f..712fd27d 100644
--- a/app/utils/setup_wizard.py
+++ b/app/utils/setup_wizard.py
@@ -90,10 +90,21 @@ def get_required_settings() -> List[Dict[str, Any]]:
"wizard_step": 2,
"wizard_category": "Security",
},
+ {
+ "key": "ai_provider",
+ "label": "AI Provider",
+ "description": "AI provider for metadata extraction and OCR refinement (openai, azure, anthropic, gemini, ollama, openrouter, portkey, litellm)",
+ "type": "string",
+ "sensitive": False,
+ "default": "openai",
+ "options": ["openai", "azure", "anthropic", "gemini", "ollama", "openrouter", "portkey", "litellm"],
+ "wizard_step": 3,
+ "wizard_category": "AI Services",
+ },
{
"key": "openai_api_key",
- "label": "OpenAI API Key",
- "description": "API key for OpenAI services (metadata extraction)",
+ "label": "API Key (OpenAI / Azure / LiteLLM)",
+ "description": "API key for OpenAI, Azure OpenAI, or LiteLLM (not required for Ollama)",
"type": "string",
"sensitive": True,
"default": None,
@@ -101,32 +112,12 @@ def get_required_settings() -> List[Dict[str, Any]]:
"wizard_category": "AI Services",
},
{
- "key": "azure_ai_key",
- "label": "Azure AI Key",
- "description": "Azure AI key for document intelligence (OCR)",
- "type": "string",
- "sensitive": True,
- "default": None,
- "wizard_step": 3,
- "wizard_category": "AI Services",
- },
- {
- "key": "azure_region",
- "label": "Azure Region",
- "description": "Azure region for AI services (e.g., eastus)",
+ "key": "openai_model",
+ "label": "Default Model",
+ "description": "Model name used when AI_MODEL is not set (e.g. gpt-4o-mini, claude-3-5-sonnet-20241022, llama3.2)",
"type": "string",
"sensitive": False,
- "default": "eastus",
- "wizard_step": 3,
- "wizard_category": "AI Services",
- },
- {
- "key": "azure_endpoint",
- "label": "Azure Endpoint",
- "description": "Azure AI endpoint URL",
- "type": "string",
- "sensitive": False,
- "default": None,
+ "default": "gpt-4o-mini",
"wizard_step": 3,
"wizard_category": "AI Services",
},
@@ -147,8 +138,6 @@ def is_setup_required() -> bool:
critical_settings = [
("session_secret", ["INSECURE_DEFAULT_FOR_DEVELOPMENT_ONLY_DO_NOT_USE_IN_PRODUCTION_MINIMUM_32_CHARS"]),
("admin_password", [None, "", "your_secure_password", "changeme", "admin"]),
- ("openai_api_key", [None, "", "", "test-key"]),
- ("azure_ai_key", [None, "", "", "test-key"]),
]
for setting_key, invalid_values in critical_settings:
diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md
index de57935b..b44cbbae 100644
--- a/docs/ConfigurationGuide.md
+++ b/docs/ConfigurationGuide.md
@@ -302,11 +302,154 @@ SECURITY_HEADER_CSP_VALUE="default-src 'self'; script-src 'self' https://trusted
- [Deployment Guide - Security Headers](DeploymentGuide.md#security-headers) for Traefik/Nginx examples
- [SECURITY_AUDIT.md](../SECURITY_AUDIT.md#infrastructure-security) for security rationale
-### OpenAI & Azure Document Intelligence
+### AI Provider & Model Selection
+
+DocuElevate supports multiple AI providers for metadata extraction and OCR text refinement. Select the provider via `AI_PROVIDER` and configure the matching credentials below.
+
+| **Variable** | **Description** | **Default** |
+|-------------------|-----------------------------------------------------------------------|--------------------|
+| `AI_PROVIDER` | Active AI provider. See supported values below. | `openai` |
+| `AI_MODEL` | Model name for the selected provider. Falls back to `OPENAI_MODEL` when not set. | *(unset)* |
+| `OPENAI_MODEL` | Default model name (used when `AI_MODEL` is not set). | `gpt-4o-mini` |
+
+**Supported `AI_PROVIDER` values**: `openai`, `azure`, `anthropic`, `gemini`, `ollama`, `openrouter`, `portkey`, `litellm`
+
+---
+
+#### OpenAI (default)
+
+| **Variable** | **Description** | **Default** |
+|-----------------------|--------------------------------------------------|----------------------------------|
+| `OPENAI_API_KEY` | OpenAI API key. | *(required)* |
+| `OPENAI_BASE_URL` | API base URL. Change for compatible proxies. | `https://api.openai.com/v1` |
+
+```bash
+AI_PROVIDER=openai
+OPENAI_API_KEY=sk-...
+OPENAI_MODEL=gpt-4o-mini
+```
+
+#### Azure OpenAI
+
+| **Variable** | **Description** | **Default** |
+|-------------------------------|----------------------------------------------|----------------|
+| `OPENAI_API_KEY` | Azure OpenAI API key. | *(required)* |
+| `OPENAI_BASE_URL` | Azure resource endpoint URL. | *(required)* |
+| `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version string. | `2024-02-01` |
+
+```bash
+AI_PROVIDER=azure
+OPENAI_API_KEY=
+OPENAI_BASE_URL=https://my-resource.openai.azure.com
+AI_MODEL=gpt-4o # deployment name in Azure
+```
+
+#### Anthropic Claude
+
+| **Variable** | **Description** |
+|---------------------|--------------------------|
+| `ANTHROPIC_API_KEY` | Anthropic API key. |
+
+```bash
+AI_PROVIDER=anthropic
+ANTHROPIC_API_KEY=sk-ant-...
+AI_MODEL=claude-3-5-sonnet-20241022
+```
+
+#### Google Gemini
+
+| **Variable** | **Description** |
+|-------------------|----------------------------|
+| `GEMINI_API_KEY` | Google AI Studio API key. |
+
+```bash
+AI_PROVIDER=gemini
+GEMINI_API_KEY=AIza...
+AI_MODEL=gemini-1.5-pro
+```
+
+#### Ollama (local LLMs – CPU-friendly)
+
+Run models locally using [Ollama](https://ollama.com). Recommended for CPU-only deployments:
+
+| **Variable** | **Description** | **Default** |
+|--------------------|-----------------------------------------|---------------------------|
+| `OLLAMA_BASE_URL` | Ollama server URL. | `http://localhost:11434` |
+
+```bash
+AI_PROVIDER=ollama
+OLLAMA_BASE_URL=http://ollama:11434 # Docker service name
+AI_MODEL=llama3.2 # or qwen2.5, phi3, etc.
+```
+
+Recommended models for document processing on CPU:
+
+- **`llama3.2`** (3B) – good balance of speed and JSON output quality
+- **`qwen2.5`** (3B/7B) – excellent at structured extraction
+- **`phi3`** (3.8B) – strong reasoning, very fast on CPU
+
+#### OpenRouter
+
+[OpenRouter](https://openrouter.ai) provides access to 100+ models from a single endpoint using the `provider/model` name format.
+
+| **Variable** | **Description** | **Default** |
+|-------------------------|-------------------------------------|-----------------------------------|
+| `OPENROUTER_API_KEY` | OpenRouter API key. | *(required)* |
+| `OPENROUTER_BASE_URL` | Override the gateway URL. | `https://openrouter.ai/api/v1` |
+
+```bash
+AI_PROVIDER=openrouter
+OPENROUTER_API_KEY=sk-or-...
+AI_MODEL=anthropic/claude-3.5-sonnet
+```
+
+#### Portkey AI Gateway
+
+[Portkey](https://portkey.ai) is an AI gateway that adds observability, caching, fallbacks, and load balancing across 200+ models behind a single OpenAI-compatible endpoint.
+
+| **Variable** | **Description** | **Default** |
+|-----------------------|----------------------------------------------------------------------------------------------------------|----------------------------------|
+| `PORTKEY_API_KEY` | Portkey account API key. | *(required)* |
+| `PORTKEY_VIRTUAL_KEY` | Optional Virtual Key (stores provider credentials in Portkey vault, keeping them out of your env file). | *(unset)* |
+| `PORTKEY_CONFIG` | Optional saved Config ID (e.g. `pc-fallback-abc123`) for routing rules, fallbacks, and load balancing. | *(unset)* |
+| `PORTKEY_BASE_URL` | Override the Portkey gateway URL (for self-hosted deployments). | `https://api.portkey.ai/v1` |
+
+```bash
+AI_PROVIDER=portkey
+PORTKEY_API_KEY=pk-...
+PORTKEY_VIRTUAL_KEY=vk-openai-abc123 # optional – routes to your OpenAI key stored in Portkey
+AI_MODEL=gpt-4o
+```
+
+Using a Config for fallback routing:
+```bash
+AI_PROVIDER=portkey
+PORTKEY_API_KEY=pk-...
+PORTKEY_CONFIG=pc-fallback-config-xyz # applies your saved routing rules
+AI_MODEL=gpt-4o
+```
+
+#### LiteLLM (aggregator proxy)
+
+[LiteLLM](https://litellm.ai) provides a unified `provider/model` interface for 100+ LLMs including OpenAI, Anthropic, Gemini, Cohere, Ollama, and many more.
+
+| **Variable** | **Description** | **Default** |
+|--------------------|-------------------------------------------------|-------------------------------|
+| `OPENAI_API_KEY` | API key forwarded to LiteLLM (provider-specific). | *(depends on model)* |
+| `OPENAI_BASE_URL` | Optional proxy/gateway URL. | `https://api.openai.com/v1` |
+
+```bash
+AI_PROVIDER=litellm
+AI_MODEL=anthropic/claude-3-5-sonnet-20241022
+OPENAI_API_KEY=sk-ant-... # passed as the api_key to LiteLLM
+```
+
+---
+
+### Azure Document Intelligence
| **Variable** | **Description** | **How to Obtain** |
|---------------------------------|------------------------------------------|--------------------------------------------------------------------------|
-| `OPENAI_API_KEY` | OpenAI API key for GPT metadata extraction. | [OpenAI API keys](https://platform.openai.com/account/api-keys) |
| `AZURE_DOCUMENT_INTELLIGENCE_KEY` | Azure Document Intelligence API key for OCR. | [Azure Portal](https://portal.azure.com/) |
| `AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT` | Endpoint URL for Azure Doc Intelligence API. | [Azure Portal](https://portal.azure.com/) |
diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md
index 6f7a88db..fb152e9c 100644
--- a/docs/DeploymentGuide.md
+++ b/docs/DeploymentGuide.md
@@ -6,7 +6,7 @@ This guide provides instructions for deploying DocuElevate in various environmen
- Docker and Docker Compose
- Access to required external services (if configured):
- - OpenAI API
+ - AI provider API key (OpenAI, Anthropic, Gemini, or other configured provider)
- Azure Document Intelligence
- Dropbox API
- Nextcloud instance
diff --git a/docs/SettingsManagement.md b/docs/SettingsManagement.md
index d710c8d8..2df0c529 100644
--- a/docs/SettingsManagement.md
+++ b/docs/SettingsManagement.md
@@ -26,7 +26,7 @@ Settings are organized into logical categories for easy navigation:
- **Core**: Database, Redis, working directory, external hostname, debug mode
- **Authentication**: Login settings, session secrets, OAuth configuration
-- **AI Services**: OpenAI and Azure AI configuration
+- **AI Services**: AI provider selection and credentials (OpenAI, Azure, Anthropic, Gemini, Ollama, OpenRouter, Portkey, LiteLLM)
- **Storage Providers**: Dropbox, Google Drive, OneDrive, S3, FTP, SFTP, WebDAV, Nextcloud, Paperless
- **Email**: SMTP configuration for sending emails
- **IMAP**: Email ingestion configuration (supports multiple accounts)
@@ -152,7 +152,8 @@ Removes a setting from the database (reverts to environment variable or default)
POST /api/settings/bulk-update
[
{"key": "debug", "value": "true"},
- {"key": "openai_model", "value": "gpt-4"}
+ {"key": "ai_provider", "value": "anthropic"},
+ {"key": "openai_model", "value": "claude-3-5-sonnet-20241022"}
]
```
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index 64d2c241..67bd5674 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -150,7 +150,7 @@ View the complete processing history with a timeline showing:
- Timestamps for each operation
**Retry Processing**: If a file's processing has failed, you can use the "Retry Processing" button to reprocess the entire file. This is useful when:
-- External API services (like OpenAI) had temporary issues
+- External API services (like the configured AI provider) had temporary issues
- Network connectivity was lost during processing
- Configuration has been updated and you want to reprocess with new settings
diff --git a/frontend/templates/about.html b/frontend/templates/about.html
index 2a1c043b..dff869bf 100644
--- a/frontend/templates/about.html
+++ b/frontend/templates/about.html
@@ -18,7 +18,8 @@
for everyone, whether you're a small startup or a large enterprise.
- We harness the power of OpenAI for metadata extraction and text refinement, integrate seamlessly
+ We harness the power of pluggable AI providers (OpenAI, Anthropic Claude, Google Gemini, Ollama,
+ OpenRouter, Portkey, and more) for metadata extraction and text refinement, integrate seamlessly
with Dropbox, Nextcloud, and Paperless NGX for storage and indexing, leverage Azure Document
Intelligence for OCR, and even use Gotenberg for file-to-PDF conversions.
@@ -33,7 +34,7 @@
Simple and secure file uploads with drag & drop support
OCR powered by Azure Document Intelligence
-
Automated metadata extraction using OpenAI
+
Automated metadata extraction using a pluggable AI provider
PDF conversion for various file formats via Gotenberg
Intelligent document classification and date extraction