Fix - handle empty config.yaml + Fix gemini /models - replace models/ as expected, instead of using 'strip' (#12189)

* fix(proxy_server.py): handle empty config yaml

Fixes https://github.com/BerriAI/litellm/issues/12163

* fix(gemini/common_utils.py): replace models/ as expected, instead of using 'strip'

Fixes https://github.com/BerriAI/litellm/issues/12160

* fix(anthropic/experimental_pass_through/messages/transformation.py): check for env var when selecting api key

* docs(config_settings.md): add api key to docs
This commit is contained in:
Krish Dholakia 2025-06-30 21:56:03 -07:00 • committed by GitHub
parent 5188c67c6c
commit ee9dd158dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 205 additions and 28 deletions

View file

@ -319,6 +319,7 @@ router_settings:
| ATHINA_API_KEY | API key for Athina service
| ATHINA_BASE_URL | Base URL for Athina service (defaults to `https://log.athina.ai`)
| AUTH_STRATEGY | Strategy used for authentication (e.g., OAuth, API key)
| ANTHROPIC_API_KEY | API key for Anthropic service
| AWS_ACCESS_KEY_ID | Access Key ID for AWS services
| AWS_PROFILE_NAME | AWS CLI profile name to be used
| AWS_REGION_NAME | Default AWS region for service interactions

View file

@ -60,12 +60,17 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> Tuple[dict, Optional[str]]:
import os
if api_key is None:
api_key = os.getenv("ANTHROPIC_API_KEY")
if "x-api-key" not in headers and api_key:
headers["x-api-key"] = api_key
if "anthropic-version" not in headers:
headers["anthropic-version"] = DEFAULT_ANTHROPIC_API_VERSION
if "content-type" not in headers:
headers["content-type"] = "application/json"
return headers, api_base
def transform_anthropic_messages_request(

View file

@ -50,6 +50,14 @@ class GeminiModelInfo(BaseLLMModelInfo):
def get_base_model(model: str) -> Optional[str]:
return model.replace("gemini/", "")
def process_model_name(self, models: List[Dict[str, str]]) -> List[str]:
litellm_model_names = []
for model in models:
stripped_model_name = model["name"].replace("models/", "")
litellm_model_name = "gemini/" + stripped_model_name
litellm_model_names.append(litellm_model_name)
return litellm_model_names
def get_models(
self, api_key: Optional[str] = None, api_base: Optional[str] = None
) -> List[str]:
@ -72,11 +80,7 @@ class GeminiModelInfo(BaseLLMModelInfo):
models = response.json()["models"]
litellm_model_names = []
for model in models:
stripped_model_name = model["name"].strip("models/")
litellm_model_name = "gemini/" + stripped_model_name
litellm_model_names.append(litellm_model_name)
litellm_model_names = self.process_model_name(models)
return litellm_model_names
def get_error_class(

View file

@ -1,25 +1,7 @@
model_list:
- model_name: gemini-2.5-pro
- model_name: "gemini/*"
litellm_params:
model: gemini/gemini-2.5-pro
- model_name: azure-batches
litellm_params:
model: azure/gpt-4o
api_key: os.environ/AZURE_API_KEY_HIDDEN
api_base: os.environ/AZURE_API_BASE_HIDDEN
- model_name: openai-gpt-4o
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY_TEST
model_info:
id: 12345678
- model_name: openai-gpt-4o
litellm_params:
model: gpt-4o
api_key: os.environ/OPENAI_API_KEY_TEST_2
model_info:
id: 12345679
general_settings:
check_managed_files_batch_cost: true
model: "gemini/*"
api_key: os.environ/GEMINI_API_KEY
litellm_settings:
check_provider_endpoint: true

View file

@ -1387,6 +1387,8 @@ class ProxyConfig:
"litellm_settings": {},
}
if config is None:
raise Exception("Config cannot be None or Empty.")
# Process includes
config = self._process_includes(
config=config, base_dir=os.path.dirname(os.path.abspath(file_path or ""))

View file

@ -1342,3 +1342,28 @@ def test_anthropic_prefix_prompt():
print(f"response: {response}")
assert response is not None
assert response.choices[0].message.content.startswith("Argentina")
@pytest.mark.asyncio
async def test_claude_tool_use_with_anthropic_acreate():
response = await litellm.anthropic.messages.acreate(
messages=[
{"role": "user", "content": "Hello, can you tell me the weather in Boston?"}
],
model="anthropic/claude-3-5-sonnet-20240620",
stream=True,
max_tokens=100,
tools=[
{
"name": "get_weather",
"description": "Get current weather information for a specific location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
},
}
],
)
async for chunk in response:
print(chunk)

View file

@ -0,0 +1,86 @@
import pytest
from litellm.llms.gemini.common_utils import GeminiModelInfo
class TestGeminiModelInfo:
"""Test suite for GeminiModelInfo class"""
def test_process_model_name_normal_cases(self):
"""Test process_model_name with normal model names"""
gemini_model_info = GeminiModelInfo()
# Test with normal model names
models = [
{"name": "models/gemini-1.5-flash"},
{"name": "models/gemini-1.5-pro"},
{"name": "models/gemini-2.0-flash-exp"},
]
result = gemini_model_info.process_model_name(models)
expected = [
"gemini/gemini-1.5-flash",
"gemini/gemini-1.5-pro",
"gemini/gemini-2.0-flash-exp",
]
assert result == expected
def test_process_model_name_edge_cases(self):
"""Test process_model_name with edge cases that could be affected by strip() vs replace()"""
gemini_model_info = GeminiModelInfo()
# Test edge cases where model names end with characters from "models/"
# These would be incorrectly processed if using strip("models/") instead of replace("models/", "")
models = [
{
"name": "models/gemini-1.5-pro"
}, # ends with 'o' - would become "gemini-1.5-pr" with strip()
{
"name": "models/test-model"
}, # ends with 'l' - would become "gemini/test-mode" with strip()
{
"name": "models/custom-models"
}, # ends with 's' - would become "gemini/custom-model" with strip()
{
"name": "models/demo"
}, # ends with 'o' - would become "gemini/dem" with strip()
]
result = gemini_model_info.process_model_name(models)
expected = [
"gemini/gemini-1.5-pro", # 'o' should be preserved
"gemini/test-model", # 'l' should be preserved
"gemini/custom-models", # 's' should be preserved
"gemini/demo", # 'o' should be preserved
]
assert result == expected
def test_process_model_name_empty_list(self):
"""Test process_model_name with empty list"""
gemini_model_info = GeminiModelInfo()
result = gemini_model_info.process_model_name([])
assert result == []
def test_process_model_name_no_models_prefix(self):
"""Test process_model_name with model names that don't have 'models/' prefix"""
gemini_model_info = GeminiModelInfo()
models = [
{"name": "gemini-1.5-flash"}, # No "models/" prefix
{"name": "custom-model"},
]
result = gemini_model_info.process_model_name(models)
expected = [
"gemini/gemini-1.5-flash",
"gemini/custom-model",
]
assert result == expected

View file

@ -539,3 +539,75 @@ async def test_delete_deployment_type_mismatch():
assert (
"12345679" not in deleted_ids
), f"Model 12345679 should NOT be deleted. Deleted IDs: {deleted_ids}"
@pytest.mark.asyncio
async def test_get_config_from_file(tmp_path, monkeypatch):
"""
Test the _get_config_from_file method of ProxyConfig class.
Tests various scenarios: valid file, non-existent file, no file path, None config.
"""
import yaml
from litellm.proxy.proxy_server import ProxyConfig
# Create a ProxyConfig instance
proxy_config = ProxyConfig()
# Test Case 1: Valid YAML config file exists
test_config = {
"model_list": [{"model_name": "gpt-4", "litellm_params": {"model": "gpt-4"}}],
"general_settings": {"master_key": "sk-test"},
"router_settings": {"enable_pre_call_checks": True},
"litellm_settings": {"drop_params": True},
}
config_file = tmp_path / "test_config.yaml"
with open(config_file, "w") as f:
yaml.dump(test_config, f)
# Clear global user_config_file_path for this test
monkeypatch.setattr("litellm.proxy.proxy_server.user_config_file_path", None)
result = await proxy_config._get_config_from_file(str(config_file))
assert result == test_config
# Verify that user_config_file_path was set
from litellm.proxy.proxy_server import user_config_file_path
assert user_config_file_path == str(config_file)
# Test Case 2: File path provided but file doesn't exist
non_existent_file = tmp_path / "non_existent.yaml"
with pytest.raises(Exception, match=f"Config file not found: {non_existent_file}"):
await proxy_config._get_config_from_file(str(non_existent_file))
# Test Case 3: No file path provided (should return default config)
monkeypatch.setattr("litellm.proxy.proxy_server.user_config_file_path", None)
expected_default = {
"model_list": [],
"general_settings": {},
"router_settings": {},
"litellm_settings": {},
}
result = await proxy_config._get_config_from_file(None)
assert result == expected_default
# Test Case 4: Empty YAML file (should raise exception for None config)
empty_file = tmp_path / "empty_config.yaml"
with open(empty_file, "w") as f:
f.write("") # Write empty content which will result in None when loaded
with pytest.raises(Exception, match="Config cannot be None or Empty."):
await proxy_config._get_config_from_file(str(empty_file))
# Test Case 5: Using global user_config_file_path when no config_file_path provided
monkeypatch.setattr(
"litellm.proxy.proxy_server.user_config_file_path", str(config_file)
)
result = await proxy_config._get_config_from_file(None)
assert result == test_config