diff --git a/.env.example b/.env.example index d68d1ac..5ccd61a 100644 --- a/.env.example +++ b/.env.example @@ -27,5 +27,13 @@ DATABASE_URL=postgresql://smriti:smriti@localhost:5432/smriti # Default model for background intelligence (extraction, draft, review) OPENAI_MODEL=gpt-4o-mini +# Generic OpenAI-compatible provider (Ollama, LM Studio, vLLM, Together, etc.) +# Set the API URL to use a local or cheap model instead of OpenAI/Anthropic. +# API key is optional — local servers like Ollama don't require one. +# Then set background_intelligence.provider to "generic" in providers.yaml. +# SMRITI_GENERIC_API_URL=http://localhost:11434/v1 +# SMRITI_GENERIC_MODEL=llama3.1:8b +# SMRITI_GENERIC_API_KEY= + # App DEBUG=false diff --git a/README.md b/README.md index c197057..fdd4440 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,12 @@ cp .env.example .env # Leave keys commented out to use mock mode (no real LLM calls). # .env is the primary config path. config/providers.yaml is optional # and gitignored — you do not need it for a standard setup. +# +# Using Ollama or another local model? Set these instead: +# SMRITI_GENERIC_API_URL=http://localhost:11434/v1 +# SMRITI_GENERIC_MODEL=llama3.1:8b +# Then set background_intelligence.provider to "generic" in +# config/providers.yaml. See .env.example for details. docker compose up -d postgres # start the database make setup # backend venv + deps + migrations + CLI + frontend diff --git a/backend/app/config_loader.py b/backend/app/config_loader.py index d734729..148b830 100644 --- a/backend/app/config_loader.py +++ b/backend/app/config_loader.py @@ -65,6 +65,7 @@ class AppProviderConfig: openai: ProviderConfig = field(default_factory=ProviderConfig) anthropic: ProviderConfig = field(default_factory=ProviderConfig) openrouter: ProviderConfig = field(default_factory=ProviderConfig) + generic: ProviderConfig = field(default_factory=ProviderConfig) chat: ChatConfig = field(default_factory=ChatConfig) background: BackgroundConfig = field(default_factory=BackgroundConfig) @@ -166,6 +167,29 @@ def load_config() -> AppProviderConfig: base_url_default="https://openrouter.ai/api/v1", ) + # Generic OpenAI-compatible provider (Ollama, LM Studio, vLLM, Together, + # Groq, etc.). Enabled when base_url is set — api_key is optional because + # local servers like Ollama don't require one. + g = providers_raw.get("generic", {}) + generic_base_url = ( + os.environ.get("SMRITI_GENERIC_API_URL") + or g.get("base_url", "") + ) + generic_api_key = ( + os.environ.get("SMRITI_GENERIC_API_KEY") + or g.get("api_key", "") + ) + generic_model = ( + os.environ.get("SMRITI_GENERIC_MODEL") + or g.get("default_model", "") + ) + generic = ProviderConfig( + enabled=bool(generic_base_url), + api_key=generic_api_key or "not-required", + default_model=generic_model, + base_url=generic_base_url, + ) + chat = ChatConfig( default_provider=os.environ.get("SMRITI_DEFAULT_PROVIDER") or chat_raw.get("default_provider", "openrouter"), @@ -183,6 +207,7 @@ def load_config() -> AppProviderConfig: openai=openai, anthropic=anthropic, openrouter=openrouter, + generic=generic, chat=chat, background=background, ) @@ -248,7 +273,7 @@ def providers_status() -> dict[str, dict]: "status_label": "Ready" if getattr(cfg, name).enabled else "Disabled", "default_model": getattr(cfg, name).default_model, } - for name in ("openai", "anthropic", "openrouter") + for name in ("openai", "anthropic", "openrouter", "generic") } # Background intelligence uses one of the above providers. Report # its ACTUAL key/enabled/configured state instead of hardcoding True, diff --git a/backend/app/providers/registry.py b/backend/app/providers/registry.py index 189c1b5..ce6b64e 100644 --- a/backend/app/providers/registry.py +++ b/backend/app/providers/registry.py @@ -108,6 +108,22 @@ def get_adapter(provider: str, allow_mock: bool = False) -> ProviderAdapter: except ImportError as e: raise ProviderNotConfiguredError(str(e)) + if p == "generic": + # Generic OpenAI-compatible provider (Ollama, LM Studio, vLLM, etc.). + # Reuses OpenAIAdapter with the user-configured base_url. The api_key + # may be "not-required" for local servers — OpenAI SDK accepts any + # non-empty string. + if not cfg.base_url: + raise ProviderNotConfiguredError( + "Generic provider requires SMRITI_GENERIC_API_URL or " + "providers.yaml generic.base_url to be set." + ) + try: + from app.providers.openai_adapter import OpenAIAdapter + return OpenAIAdapter(api_key=cfg.api_key, base_url=cfg.base_url) + except ImportError as e: + raise ProviderNotConfiguredError(str(e)) + raise ProviderNotConfiguredError(f"Unknown provider: {provider}")