fix(llm): thread LLM_API_KEY into the SDK's native OpenAIProvider

``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/<model>`` + ``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) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-26 07:26:48 -07:00
parent ead54ba82c
commit 0518599f29
+45 -11
View File
@@ -1,11 +1,26 @@
"""Multi-provider routing setup.
Wraps the SDK's :class:`MultiProvider` and registers a custom Anthropic
route so models named ``anthropic/<model>`` 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/<model>`` → :class:`AnthropicCachingLitellmModel` so
prompt caching kicks in (we inject ``cache_control`` on the system
message before the litellm call).
- ``openai/<model>`` (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/<model>`` 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,
)