diff --git a/README.md b/README.md index 93f85a6..30a8be4 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ docker run --rm sethblack/python-seo-analyzer http://example.com/ --sitemap /pat # Replace "your_api_key_here" with your actual Anthropic API key docker run --rm -e ANTHROPIC_API_KEY="your_api_key_here" sethblack/python-seo-analyzer http://example.com/ --run-llm-analysis +# Run with AI analysis through OrcaRouter (requires ORCAROUTER_API_KEY) +# Replace "your_api_key_here" with your actual OrcaRouter API key +docker run --rm -e ORCAROUTER_API_KEY="your_api_key_here" sethblack/python-seo-analyzer http://example.com/ --run-llm-analysis + # Save HTML output to your local machine # This mounts the current directory (.) into /app/output inside the container. # The output file 'results.html' will be saved in your current directory. @@ -84,6 +88,9 @@ docker run --rm my-seo-analyzer http://example.com/ # Run with AI analysis using the locally built image docker run --rm -e ANTHROPIC_API_KEY="your_api_key_here" my-seo-analyzer http://example.com/ --run-llm-analysis +# Run with AI analysis through OrcaRouter using the locally built image +docker run --rm -e ORCAROUTER_API_KEY="your_api_key_here" my-seo-analyzer http://example.com/ --run-llm-analysis + # Run with HTML output saved locally using the built image docker run --rm -v "$(pwd):/app/output" my-seo-analyzer /bin/sh -c "python-seo-analyzer http://example.com/ --output-format html > /app/output/results.html" # Adjust Windows commands as needed (see volume mounting example above) @@ -153,6 +160,8 @@ AI Optimization The first pass of AI optimization features use Anthropic's `claude-3-sonnet-20240229` model to evaluate the content of the site. You will need to have an API key from [Anthropic](https://www.anthropic.com/) to use this feature. The API key needs to be set as the environment variable `ANTHROPIC_API_KEY`. I recommend using a `.env` file to set this variable. Once the API key is set, the AI optimization features can be enabled with the `--run-llm-analysis` flag. +You can also run the AI optimization pass through [OrcaRouter](https://www.orcarouter.ai), an OpenAI- and Anthropic-compatible AI gateway. Set the environment variable `ORCAROUTER_API_KEY` and OrcaRouter is used instead of Anthropic, routing the same analysis through its Anthropic-compatible endpoint. OrcaRouter exposes many models under a `vendor/model` namespace (e.g. `anthropic/claude-sonnet-4.6`, `openai/gpt-4o`); the default model is `anthropic/claude-sonnet-4.6`, and you can pick another one with the `ORCAROUTER_MODEL` environment variable. + Notes ----- diff --git a/pyseoanalyzer/llm_analyst.py b/pyseoanalyzer/llm_analyst.py index bc189b0..0c22d34 100644 --- a/pyseoanalyzer/llm_analyst.py +++ b/pyseoanalyzer/llm_analyst.py @@ -56,15 +56,37 @@ class SEORecommendations(BaseModel): ) +# OrcaRouter exposes an Anthropic-compatible endpoint (https://api.orcarouter.ai) +# in front of many model providers using a "vendor/model" namespace. Setting +# ORCAROUTER_API_KEY routes the AI optimization pass through it, otherwise the +# original Anthropic path is used. +ORCAROUTER_API_URL = "https://api.orcarouter.ai" +ORCAROUTER_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6" + + class LLMSEOEnhancer: def __init__(self): - self.llm = ChatAnthropic( - model="claude-3-sonnet-20240229", - anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY"), - temperature=0, - timeout=30, - max_retries=3, - ) + orca_router_api_key = os.environ.get("ORCAROUTER_API_KEY") + + if orca_router_api_key: + self.llm = ChatAnthropic( + model=os.environ.get( + "ORCAROUTER_MODEL", ORCAROUTER_DEFAULT_MODEL + ), + anthropic_api_key=orca_router_api_key, + anthropic_api_url=ORCAROUTER_API_URL, + temperature=0, + timeout=30, + max_retries=3, + ) + else: + self.llm = ChatAnthropic( + model="claude-3-sonnet-20240229", + anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY"), + temperature=0, + timeout=30, + max_retries=3, + ) self._setup_chains() def _setup_chains(self): diff --git a/tests/test_llm_analyst.py b/tests/test_llm_analyst.py index 500f623..12c9986 100644 --- a/tests/test_llm_analyst.py +++ b/tests/test_llm_analyst.py @@ -1,9 +1,10 @@ import pytest -from pyseoanalyzer.llm_analyst import LLMSEOEnhancer from langchain_anthropic import ChatAnthropic -from langchain.chains import LLMChain -from langchain.prompts import PromptTemplate -import json +from pyseoanalyzer.llm_analyst import ( + LLMSEOEnhancer, + ORCAROUTER_API_URL, + ORCAROUTER_DEFAULT_MODEL, +) @pytest.fixture @@ -16,15 +17,42 @@ def seo_data(): } -def test_init(): +@pytest.fixture +def anthropic_key(monkeypatch): + monkeypatch.delenv("ORCAROUTER_API_KEY", raising=False) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test") + + +@pytest.fixture +def orcarouter_key(monkeypatch): + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.setenv("ORCAROUTER_API_KEY", "sk-orca-test") + + +def test_init_uses_anthropic(anthropic_key): enhancer = LLMSEOEnhancer() assert isinstance(enhancer.llm, ChatAnthropic) assert enhancer.llm.model == "claude-3-sonnet-20240229" assert enhancer.llm.temperature == 0 +def test_init_uses_orcarouter_when_configured(orcarouter_key): + enhancer = LLMSEOEnhancer() + assert isinstance(enhancer.llm, ChatAnthropic) + assert enhancer.llm.model == ORCAROUTER_DEFAULT_MODEL + assert enhancer.llm.anthropic_api_url == ORCAROUTER_API_URL + assert enhancer.llm.temperature == 0 + + +def test_init_orcarouter_honors_model_override(orcarouter_key, monkeypatch): + monkeypatch.setenv("ORCAROUTER_MODEL", "openai/gpt-4o") + enhancer = LLMSEOEnhancer() + assert enhancer.llm.model == "openai/gpt-4o" + assert enhancer.llm.anthropic_api_url == ORCAROUTER_API_URL + + @pytest.mark.asyncio -async def test_enhance_seo_analysis(seo_data): +async def test_enhance_seo_analysis(anthropic_key, seo_data): enhancer = LLMSEOEnhancer() result = await enhancer.enhance_seo_analysis(seo_data)