From 0518599f2964d84e96483fa379925187149281aa Mon Sep 17 00:00:00 2001 From: 0xallam Date: Sun, 26 Apr 2026 07:26:48 -0700 Subject: [PATCH] fix(llm): thread LLM_API_KEY into the SDK's native OpenAIProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``MultiProvider`` was constructed with no openai kwargs, so the inner ``OpenAIProvider`` defaulted to reading ``OPENAI_API_KEY`` from the environment. Strix's contract is that ``LLM_API_KEY`` works for every provider, so users with ``STRIX_LLM=openai/`` + ``LLM_API_KEY`` hit ``openai.OpenAIError`` at the first turn — the warm-up call worked because that path goes through ``litellm.completion`` directly with explicit creds, but the actual scan went through the SDK's MultiProvider where the key was never plumbed. Pass ``Settings.llm.api_key`` and ``Settings.llm.api_base`` through to the underlying ``OpenAIProvider`` via the ``openai_api_key`` / ``openai_base_url`` ctor kwargs. ``openai_use_responses`` flips to ``False`` when ``LLM_API_BASE`` is set — non-default base URLs are the reliable signal that the user is on an OpenAI-compatible endpoint that doesn't speak the Responses API. Genuine OpenAI usage keeps the Responses API as the default transport. The ``anthropic/`` prefix continues to route through ``AnthropicCachingLitellmModel`` for prompt caching; ``litellm/`` and other prefixes still fall through to the SDK's stock routing. Co-Authored-By: Claude Opus 4.7 (1M context) --- strix/llm/multi_provider_setup.py | 56 +++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/strix/llm/multi_provider_setup.py b/strix/llm/multi_provider_setup.py index 1e2e096..b8c472f 100644 --- a/strix/llm/multi_provider_setup.py +++ b/strix/llm/multi_provider_setup.py @@ -1,11 +1,26 @@ """Multi-provider routing setup. -Wraps the SDK's :class:`MultiProvider` and registers a custom Anthropic -route so models named ``anthropic/`` go through -:class:`AnthropicCachingLitellmModel` (which injects ``cache_control`` -on the system message). Every other prefix -(``openai/`` / ``gemini/`` / ``openrouter/`` / ``litellm/...``) falls -through to the SDK's built-in litellm routing. +Wraps the SDK's :class:`MultiProvider` and threads Strix's +``LLM_API_KEY`` / ``LLM_API_BASE`` into the underlying provider chain. + +Routing: + +- ``anthropic/`` → :class:`AnthropicCachingLitellmModel` so + prompt caching kicks in (we inject ``cache_control`` on the system + message before the litellm call). +- ``openai/`` (and bare model names) → SDK-native + :class:`OpenAIProvider`, instantiated with our settings credentials so + ``LLM_API_KEY`` works without forcing the user to also export + ``OPENAI_API_KEY``. Keeps the Responses API as the default transport + for genuine OpenAI usage. +- Every other prefix (``litellm/...``, ``any-llm/...``, …) falls through + to whatever the SDK does natively. + +Real-OpenAI vs OpenAI-compatible differentiation is by +``Settings.llm.api_base`` presence. If the user pointed at a non-default +base URL they're almost certainly on an OpenAI-compatible endpoint that +doesn't speak the Responses API, so we flip ``openai_use_responses=False`` +to make the inner provider use chat-completions transport instead. """ from __future__ import annotations @@ -16,6 +31,7 @@ from agents.exceptions import UserError from agents.models.interface import Model, ModelProvider from agents.models.multi_provider import MultiProvider, MultiProviderMap +from strix.config import load_settings from strix.llm.anthropic_cache_wrapper import AnthropicCachingLitellmModel @@ -45,11 +61,29 @@ class _AnthropicCachingProvider(ModelProvider): def build_multi_provider() -> MultiProvider: """Build the configured MultiProvider. - Registers the ``anthropic/`` route through our caching wrapper so - prompt caching kicks in; everything else falls through to the SDK's - built-in routing. + Registers the ``anthropic/`` route through our caching wrapper and + threads ``Settings.llm`` credentials into the SDK-native + :class:`OpenAIProvider` so ``openai/`` works with our single + ``LLM_API_KEY`` env var. ``Settings.llm.api_base`` (when set) flips + the OpenAI provider to chat-completions transport — the de-facto + signal that the user is hitting an OpenAI-compatible endpoint that + doesn't implement the Responses API. """ pmap = MultiProviderMap() # type: ignore[no-untyped-call] pmap.add_provider("anthropic", _AnthropicCachingProvider()) - logger.debug("MultiProvider built with anthropic/ caching route") - return MultiProvider(provider_map=pmap) + + llm = load_settings().llm + use_responses = llm.api_base is None # default endpoint → real OpenAI + logger.debug( + "MultiProvider built with anthropic/ cached + openai/ native " + "(api_key=%s, base_url=%s, use_responses=%s)", + "set" if llm.api_key else "unset", + llm.api_base or "default", + use_responses, + ) + return MultiProvider( + provider_map=pmap, + openai_api_key=llm.api_key, + openai_base_url=llm.api_base, + openai_use_responses=use_responses, + )