Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
openai_key: Optional[str] = None
groq_key: Optional[str] = None
gigachat_key: Optional[str] = None
xai_key: Optional[str] = None
databricks_key: Optional[str] = None
openai_like_key: Optional[str] = None
azure_key: Optional[str] = None
Expand Down
3 changes: 2 additions & 1 deletion litellm/llms/xai/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import litellm
from litellm._logging import verbose_logger
from litellm.llms.xai.common_utils import XAIModelInfo
from litellm.litellm_core_utils.prompt_templates.common_utils import (
filter_value_from_dict,
strip_name_from_messages,
Expand All @@ -26,7 +27,7 @@ def _get_openai_compatible_provider_info(
self, api_base: Optional[str], api_key: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:
api_base = api_base or get_secret_str("XAI_API_BASE") or XAI_API_BASE # type: ignore
dynamic_api_key = api_key or get_secret_str("XAI_API_KEY")
dynamic_api_key = XAIModelInfo.get_api_key(api_key)
return api_base, dynamic_api_key

def get_supported_openai_params(self, model: str) -> list:
Expand Down
7 changes: 6 additions & 1 deletion litellm/llms/xai/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ def get_api_base(api_base: Optional[str] = None) -> Optional[str]:

@staticmethod
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
return api_key or get_secret_str("XAI_API_KEY")
return (
api_key
or litellm.xai_key
or get_secret_str("XAI_API_KEY")
or litellm.api_key
)

@staticmethod
def get_base_model(model: str) -> Optional[str]:
Expand Down
8 changes: 2 additions & 6 deletions litellm/llms/xai/responses/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import litellm
from litellm._logging import verbose_logger
from litellm.llms.xai.common_utils import XAIModelInfo
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
Expand Down Expand Up @@ -103,11 +104,7 @@ def validate_environment(
Uses XAI_API_KEY from environment or litellm_params.
"""
litellm_params = litellm_params or GenericLiteLLMParams()
api_key = (
litellm_params.api_key
or litellm.api_key
or get_secret_str("XAI_API_KEY")
)
api_key = XAIModelInfo.get_api_key(litellm_params.api_key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Subtle priority reversal in Responses API key resolution

The old code checked litellm.api_key before XAI_API_KEY, whereas XAIModelInfo.get_api_key now checks XAI_API_KEY first and litellm.api_key last. Any Responses API user who had both litellm.api_key and XAI_API_KEY set would silently switch from using litellm.api_key to using XAI_API_KEY after this change. The new ordering is arguably more correct (provider-specific env var should beat the generic fallback), but it is a backwards-incompatible behaviour change for that configuration.

Rule Used: What: avoid backwards-incompatible changes without... (source)


if not api_key:
raise ValueError(
Expand Down Expand Up @@ -143,4 +140,3 @@ def get_complete_url(
api_base = api_base.rstrip("/")

return f"{api_base}/responses"

100 changes: 100 additions & 0 deletions tests/litellm/llms/xai/test_xai_key_fallback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import os
import litellm
from litellm.llms.xai.chat.transformation import XAIChatConfig
from litellm.llms.xai.common_utils import XAIModelInfo
from litellm.llms.xai.responses.transformation import XAIResponsesAPIConfig


def test_get_api_key_priority():
"""
Test the fallback order of XAI API key resolution:
1. api_key parameter
2. litellm.xai_key
3. XAI_API_KEY environment variable
4. litellm.api_key
5. None
"""

original_env = os.environ.get("XAI_API_KEY")
had_env = "XAI_API_KEY" in os.environ
original_xai_key = litellm.xai_key
original_api_key = litellm.api_key

try:
# Case 1: api_key parameter is passed
litellm.xai_key = "xai_key_value"
litellm.api_key = "common_api_key"
os.environ["XAI_API_KEY"] = "env_api_key"
result = XAIModelInfo.get_api_key("param_api_key")
assert result == "param_api_key"

# Case 2: api_key not passed, use litellm.xai_key
result = XAIModelInfo.get_api_key(None)
assert result == "xai_key_value"

# Case 3: api_key and xai_key not set, prefer XAI_API_KEY over litellm.api_key
litellm.xai_key = None
result = XAIModelInfo.get_api_key(None)
assert result == "env_api_key"

# Case 4: Empty XAI_API_KEY falls through to litellm.api_key
os.environ["XAI_API_KEY"] = ""
result = XAIModelInfo.get_api_key(None)
assert result == "common_api_key"

# Case 5: None of the above, return None
os.environ.pop("XAI_API_KEY", None)
litellm.api_key = None
result = XAIModelInfo.get_api_key(None)
assert result is None
finally:
if had_env:
os.environ["XAI_API_KEY"] = original_env
else:
os.environ.pop("XAI_API_KEY", None)
litellm.xai_key = original_xai_key
litellm.api_key = original_api_key


def test_chat_config_uses_xai_key_fallback():
original_env = os.environ.get("XAI_API_KEY")
had_env = "XAI_API_KEY" in os.environ
original_xai_key = litellm.xai_key
original_api_key = litellm.api_key

try:
litellm.xai_key = "xai_key_value"
litellm.api_key = None
os.environ.pop("XAI_API_KEY", None)
_, api_key = XAIChatConfig()._get_openai_compatible_provider_info(None, None)
assert api_key == "xai_key_value"
finally:
if had_env:
os.environ["XAI_API_KEY"] = original_env
else:
os.environ.pop("XAI_API_KEY", None)
litellm.xai_key = original_xai_key
litellm.api_key = original_api_key


def test_responses_config_uses_xai_key_fallback():
original_env = os.environ.get("XAI_API_KEY")
had_env = "XAI_API_KEY" in os.environ
original_xai_key = litellm.xai_key
original_api_key = litellm.api_key

try:
litellm.xai_key = "xai_key_value"
litellm.api_key = None
os.environ.pop("XAI_API_KEY", None)
headers = XAIResponsesAPIConfig().validate_environment(
{}, "xai/grok-3-mini", None
)
assert headers["Authorization"] == "Bearer xai_key_value"
finally:
if had_env:
os.environ["XAI_API_KEY"] = original_env
else:
os.environ.pop("XAI_API_KEY", None)
litellm.xai_key = original_xai_key
litellm.api_key = original_api_key
Loading