mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
security: move Gemini API keys from URL query params to x-goog-api-key header
API keys passed as ?key=<KEY> in URLs leak into httpx error tracebacks when requests fail. Move all Google AI Studio / Gemini API key usage from URL query parameters to the x-goog-api-key HTTP header, which is not included in httpx.HTTPStatusError messages. WebSocket URLs (realtime) are excluded since WS clients do not support custom HTTP headers universally. Backport of BerriAI/litellm#25117 to v1.81.3-stable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
61ed8f9e03
commit
f9ac3e43bc
9 changed files with 66 additions and 51 deletions
|
|
@ -28,7 +28,9 @@ class GeminiModelInfo(BaseLLMModelInfo):
|
|||
api_key: Optional[str] = None,
|
||||
api_base: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Google AI Studio sends api key in query params"""
|
||||
"""Google AI Studio uses x-goog-api-key header for authentication."""
|
||||
if api_key:
|
||||
headers["x-goog-api-key"] = api_key
|
||||
return headers
|
||||
|
||||
@property
|
||||
|
|
@ -71,7 +73,8 @@ class GeminiModelInfo(BaseLLMModelInfo):
|
|||
)
|
||||
|
||||
response = litellm.module_level_client.get(
|
||||
url=f"{api_base}{endpoint}?key={api_key}",
|
||||
url=f"{api_base}{endpoint}",
|
||||
headers={"x-goog-api-key": api_key},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class GoogleAIStudioFilesHandler(GeminiModelInfo, BaseFilesConfig):
|
|||
if not api_key:
|
||||
raise ValueError("api_key is required")
|
||||
|
||||
url = "{}/{}?key={}".format(api_base, endpoint, api_key)
|
||||
url = "{}/{}".format(api_base, endpoint)
|
||||
return url
|
||||
|
||||
def get_supported_openai_params(
|
||||
|
|
|
|||
|
|
@ -66,9 +66,13 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
model: str,
|
||||
litellm_params: Optional[GenericLiteLLMParams],
|
||||
) -> dict:
|
||||
"""Google AI Studio uses API key in query params, not headers."""
|
||||
"""Google AI Studio uses x-goog-api-key header for authentication."""
|
||||
headers = headers or {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
if litellm_params:
|
||||
api_key = GeminiModelInfo.get_api_key(litellm_params.get("api_key"))
|
||||
if api_key:
|
||||
headers["x-goog-api-key"] = api_key
|
||||
return headers
|
||||
|
||||
def get_complete_url(
|
||||
|
|
@ -89,11 +93,10 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
"Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable."
|
||||
)
|
||||
|
||||
query_params = f"key={api_key}"
|
||||
if stream:
|
||||
query_params += "&alt=sse"
|
||||
|
||||
return f"{api_base}/{self.api_version}/interactions?{query_params}"
|
||||
return f"{api_base}/{self.api_version}/interactions?alt=sse"
|
||||
|
||||
return f"{api_base}/{self.api_version}/interactions"
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
|
|
@ -182,10 +185,9 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
) -> Tuple[str, Dict]:
|
||||
"""GET /{api_version}/interactions/{interaction_id}"""
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
api_key = GeminiModelInfo.get_api_key(litellm_params.api_key)
|
||||
if not api_key:
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}?key={api_key}", {}
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}", {}
|
||||
|
||||
def transform_get_interaction_response(
|
||||
self,
|
||||
|
|
@ -213,10 +215,9 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
) -> Tuple[str, Dict]:
|
||||
"""DELETE /{api_version}/interactions/{interaction_id}"""
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
api_key = GeminiModelInfo.get_api_key(litellm_params.api_key)
|
||||
if not api_key:
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}?key={api_key}", {}
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}", {}
|
||||
|
||||
def transform_delete_interaction_response(
|
||||
self,
|
||||
|
|
@ -241,10 +242,9 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
|
|||
) -> Tuple[str, Dict]:
|
||||
"""POST /{api_version}/interactions/{interaction_id}:cancel (if supported)"""
|
||||
resolved_api_base = GeminiModelInfo.get_api_base(api_base)
|
||||
api_key = GeminiModelInfo.get_api_key(litellm_params.api_key)
|
||||
if not api_key:
|
||||
if not GeminiModelInfo.get_api_key(litellm_params.api_key):
|
||||
raise ValueError("Google API key is required")
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}:cancel?key={api_key}", {}
|
||||
return f"{resolved_api_base}/{self.api_version}/interactions/{interaction_id}:cancel", {}
|
||||
|
||||
def transform_cancel_interaction_response(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ class GeminiRealtimeConfig(BaseRealtimeConfig):
|
|||
raise ValueError("api_key is required for Gemini API calls")
|
||||
api_base = api_base.replace("https://", "wss://")
|
||||
api_base = api_base.replace("http://", "ws://")
|
||||
# WebSocket connections do not support custom HTTP headers in all clients,
|
||||
# so the API key must remain as a query parameter here. This is an accepted
|
||||
# limitation; httpx is not used for WebSocket so MaskedHTTPStatusError
|
||||
# already covers the main leak vector.
|
||||
return f"{api_base}/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key={api_key}"
|
||||
|
||||
def map_model_turn_event(
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class GeminiVectorStoreConfig(BaseVectorStoreConfig):
|
|||
def get_auth_credentials(
|
||||
self, litellm_params: dict
|
||||
) -> BaseVectorStoreAuthCredentials:
|
||||
"""Gemini uses API key in query params, not headers."""
|
||||
"""Gemini uses x-goog-api-key header for authentication."""
|
||||
return {}
|
||||
|
||||
def get_vector_store_endpoints_by_type(self) -> VectorStoreIndexEndpoints:
|
||||
|
|
@ -79,7 +79,8 @@ class GeminiVectorStoreConfig(BaseVectorStoreConfig):
|
|||
api_key = litellm_params.get("api_key") or get_api_key_from_env()
|
||||
if api_key:
|
||||
self._cached_api_key = api_key
|
||||
|
||||
headers["x-goog-api-key"] = api_key
|
||||
|
||||
return headers
|
||||
|
||||
def get_complete_url(self, api_base: Optional[str], litellm_params: dict) -> str:
|
||||
|
|
@ -133,13 +134,10 @@ class GeminiVectorStoreConfig(BaseVectorStoreConfig):
|
|||
if model and model.startswith("gemini/"):
|
||||
model = model.replace("gemini/", "")
|
||||
|
||||
# Get API key - Gemini requires it as a query parameter
|
||||
api_key = litellm_params.get("api_key") or GeminiModelInfo.get_api_key()
|
||||
if not api_key:
|
||||
raise ValueError("GEMINI_API_KEY or GOOGLE_API_KEY is required")
|
||||
|
||||
# Build the URL for generateContent with API key
|
||||
url = f"{api_base}/models/{model}:generateContent?key={api_key}"
|
||||
url = f"{api_base}/models/{model}:generateContent"
|
||||
|
||||
# Build file_search tool configuration (using snake_case as per Gemini docs)
|
||||
file_search_config: Dict[str, Any] = {
|
||||
|
|
@ -289,11 +287,8 @@ class GeminiVectorStoreConfig(BaseVectorStoreConfig):
|
|||
Transform create request to Gemini's fileSearchStores format.
|
||||
"""
|
||||
url = f"{api_base}/fileSearchStores"
|
||||
|
||||
# Append API key as query parameter (required by Gemini)
|
||||
api_key = self._cached_api_key or get_api_key_from_env()
|
||||
if api_key:
|
||||
url = f"{url}?key={api_key}"
|
||||
|
||||
# API key is passed via x-goog-api-key header (set in validate_environment)
|
||||
|
||||
request_body: Dict[str, Any] = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -330,8 +330,14 @@ def _get_gemini_url(
|
|||
mode: all_gemini_url_modes,
|
||||
model: str,
|
||||
stream: Optional[bool],
|
||||
gemini_api_key: Optional[str],
|
||||
gemini_api_key: Optional[str] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""Build the Gemini API URL for the given mode.
|
||||
|
||||
The API key is NOT included in the URL. Callers must pass it via the
|
||||
``x-goog-api-key`` header instead to avoid leaking credentials in
|
||||
error tracebacks.
|
||||
"""
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
|
|
@ -343,29 +349,29 @@ def _get_gemini_url(
|
|||
endpoint = "generateContent"
|
||||
if stream is True:
|
||||
endpoint = "streamGenerateContent"
|
||||
url = "https://generativelanguage.googleapis.com/{}/{}:{}?key={}&alt=sse".format(
|
||||
api_version, _gemini_model_name, endpoint, gemini_api_key
|
||||
url = "https://generativelanguage.googleapis.com/{}/{}:{}?alt=sse".format(
|
||||
api_version, _gemini_model_name, endpoint
|
||||
)
|
||||
else:
|
||||
url = (
|
||||
"https://generativelanguage.googleapis.com/{}/{}:{}?key={}".format(
|
||||
api_version, _gemini_model_name, endpoint, gemini_api_key
|
||||
"https://generativelanguage.googleapis.com/{}/{}:{}".format(
|
||||
api_version, _gemini_model_name, endpoint
|
||||
)
|
||||
)
|
||||
elif mode == "embedding":
|
||||
endpoint = "embedContent"
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format(
|
||||
_gemini_model_name, endpoint, gemini_api_key
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}".format(
|
||||
_gemini_model_name, endpoint
|
||||
)
|
||||
elif mode == "batch_embedding":
|
||||
endpoint = "batchEmbedContents"
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format(
|
||||
_gemini_model_name, endpoint, gemini_api_key
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}".format(
|
||||
_gemini_model_name, endpoint
|
||||
)
|
||||
elif mode == "count_tokens":
|
||||
endpoint = "countTokens"
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format(
|
||||
_gemini_model_name, endpoint, gemini_api_key
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}:{}".format(
|
||||
_gemini_model_name, endpoint
|
||||
)
|
||||
elif mode == "image_generation":
|
||||
raise ValueError(
|
||||
|
|
|
|||
|
|
@ -56,10 +56,10 @@ class ContextCachingEndpoints(VertexBase):
|
|||
token, url
|
||||
"""
|
||||
if custom_llm_provider == "gemini":
|
||||
auth_header = None
|
||||
auth_header = {"x-goog-api-key": gemini_api_key} # type: ignore[assignment]
|
||||
endpoint = "cachedContents"
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}?key={}".format(
|
||||
endpoint, gemini_api_key
|
||||
url = "https://generativelanguage.googleapis.com/v1beta/{}".format(
|
||||
endpoint
|
||||
)
|
||||
elif custom_llm_provider == "vertex_ai":
|
||||
auth_header = vertex_auth_header
|
||||
|
|
@ -287,7 +287,9 @@ class ContextCachingEndpoints(VertexBase):
|
|||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if token is not None:
|
||||
if isinstance(token, dict):
|
||||
headers.update(token)
|
||||
elif token is not None:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if extra_headers is not None:
|
||||
headers.update(extra_headers)
|
||||
|
|
@ -419,7 +421,9 @@ class ContextCachingEndpoints(VertexBase):
|
|||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if token is not None:
|
||||
if isinstance(token, dict):
|
||||
headers.update(token)
|
||||
elif token is not None:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if extra_headers is not None:
|
||||
headers.update(extra_headers)
|
||||
|
|
|
|||
|
|
@ -420,9 +420,8 @@ class VertexBase:
|
|||
mode=mode,
|
||||
model=model,
|
||||
stream=stream,
|
||||
gemini_api_key=gemini_api_key,
|
||||
)
|
||||
auth_header = None # this field is not used for gemin
|
||||
auth_header = {"x-goog-api-key": gemini_api_key} # type: ignore[assignment]
|
||||
else:
|
||||
vertex_location = self.get_vertex_region(
|
||||
vertex_region=vertex_location,
|
||||
|
|
|
|||
|
|
@ -135,12 +135,12 @@ class GeminiRAGIngestion(BaseRAGIngestion):
|
|||
Returns:
|
||||
Store name (format: fileSearchStores/xxxxxxx)
|
||||
"""
|
||||
url = f"{base_url}/fileSearchStores?key={api_key}"
|
||||
|
||||
url = f"{base_url}/fileSearchStores"
|
||||
|
||||
request_body = {
|
||||
"displayName": display_name
|
||||
}
|
||||
|
||||
|
||||
client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.RAG,
|
||||
params={"timeout": 60.0},
|
||||
|
|
@ -148,7 +148,10 @@ class GeminiRAGIngestion(BaseRAGIngestion):
|
|||
response = await client.post(
|
||||
url,
|
||||
json=request_body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": api_key,
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
|
|
@ -222,7 +225,7 @@ class GeminiRAGIngestion(BaseRAGIngestion):
|
|||
# base_url is like: https://generativelanguage.googleapis.com/v1beta
|
||||
# We need: https://generativelanguage.googleapis.com/upload/v1beta/{store_id}:uploadToFileSearchStore
|
||||
api_base = base_url.replace("/v1beta", "") # Get base without version
|
||||
url = f"{api_base}/upload/v1beta/{vector_store_id}:uploadToFileSearchStore?key={api_key}"
|
||||
url = f"{api_base}/upload/v1beta/{vector_store_id}:uploadToFileSearchStore"
|
||||
|
||||
# Build request body with chunking config and metadata if provided
|
||||
request_body: Dict[str, Any] = {
|
||||
|
|
@ -252,6 +255,7 @@ class GeminiRAGIngestion(BaseRAGIngestion):
|
|||
"X-Goog-Upload-Header-Content-Length": str(file_size),
|
||||
"X-Goog-Upload-Header-Content-Type": content_type,
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": api_key,
|
||||
}
|
||||
|
||||
verbose_logger.debug(f"Initiating resumable upload: {url}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue