fix(config): don't set litellm.api_key global when LLM_API_BASE is configured (#1095)

When LLM_API_BASE is set, the model routes through openai-agents'
LitellmModel which always forwards api_key=self.api_key (None by
default) as an explicit kwarg on every litellm.acompletion() call.
Setting litellm.api_key as a module-level global on top of that caused:

  TypeError: acompletion() got multiple values for keyword argument
             'api_key'           (anthropic/ prefix path)
  TypeError: AsyncCompletions.create() got an unexpected keyword
             argument 'api_key' (openai/ prefix path)

Fix: guard _configure_litellm_default('api_key', ...) behind
'if not llm.api_base'. The key still reaches the provider via
_mirror_api_key_to_provider_env (sets the provider-specific *_API_KEY
env var e.g. ANTHROPIC_API_KEY) and set_default_openai_key, so
skipping the module-level global is safe.

Fixes #1095
This commit is contained in:
Suraj 2026-08-18 07:04:24 +05:30
parent 8ede419dcc
commit 7e52482c0b
2 changed files with 149 additions and 1 deletions

View file

@ -562,7 +562,26 @@ def configure_sdk_model_defaults(settings: Settings) -> None:
_configure_openrouter_attribution(llm.model)
if llm.api_key:
set_default_openai_key(llm.api_key, use_for_tracing=False)
_configure_litellm_default("api_key", llm.api_key)
# Do NOT set litellm.api_key as a module-level global when a custom
# api_base is configured. When api_base is active the model resolves
# through openai-agents' LitellmModel, which always forwards
# api_key=self.api_key (None by default, because Strix never populates
# it on the instance) as an *explicit* kwarg in every
# litellm.acompletion() call. litellm's internal dispatch then merges
# the module-level global into the same kwargs dict that already
# carries the explicit kwarg, producing:
#
# TypeError: acompletion() got multiple values for keyword argument
# 'api_key' (anthropic/ prefix path)
# TypeError: AsyncCompletions.create() got an unexpected keyword
# argument 'api_key' (openai/ prefix path)
#
# The key is still delivered to the provider via
# _mirror_api_key_to_provider_env (sets the provider-specific
# *_API_KEY env var, e.g. ANTHROPIC_API_KEY) and set_default_openai_key
# above, so skipping the global default here is safe.
if not llm.api_base:
_configure_litellm_default("api_key", llm.api_key)
_mirror_api_key_to_provider_env(llm.model, llm.api_key)
if llm.api_base:
os.environ["OPENAI_BASE_URL"] = llm.api_base

View file

@ -0,0 +1,129 @@
"""Regression tests for issue #1095.
LLM CONNECTION FAILED duplicate api_key keyword argument when LLM_API_BASE
is set to a custom endpoint.
When LLM_API_BASE is configured the model resolves through openai-agents'
LitellmModel, which always forwards api_key=self.api_key (None by default) as
an *explicit* kwarg on every litellm.acompletion() call. If litellm.api_key
is also set as a module-level global by configure_sdk_model_defaults, litellm's
internal dispatch merges the global into the same kwargs dict that already
carries the explicit kwarg and raises:
TypeError: acompletion() got multiple values for keyword argument 'api_key'
(anthropic/ / litellm/ prefix path)
TypeError: AsyncCompletions.create() got an unexpected keyword argument
'api_key' (openai/ prefix path)
The fix: skip _configure_litellm_default("api_key", ...) when api_base is set.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import litellm
import pytest
from strix.config import loader
from strix.config.loader import load_settings
from strix.config.models import configure_sdk_model_defaults
if TYPE_CHECKING:
from collections.abc import Iterator
_ENV_KEYS = [
# Primary Strix settings
"STRIX_LLM",
"LLM_API_KEY",
"LLM_API_BASE",
# All aliases that LlmSettings maps to api_base (settings.py AliasChoices),
# so configure_sdk_model_defaults()'s os.environ["OPENAI_BASE_URL"] write
# in one test does not bleed into the next test's load_settings() call.
"OPENAI_API_BASE",
"OPENAI_BASE_URL",
"LITELLM_BASE_URL",
"OLLAMA_API_BASE",
]
@pytest.fixture(autouse=True)
def _reset(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Isolate litellm.api_key and the settings cache between tests."""
for key in _ENV_KEYS:
monkeypatch.delenv(key, raising=False)
monkeypatch.setattr(loader, "_cached", None)
monkeypatch.setattr(loader, "_override", None)
saved_api_key = litellm.api_key
litellm.api_key = None
try:
yield
finally:
litellm.api_key = saved_api_key
# ---------------------------------------------------------------------------
# Core regression: litellm.api_key must NOT be set when api_base is active
# ---------------------------------------------------------------------------
def test_litellm_api_key_global_not_set_when_api_base_configured(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Issue #1095 (anthropic/ path): litellm.api_key must remain None when
LLM_API_BASE is set, so that LitellmModel's explicit api_key=None kwarg
does not collide with a module-level global."""
monkeypatch.setenv("STRIX_LLM", "anthropic/deepseek-main")
monkeypatch.setenv("LLM_API_KEY", "placeholder")
monkeypatch.setenv("LLM_API_BASE", "http://192.168.1.18:8081")
configure_sdk_model_defaults(load_settings())
# The module-level litellm.api_key must NOT be set; the key reaches the
# provider through provider-specific env vars (_mirror_api_key_to_provider_env).
assert litellm.api_key is None, (
"litellm.api_key was set as a module-level global even though "
"LLM_API_BASE is configured. This causes "
"'acompletion() got multiple values for keyword argument api_key' "
"(issue #1095)."
)
def test_litellm_api_key_global_not_set_for_openai_prefix_with_custom_base(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Issue #1095 (openai/ path): same guard applies regardless of the
STRIX_LLM prefix used."""
monkeypatch.setenv("STRIX_LLM", "openai/deepseek-main")
monkeypatch.setenv("LLM_API_KEY", "placeholder")
monkeypatch.setenv("LLM_API_BASE", "http://192.168.1.18:8081/v1")
configure_sdk_model_defaults(load_settings())
assert litellm.api_key is None, (
"litellm.api_key was set as a module-level global even though "
"LLM_API_BASE is configured (openai/ prefix path). This causes "
"'AsyncCompletions.create() got an unexpected keyword argument api_key'"
" (issue #1095)."
)
def test_litellm_api_key_global_set_when_no_api_base(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Positive case: litellm.api_key IS set when no custom api_base is given
(standard direct-to-provider path)."""
monkeypatch.setenv("STRIX_LLM", "openai/gpt-5.4")
monkeypatch.setenv("LLM_API_KEY", "sk-real-key")
# LLM_API_BASE is intentionally absent
configure_sdk_model_defaults(load_settings())
assert litellm.api_key == "sk-real-key", (
"litellm.api_key should be set globally when no custom api_base is "
"configured (standard direct-to-provider path)."
)