diff --git a/bookstack_agent/api/main.py b/bookstack_agent/api/main.py index df8bb9b..52d2d65 100644 --- a/bookstack_agent/api/main.py +++ b/bookstack_agent/api/main.py @@ -61,9 +61,11 @@ @asynccontextmanager async def lifespan(application: FastAPI) -> AsyncGenerator[None, None]: """Create the BookstackQAAgent and session store at startup.""" + llm_base_url = os.environ.get("LLM_BASE_URL") + llm_key_var = "LLM_API_KEY" if llm_base_url else "ANTHROPIC_API_KEY" missing = [ v - for v in ("ANTHROPIC_API_KEY", "BOOKSTACK_TOKEN_ID", "BOOKSTACK_TOKEN_SECRET") + for v in (llm_key_var, "BOOKSTACK_TOKEN_ID", "BOOKSTACK_TOKEN_SECRET") if not os.environ.get(v) ] if missing: @@ -77,7 +79,7 @@ async def lifespan(application: FastAPI) -> AsyncGenerator[None, None]: ), token_id=os.environ["BOOKSTACK_TOKEN_ID"], token_secret=os.environ["BOOKSTACK_TOKEN_SECRET"], - llm_base_url=os.environ.get("LLM_BASE_URL"), + llm_base_url=llm_base_url, ) # OrderedDict preserves insertion order for LRU-style eviction diff --git a/src/aieng_bot/bookstack/agent.py b/src/aieng_bot/bookstack/agent.py index f3b54a8..898b255 100644 --- a/src/aieng_bot/bookstack/agent.py +++ b/src/aieng_bot/bookstack/agent.py @@ -53,6 +53,11 @@ class BookstackQAAgent: ``https://proxy.vectorinstitute.ai``) instead of the Anthropic API directly. Defaults to ``LLM_BASE_URL`` env var, or the Anthropic API if unset. + llm_api_key : str, optional + Bearer token for the LLM gateway. Required when ``llm_base_url`` is + set; ignored otherwise. The gateway expects ``Authorization: Bearer`` + rather than the Anthropic ``x-api-key`` header. Defaults to + ``LLM_API_KEY`` env var. """ @@ -66,20 +71,37 @@ def __init__( api_key: str | None = None, model: str | None = None, llm_base_url: str | None = None, + llm_api_key: str | None = None, ) -> None: """Initialise the agent.""" - resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY") - if not resolved_key: - raise ValueError("ANTHROPIC_API_KEY environment variable not set") - resolved_llm_base_url = llm_base_url or os.environ.get("LLM_BASE_URL") - self._sync_client = anthropic.Anthropic( - api_key=resolved_key, base_url=resolved_llm_base_url - ) - self._async_client = anthropic.AsyncAnthropic( - api_key=resolved_key, base_url=resolved_llm_base_url - ) + if resolved_llm_base_url: + # Gateway mode: the gateway requires Authorization: Bearer, not x-api-key. + # LLM_API_KEY is the gateway-issued bearer token; ANTHROPIC_API_KEY is not used. + resolved_llm_api_key = llm_api_key or os.environ.get("LLM_API_KEY") + if not resolved_llm_api_key: + raise ValueError( + "LLM_API_KEY must be set when LLM_BASE_URL is configured" + ) + self._sync_client = anthropic.Anthropic( + api_key=resolved_llm_api_key, + base_url=resolved_llm_base_url, + default_headers={"Authorization": f"Bearer {resolved_llm_api_key}"}, + ) + self._async_client = anthropic.AsyncAnthropic( + api_key=resolved_llm_api_key, + base_url=resolved_llm_base_url, + default_headers={"Authorization": f"Bearer {resolved_llm_api_key}"}, + ) + else: + # Direct Anthropic mode: standard x-api-key authentication. + resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY") + if not resolved_key: + raise ValueError("ANTHROPIC_API_KEY environment variable not set") + self._sync_client = anthropic.Anthropic(api_key=resolved_key) + self._async_client = anthropic.AsyncAnthropic(api_key=resolved_key) + self.bookstack = BookStackClient(base_url, token_id, token_secret) self.model = model or get_model_name()