mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-13 15:07:47 +00:00
Fix/gemini api key environment variable support (#12507)
* Fix: Add support for GOOGLE_API_KEY environment variables for Gemini API authentication * added test cases * incoperated feedback to make it more maintainable * fix failed linting CI
This commit is contained in:
@@ -44,7 +44,7 @@ class GeminiModelInfo(BaseLLMModelInfo):
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
|
||||
return api_key or (get_secret_str("GEMINI_API_KEY"))
|
||||
return api_key or (get_secret_str("GOOGLE_API_KEY")) or (get_secret_str("GEMINI_API_KEY"))
|
||||
|
||||
@staticmethod
|
||||
def get_base_model(model: str) -> Optional[str]:
|
||||
@@ -66,7 +66,7 @@ class GeminiModelInfo(BaseLLMModelInfo):
|
||||
endpoint = f"/{self.api_version}/models"
|
||||
if api_base is None or api_key is None:
|
||||
raise ValueError(
|
||||
"GEMINI_API_BASE or GEMINI_API_KEY is not set. Please set the environment variable, to query Gemini's `/models` endpoint."
|
||||
"GEMINI_API_BASE or GEMINI_API_KEY/GOOGLE_API_KEY is not set. Please set the environment variable, to query Gemini's `/models` endpoint."
|
||||
)
|
||||
|
||||
response = litellm.module_level_client.get(
|
||||
@@ -133,3 +133,7 @@ def encode_unserializable_types(
|
||||
else:
|
||||
processed_data[key] = value
|
||||
return processed_data
|
||||
|
||||
|
||||
def get_api_key_from_env() -> Optional[str]:
|
||||
return get_secret_str("GOOGLE_API_KEY") or get_secret_str("GEMINI_API_KEY")
|
||||
|
||||
@@ -11,7 +11,6 @@ from litellm.llms.base_llm.google_genai.transformation import (
|
||||
BaseGoogleGenAIGenerateContentConfig,
|
||||
)
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexLLM
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -24,7 +23,7 @@ else:
|
||||
GenerateContentConfigDict = Any
|
||||
GenerateContentContentListUnionDict = Any
|
||||
GenerateContentResponse = Any
|
||||
|
||||
from ..common_utils import get_api_key_from_env
|
||||
|
||||
class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM):
|
||||
"""
|
||||
@@ -131,7 +130,7 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM):
|
||||
return (
|
||||
litellm_params.pop("api_key", None)
|
||||
or litellm_params.pop("gemini_api_key", None)
|
||||
or get_secret_str("GEMINI_API_KEY")
|
||||
or get_api_key_from_env()
|
||||
or litellm.api_key
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ This file contains the transformation logic for the Gemini realtime API.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
@@ -55,7 +54,7 @@ from litellm.types.realtime import (
|
||||
)
|
||||
from litellm.utils import get_empty_usage
|
||||
|
||||
from ..common_utils import encode_unserializable_types
|
||||
from ..common_utils import encode_unserializable_types, get_api_key_from_env
|
||||
|
||||
MAP_GEMINI_FIELD_TO_OPENAI_EVENT: Dict[str, OpenAIRealtimeEventTypes] = {
|
||||
"setupComplete": OpenAIRealtimeEventTypes.SESSION_CREATED,
|
||||
@@ -81,7 +80,7 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
if api_base is None:
|
||||
api_base = "wss://generativelanguage.googleapis.com"
|
||||
if api_key is None:
|
||||
api_key = os.environ.get("GEMINI_API_KEY")
|
||||
api_key = get_api_key_from_env()
|
||||
if api_key is None:
|
||||
raise ValueError("api_key is required for Gemini API calls")
|
||||
api_base = api_base.replace("https://", "wss://")
|
||||
@@ -188,9 +187,9 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
|
||||
vertex_gemini_config = VertexGeminiConfig()
|
||||
vertex_gemini_config._map_function(value)
|
||||
optional_params["generationConfig"][
|
||||
"tools"
|
||||
] = vertex_gemini_config._map_function(value)
|
||||
optional_params["generationConfig"]["tools"] = (
|
||||
vertex_gemini_config._map_function(value)
|
||||
)
|
||||
elif key == "input_audio_transcription" and value is not None:
|
||||
optional_params["inputAudioTranscription"] = {}
|
||||
elif key == "turn_detection":
|
||||
@@ -201,10 +200,10 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
if (
|
||||
len(transformed_audio_activity_config) > 0
|
||||
): # if the config is not empty, add it to the optional params
|
||||
optional_params[
|
||||
"realtimeInputConfig"
|
||||
] = BidiGenerateContentRealtimeInputConfig(
|
||||
automaticActivityDetection=transformed_audio_activity_config
|
||||
optional_params["realtimeInputConfig"] = (
|
||||
BidiGenerateContentRealtimeInputConfig(
|
||||
automaticActivityDetection=transformed_audio_activity_config
|
||||
)
|
||||
)
|
||||
if len(optional_params["generationConfig"]) == 0:
|
||||
optional_params.pop("generationConfig")
|
||||
@@ -405,15 +404,17 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
output_index=0,
|
||||
event_id="event_{}".format(uuid.uuid4()),
|
||||
item_id=output_item_id,
|
||||
part={
|
||||
"type": "text",
|
||||
"text": "",
|
||||
}
|
||||
if delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "",
|
||||
},
|
||||
part=(
|
||||
{
|
||||
"type": "text",
|
||||
"text": "",
|
||||
}
|
||||
if delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "",
|
||||
}
|
||||
),
|
||||
response_id=response_id,
|
||||
)
|
||||
response_items.append(response_content_part_added)
|
||||
@@ -440,9 +441,11 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
)
|
||||
|
||||
return OpenAIRealtimeResponseDelta(
|
||||
type="response.text.delta"
|
||||
if delta_type == "text"
|
||||
else "response.audio.delta",
|
||||
type=(
|
||||
"response.text.delta"
|
||||
if delta_type == "text"
|
||||
else "response.audio.delta"
|
||||
),
|
||||
content_index=0,
|
||||
event_id="event_{}".format(uuid.uuid4()),
|
||||
item_id=output_item_id,
|
||||
@@ -513,12 +516,14 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
event_id="event_{}".format(uuid.uuid4()),
|
||||
item_id=current_output_item_id,
|
||||
output_index=0,
|
||||
part={"type": "text", "text": delta_done_event_text}
|
||||
if delta_done_event_text and delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "", # gemini doesn't return transcript for audio
|
||||
},
|
||||
part=(
|
||||
{"type": "text", "text": delta_done_event_text}
|
||||
if delta_done_event_text and delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "", # gemini doesn't return transcript for audio
|
||||
}
|
||||
),
|
||||
response_id=current_response_id,
|
||||
)
|
||||
returned_items.append(response_content_part_done)
|
||||
@@ -535,12 +540,14 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": delta_done_event_text}
|
||||
if delta_done_event_text and delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "",
|
||||
}
|
||||
(
|
||||
{"type": "text", "text": delta_done_event_text}
|
||||
if delta_done_event_text and delta_type == "text"
|
||||
else {
|
||||
"type": "audio",
|
||||
"transcript": "",
|
||||
}
|
||||
)
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -674,9 +681,11 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
object="realtime.response",
|
||||
id=current_response_id,
|
||||
status="completed",
|
||||
output=[output_item["item"] for output_item in output_items]
|
||||
if output_items
|
||||
else [],
|
||||
output=(
|
||||
[output_item["item"] for output_item in output_items]
|
||||
if output_items
|
||||
else []
|
||||
),
|
||||
conversation_id=current_conversation_id,
|
||||
modalities=_modalities,
|
||||
usage=responses_api_usage.model_dump(),
|
||||
@@ -828,9 +837,9 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
||||
"session_configuration_request"
|
||||
]
|
||||
current_item_chunks = realtime_response_transform_input["current_item_chunks"]
|
||||
current_delta_type: Optional[
|
||||
ALL_DELTA_TYPES
|
||||
] = realtime_response_transform_input["current_delta_type"]
|
||||
current_delta_type: Optional[ALL_DELTA_TYPES] = (
|
||||
realtime_response_transform_input["current_delta_type"]
|
||||
)
|
||||
returned_message: List[OpenAIRealtimeEvents] = []
|
||||
|
||||
for key, value in json_message.items():
|
||||
|
||||
+3
-2
@@ -148,6 +148,7 @@ from .llms.custom_llm import CustomLLM, custom_chat_llm_router
|
||||
from .llms.databricks.embed.handler import DatabricksEmbeddingHandler
|
||||
from .llms.deprecated_providers import aleph_alpha, palm
|
||||
from .llms.groq.chat.handler import GroqChatCompletion
|
||||
from .llms.gemini.common_utils import get_api_key_from_env
|
||||
from .llms.huggingface.embedding.handler import HuggingFaceEmbedding
|
||||
from .llms.nlp_cloud.chat.handler import completion as nlp_cloud_chat_completion
|
||||
from .llms.ollama.completion import handler as ollama
|
||||
@@ -2595,7 +2596,7 @@ def completion( # type: ignore # noqa: PLR0915
|
||||
|
||||
gemini_api_key = (
|
||||
api_key
|
||||
or get_secret("GEMINI_API_KEY")
|
||||
or get_api_key_from_env()
|
||||
or get_secret("PALM_API_KEY") # older palm api key should also work
|
||||
or litellm.api_key
|
||||
)
|
||||
@@ -3999,7 +4000,7 @@ def embedding( # noqa: PLR0915
|
||||
)
|
||||
elif custom_llm_provider == "gemini":
|
||||
gemini_api_key = (
|
||||
api_key or get_secret_str("GEMINI_API_KEY") or litellm.api_key
|
||||
api_key or get_api_key_from_env() or litellm.api_key
|
||||
)
|
||||
|
||||
api_base = api_base or litellm.api_base or get_secret_str("GEMINI_API_BASE")
|
||||
|
||||
@@ -190,7 +190,7 @@ async def gemini_proxy_route(
|
||||
)
|
||||
if gemini_api_key is None:
|
||||
raise Exception(
|
||||
"Required 'GEMINI_API_KEY' in environment to make pass-through calls to Google AI Studio."
|
||||
"Required 'GEMINI_API_KEY'/'GOOGLE_API_KEY' in environment to make pass-through calls to Google AI Studio."
|
||||
)
|
||||
# Merge query parameters, giving precedence to those in updated_url
|
||||
merged_params = dict(request.query_params)
|
||||
|
||||
+3
-2
@@ -5279,10 +5279,11 @@ def validate_environment( # noqa: PLR0915
|
||||
else:
|
||||
missing_keys.append("FEATHERLESS_AI_API_KEY")
|
||||
elif custom_llm_provider == "gemini":
|
||||
if "GEMINI_API_KEY" in os.environ:
|
||||
if ("GOOGLE_API_KEY" in os.environ) or ("GEMINI_API_KEY" in os.environ):
|
||||
keys_in_environment = True
|
||||
else:
|
||||
missing_keys.append("GEMINI_API_KEY")
|
||||
missing_keys.append("GOOGLE_API_KEY")
|
||||
missing_keys.append("GEMINI_API_KEY")
|
||||
elif custom_llm_provider == "groq":
|
||||
if "GROQ_API_KEY" in os.environ:
|
||||
keys_in_environment = True
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import pytest
|
||||
import litellm
|
||||
import os
|
||||
from unittest.mock import patch, Mock
|
||||
from litellm import completion
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_gemini_api_key(monkeypatch):
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "fake-gemini-key-for-testing")
|
||||
|
||||
|
||||
def test_gemini_completion():
|
||||
response = completion(
|
||||
model="gemini/gemini-2.0-flash-exp-image-generation",
|
||||
messages=[{"role": "user", "content": "Test message"}],
|
||||
mock_response="Test Message",
|
||||
)
|
||||
assert response.choices[0].message.content is not None
|
||||
|
||||
|
||||
def test_gemini_completion_no_api_key():
|
||||
"""Test Gemini completion fails gracefully when no API key is provided."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Remove all API keys
|
||||
for key in ["GOOGLE_API_KEY", "GEMINI_API_KEY"]:
|
||||
if key in os.environ:
|
||||
del os.environ[key]
|
||||
|
||||
# Test without mock_response to ensure actual API key validation
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
completion(
|
||||
model="gemini/gemini-1.5-flash",
|
||||
messages=[{"role": "user", "content": "Test message"}],
|
||||
)
|
||||
|
||||
# Check that the exception message contains API key related text
|
||||
error_message = str(exc_info.value).lower()
|
||||
assert any(
|
||||
keyword in error_message
|
||||
for keyword in [
|
||||
"api key",
|
||||
"authentication",
|
||||
"unauthorized",
|
||||
"invalid",
|
||||
"missing",
|
||||
"credential",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_gemini_completion_no_api_key_with_mock():
|
||||
"""Alternative test that properly mocks the API key validation."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
# Remove all API keys
|
||||
for key in ["GOOGLE_API_KEY", "GEMINI_API_KEY"]:
|
||||
if key in os.environ:
|
||||
del os.environ[key]
|
||||
|
||||
with patch("litellm.get_secret") as mock_get_secret:
|
||||
mock_get_secret.return_value = None
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
completion(
|
||||
model="gemini/gemini-1.5-flash",
|
||||
messages=[{"role": "user", "content": "Test message"}],
|
||||
)
|
||||
|
||||
error_message = str(exc_info.value).lower()
|
||||
assert any(
|
||||
keyword in error_message
|
||||
for keyword in [
|
||||
"api key",
|
||||
"authentication",
|
||||
"unauthorized",
|
||||
"invalid",
|
||||
"missing",
|
||||
"credential",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_key_env", ["GOOGLE_API_KEY", "GEMINI_API_KEY"])
|
||||
def test_gemini_completion_both_env_vars(monkeypatch, api_key_env):
|
||||
"""Test Gemini completion works with both environment variable names."""
|
||||
# Clear all API keys first
|
||||
monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
|
||||
# Set the specific API key being tested
|
||||
monkeypatch.setenv(api_key_env, f"fake-{api_key_env.lower()}-for-testing")
|
||||
|
||||
response = completion(
|
||||
model="gemini/gemini-1.5-flash",
|
||||
messages=[{"role": "user", "content": f"Test with {api_key_env}"}],
|
||||
mock_response=f"Mocked response using {api_key_env}",
|
||||
)
|
||||
assert (
|
||||
response["choices"][0]["message"]["content"]
|
||||
== f"Mocked response using {api_key_env}"
|
||||
)
|
||||
@@ -2,7 +2,7 @@
|
||||
Test cases for spend log cleanup functionality
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
Reference in New Issue
Block a user