From ac6d5c6daeef4b2ec59e03b87d8ef63556ec503e Mon Sep 17 00:00:00 2001 From: Stanislav Luchanskiy Date: Wed, 24 Sep 2025 22:32:58 +0300 Subject: [PATCH] feat(llm): support remote API base (Ollama/LM Studio/LiteLLM) + docs (#24) Co-authored-by: Ahmed Allam Co-authored-by: Ahmed Allam <49919286+0xallam@users.noreply.github.com> --- README.md | 6 +-- pyproject.toml | 2 +- strix/cli/main.py | 102 +++++++++++++++++++++++++++++++++++----------- strix/llm/llm.py | 9 ++++ 4 files changed, 92 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index eea72d3..a3a91de 100644 --- a/README.md +++ b/README.md @@ -89,12 +89,12 @@ strix --target api.your-app.com --instruction "Prioritize authentication and aut ### ⚙️ Configuration ```bash -# Required export STRIX_LLM="openai/gpt-5" export LLM_API_KEY="your-api-key" -# Recommended -export PERPLEXITY_API_KEY="your-api-key" +# Optional +export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio +export PERPLEXITY_API_KEY="your-api-key" # for search capabilities ``` [📚 View supported AI models](https://docs.litellm.ai/docs/providers) diff --git a/pyproject.toml b/pyproject.toml index 8d755be..55dd1c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "strix-agent" -version = "0.1.17" +version = "0.1.18" description = "Open-source AI Hackers for your apps" authors = ["Strix "] readme = "README.md" diff --git a/strix/cli/main.py b/strix/cli/main.py index 7a10821..88be839 100644 --- a/strix/cli/main.py +++ b/strix/cli/main.py @@ -48,8 +48,23 @@ def validate_environment() -> None: if not os.getenv("STRIX_LLM"): missing_required_vars.append("STRIX_LLM") + has_base_url = any( + [ + os.getenv("LLM_API_BASE"), + os.getenv("OPENAI_API_BASE"), + os.getenv("LITELLM_BASE_URL"), + os.getenv("OLLAMA_API_BASE"), + ] + ) + if not os.getenv("LLM_API_KEY"): - missing_required_vars.append("LLM_API_KEY") + if not has_base_url: + missing_required_vars.append("LLM_API_KEY") + else: + missing_optional_vars.append("LLM_API_KEY") + + if not has_base_url: + missing_optional_vars.append("LLM_API_BASE") if not os.getenv("PERPLEXITY_API_KEY"): missing_optional_vars.append("PERPLEXITY_API_KEY") @@ -65,40 +80,72 @@ def validate_environment() -> None: error_text.append(" is not set\n", style="white") if missing_optional_vars: - error_text.append( - "\nOptional (but recommended) environment variables:\n", style="dim white" - ) + error_text.append("\nOptional environment variables:\n", style="dim white") for var in missing_optional_vars: error_text.append(f"• {var}", style="dim yellow") error_text.append(" is not set\n", style="dim white") error_text.append("\nRequired environment variables:\n", style="white") - error_text.append("• ", style="white") - error_text.append("STRIX_LLM", style="bold cyan") - error_text.append( - " - Model name to use with litellm (e.g., 'openai/gpt-5')\n", - style="white", - ) - error_text.append("• ", style="white") - error_text.append("LLM_API_KEY", style="bold cyan") - error_text.append(" - API key for the LLM provider\n", style="white") + for var in missing_required_vars: + if var == "STRIX_LLM": + error_text.append("• ", style="white") + error_text.append("STRIX_LLM", style="bold cyan") + error_text.append( + " - Model name to use with litellm (e.g., 'openai/gpt-5')\n", + style="white", + ) + elif var == "LLM_API_KEY": + error_text.append("• ", style="white") + error_text.append("LLM_API_KEY", style="bold cyan") + error_text.append( + " - API key for the LLM provider (required for cloud providers)\n", + style="white", + ) if missing_optional_vars: error_text.append("\nOptional environment variables:\n", style="white") - error_text.append("• ", style="white") - error_text.append("PERPLEXITY_API_KEY", style="bold cyan") - error_text.append( - " - API key for Perplexity AI web search (enables real-time research)\n", - style="white", - ) + for var in missing_optional_vars: + if var == "LLM_API_KEY": + error_text.append("• ", style="white") + error_text.append("LLM_API_KEY", style="bold cyan") + error_text.append(" - API key for the LLM provider\n", style="white") + elif var == "LLM_API_BASE": + error_text.append("• ", style="white") + error_text.append("LLM_API_BASE", style="bold cyan") + error_text.append( + " - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n", + style="white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append("• ", style="white") + error_text.append("PERPLEXITY_API_KEY", style="bold cyan") + error_text.append( + " - API key for Perplexity AI web search (enables real-time research)\n", + style="white", + ) error_text.append("\nExample setup:\n", style="white") error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white") - error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white") + + if "LLM_API_KEY" in missing_required_vars: + error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white") + if missing_optional_vars: - error_text.append( - "export PERPLEXITY_API_KEY='your-perplexity-key-here'", style="dim white" - ) + for var in missing_optional_vars: + if var == "LLM_API_KEY": + error_text.append( + "export LLM_API_KEY='your-api-key-here' # optional with local models\n", + style="dim white", + ) + elif var == "LLM_API_BASE": + error_text.append( + "export LLM_API_BASE='http://localhost:11434' # needed for local models only\n", + style="dim white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append( + "export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white" + ) panel = Panel( error_text, @@ -152,6 +199,15 @@ async def warm_up_llm() -> None: if api_key: litellm.api_key = api_key + api_base = ( + os.getenv("LLM_API_BASE") + or os.getenv("OPENAI_API_BASE") + or os.getenv("LITELLM_BASE_URL") + or os.getenv("OLLAMA_API_BASE") + ) + if api_base: + litellm.api_base = api_base + test_messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Reply with just 'OK'."}, diff --git a/strix/llm/llm.py b/strix/llm/llm.py index f24c509..aed2b4a 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -28,6 +28,15 @@ api_key = os.getenv("LLM_API_KEY") if api_key: litellm.api_key = api_key +api_base = ( + os.getenv("LLM_API_BASE") + or os.getenv("OPENAI_API_BASE") + or os.getenv("LITELLM_BASE_URL") + or os.getenv("OLLAMA_API_BASE") +) +if api_base: + litellm.api_base = api_base + class LLMRequestFailedError(Exception): def __init__(self, message: str, details: str | None = None):