From f7374ad80043f9f1bb924517a2282cd89fc9960a Mon Sep 17 00:00:00 2001 From: balazss Date: Tue, 17 Mar 2026 20:05:28 -0700 Subject: [PATCH] feat(gemini): add oauth login and google code assist provider support --- docs/my-website/docs/providers/gemini.md | 21 +- .../docs/providers/google_code_assist.md | 84 ++++++ docs/my-website/sidebars.js | 1 + litellm/llms/gemini/authenticator.py | 260 ++++++++++++++++++ litellm/llms/gemini/common_utils.py | 116 ++++++++ litellm/llms/gemini/fallback_handler.py | 47 ++++ litellm/llms/google_code_assist/chat.py | 226 +++++++++++++++ .../llms/google_code_assist/transformation.py | 195 +++++++++++++ litellm/llms/vertex_ai/common_utils.py | 33 +-- litellm/llms/vertex_ai/vertex_llm_base.py | 43 ++- litellm/main.py | 129 +++++++-- litellm/proxy/client/cli/README.md | 12 + litellm/proxy/client/cli/commands/gemini.py | 19 ++ litellm/proxy/client/cli/main.py | 2 + litellm/types/utils.py | 1 + litellm/utils.py | 13 + poetry.lock | 73 ++--- .../test_google_code_assist.py | 127 +++++++++ 18 files changed, 1324 insertions(+), 78 deletions(-) create mode 100644 docs/my-website/docs/providers/google_code_assist.md create mode 100644 litellm/llms/gemini/authenticator.py create mode 100644 litellm/llms/gemini/fallback_handler.py create mode 100644 litellm/llms/google_code_assist/chat.py create mode 100644 litellm/llms/google_code_assist/transformation.py create mode 100644 litellm/proxy/client/cli/commands/gemini.py create mode 100644 tests/test_litellm/llms/google_code_assist/test_google_code_assist.py diff --git a/docs/my-website/docs/providers/gemini.md b/docs/my-website/docs/providers/gemini.md index 0aaf3d5ae81..c38f8121ba9 100644 --- a/docs/my-website/docs/providers/gemini.md +++ b/docs/my-website/docs/providers/gemini.md @@ -34,6 +34,26 @@ import os os.environ["GEMINI_API_KEY"] = "your-api-key" ``` +## OAuth Authentication (Optional) + +You can also authenticate Gemini requests with OAuth credentials. + +```bash +export GEMINI_OAUTH_CLIENT_ID="" +export GEMINI_OAUTH_CLIENT_SECRET="" +litellm-proxy gemini login +``` + +LiteLLM then reads OAuth credentials from: + +1. `GEMINI_OAUTH_TOKEN` +2. `GEMINI_CREDENTIALS_PATH` +3. `~/.config/litellm/gemini_oauth/oauth_creds.json` +4. `~/.gemini/oauth_creds.json` +5. `~/.config/gcloud/application_default_credentials.json` + +For Google Code Assist-specific usage, see [Google Code Assist](./google_code_assist.md). + ## Sample Usage ```python from litellm import completion @@ -2459,4 +2479,3 @@ LiteLLM automatically calculates costs using `output_cost_per_image_token` from ``` For more details, see [Google's Gemini pricing documentation](https://ai.google.dev/gemini-api/docs/pricing). - diff --git a/docs/my-website/docs/providers/google_code_assist.md b/docs/my-website/docs/providers/google_code_assist.md new file mode 100644 index 00000000000..6dd3491445f --- /dev/null +++ b/docs/my-website/docs/providers/google_code_assist.md @@ -0,0 +1,84 @@ +# Google Code Assist + +Use Google Code Assist models through LiteLLM using Gemini OAuth credentials. + +| Property | Details | +|-------|-------| +| Description | Google Code Assist API via Google Cloud Code Assist backend | +| Provider Route on LiteLLM | `google_code_assist/` | +| Supported Endpoints | `/chat/completions` | +| Base Endpoint | `https://cloudcode-pa.googleapis.com` | + +## Model Format + +Use: + +`google_code_assist/` + +Example: + +`google_code_assist/gemini-2.5-pro` + +## Authentication + +Google Code Assist requires OAuth credentials (not API key auth). + +LiteLLM checks credentials in this order: + +1. `GEMINI_OAUTH_TOKEN` +2. `GEMINI_CREDENTIALS_PATH` +3. `~/.config/litellm/gemini_oauth/oauth_creds.json` +4. `~/.gemini/oauth_creds.json` +5. `~/.config/gcloud/application_default_credentials.json` + +### Login via CLI + +```bash +export GEMINI_OAUTH_CLIENT_ID="" +export GEMINI_OAUTH_CLIENT_SECRET="" +litellm-proxy gemini login +``` + +This opens a browser loopback OAuth flow and stores credentials locally. + +## Usage - LiteLLM Python SDK + +```python +from litellm import completion + +response = completion( + model="google_code_assist/gemini-2.5-pro", + messages=[{"role": "user", "content": "Write a Python function to parse CSV safely."}], +) + +print(response.choices[0].message.content) +``` + +## Usage - LiteLLM Proxy + +```yaml +model_list: + - model_name: google-code-assist + litellm_params: + model: google_code_assist/gemini-2.5-pro +``` + +```bash +litellm --config /path/to/config.yaml +``` + +```bash +curl http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "model": "google-code-assist", + "messages": [{"role": "user", "content": "Refactor this code for readability."}] + }' +``` + +## Optional Params + +- `google_code_assist_project`: Optional project override sent to the Code Assist backend request. + +LiteLLM also performs the required `loadCodeAssist` handshake before `generateContent` automatically. diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 79a0279bad5..94a0a68a40b 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -796,6 +796,7 @@ const sidebars = { label: "Google AI Studio", items: [ "providers/gemini", + "providers/google_code_assist", "providers/gemini/videos", "providers/google_ai_studio/files", "providers/google_ai_studio/image_gen", diff --git a/litellm/llms/gemini/authenticator.py b/litellm/llms/gemini/authenticator.py new file mode 100644 index 00000000000..c5f6aab8a70 --- /dev/null +++ b/litellm/llms/gemini/authenticator.py @@ -0,0 +1,260 @@ +import json +import os +import time +import webbrowser +import http.server +import html +import urllib.parse +import secrets +import hashlib +import base64 +from typing import Any, Dict + +import httpx + +from litellm._logging import verbose_logger +from litellm.secret_managers.main import get_secret_str + +GEMINI_SCOPES = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/generative-language", +] + +# Google OAuth URLs +GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" + + +class GeminiAuthenticator: + def __init__(self) -> None: + """Initialize the Gemini authenticator with configurable token paths.""" + # Token storage paths + self.token_dir = os.getenv( + "GEMINI_OAUTH_TOKEN_DIR", + os.path.expanduser("~/.config/litellm/gemini_oauth"), + ) + self.oauth_creds_file = os.path.join( + self.token_dir, + os.getenv("GEMINI_OAUTH_CREDS_FILE", "oauth_creds.json"), + ) + self._ensure_token_dir() + + @staticmethod + def _get_oauth_client_credentials() -> tuple[str, str]: + client_id = get_secret_str("GEMINI_OAUTH_CLIENT_ID") + client_secret = get_secret_str("GEMINI_OAUTH_CLIENT_SECRET") + + if not client_id or not client_secret: + raise ValueError( + "Missing Gemini OAuth client credentials. " + "Set GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET." + ) + return client_id, client_secret + + def get_token(self) -> str: + """ + Get the OAuth token, refreshing if necessary. + + Returns: + str: The Gemini access token. + + Raises: + Exception: If unable to obtain or refresh an access token. + """ + try: + if os.path.exists(self.oauth_creds_file): + with open(self.oauth_creds_file, "r") as f: + creds = json.load(f) + + # Check if access_token exists and is valid (rough check) + if ( + creds.get("access_token") + and creds.get("expires_at", 0) > time.time() + 60 + ): + return creds.get("access_token") + + # If expired but has refresh_token, try to refresh + if creds.get("refresh_token"): + verbose_logger.debug( + "Gemini access token expired, refreshing..." + ) + return self._refresh_token(creds.get("refresh_token")) + except Exception as e: + verbose_logger.warning(f"Error reading Gemini OAuth credentials: {e}") + + # If we get here, we need to log in + verbose_logger.info("Starting Gemini OAuth login flow...") + creds = self._login() + self._write_oauth_creds(creds) + return creds.get("access_token") + + def _refresh_token(self, refresh_token: str) -> str: + """Refresh the access token using the refresh token.""" + client_id, client_secret = self._get_oauth_client_credentials() + data = { + "client_id": client_id, + "client_secret": client_secret, + "refresh_token": refresh_token, + "grant_type": "refresh_token", + } + resp = httpx.post(GOOGLE_TOKEN_URL, data=data) + resp.raise_for_status() + new_creds = resp.json() + + # Load existing creds to merge if possible + creds = {} + if os.path.exists(self.oauth_creds_file): + try: + with open(self.oauth_creds_file, "r") as f: + creds = json.load(f) + except Exception: + pass + + creds.update(new_creds) + if "expires_in" in new_creds: + creds["expires_at"] = time.time() + new_creds["expires_in"] + + self._write_oauth_creds(creds) + + return creds.get("access_token") + + def _ensure_token_dir(self) -> None: + """Ensure the token directory exists.""" + if not os.path.exists(self.token_dir): + os.makedirs(self.token_dir, mode=0o700, exist_ok=True) + else: + try: + os.chmod(self.token_dir, 0o700) + except OSError: + pass + + def _write_oauth_creds(self, creds: Dict[str, Any]) -> None: + """ + Write oauth credentials with user-only permissions. + """ + fd = os.open( + self.oauth_creds_file, + os.O_WRONLY | os.O_CREAT | os.O_TRUNC, + 0o600, + ) + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(creds, f) + try: + os.chmod(self.oauth_creds_file, 0o600) + except OSError: + pass + + def _login(self) -> Dict[str, Any]: + """Perform loopback flow login.""" + client_id, client_secret = self._get_oauth_client_credentials() + # PKCE + code_verifier = secrets.token_urlsafe(64) + code_challenge = ( + base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()) + .decode() + .replace("=", "") + ) + + state = secrets.token_urlsafe(32) + + # Local web server for callback + auth_code = None + error = None + + class CallbackHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + nonlocal auth_code, error + parsed_url = urllib.parse.urlparse(self.path) + if parsed_url.path != "/oauth2callback": + self.send_response(404) + self.end_headers() + return + + query = parsed_url.query + params = urllib.parse.parse_qs(query) + + if params.get("state", [None])[0] != state: + error = "state_mismatch" + self.send_response(400) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write( + b"

Authentication failed

State mismatch. Possible CSRF attack.

" + ) + elif "code" in params: + auth_code = params["code"][0] + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write( + b"

Authentication successful!

You can close this tab and return to the terminal.

" + ) + elif "error" in params: + error = params["error"][0] + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write( + ( + "

Authentication failed

" + f"{html.escape(error)}" + "

" + ).encode() + ) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + # Suppress logging to avoid noise in the terminal + pass + + server = http.server.HTTPServer(("127.0.0.1", 0), CallbackHandler) + port = server.server_port + redirect_uri = f"http://127.0.0.1:{port}/oauth2callback" + + params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + "scope": " ".join(GEMINI_SCOPES), + "state": state, + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "access_type": "offline", + "prompt": "consent", + } + auth_url = f"{GOOGLE_AUTH_URL}?{urllib.parse.urlencode(params)}" + + print( # noqa: T201 + f"Please visit the following URL to authenticate:\n\n{auth_url}\n" + ) + webbrowser.open(auth_url) + + # Wait for callback + server.handle_request() + server.server_close() + + if error: + raise Exception(f"OAuth login failed: {error}") + if not auth_code: + raise Exception("OAuth login failed: No code received") + + # Exchange code for token + data = { + "client_id": client_id, + "client_secret": client_secret, + "code": auth_code, + "code_verifier": code_verifier, + "grant_type": "authorization_code", + "redirect_uri": redirect_uri, + } + resp = httpx.post(GOOGLE_TOKEN_URL, data=data) + resp.raise_for_status() + creds = resp.json() + if "expires_in" in creds: + creds["expires_at"] = time.time() + creds["expires_in"] + + return creds diff --git a/litellm/llms/gemini/common_utils.py b/litellm/llms/gemini/common_utils.py index 87c107fab37..f93a105f09f 100644 --- a/litellm/llms/gemini/common_utils.py +++ b/litellm/llms/gemini/common_utils.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Union import httpx import litellm +from litellm._logging import verbose_logger from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter from litellm.llms.base_llm.chat.transformation import BaseLLMException @@ -154,6 +155,121 @@ def get_api_key_from_env() -> Optional[str]: return get_secret_str("GOOGLE_API_KEY") or get_secret_str("GEMINI_API_KEY") +def should_fallback_to_google_code_assist(error: Exception) -> bool: + """ + Returns True if the error indicates missing OAuth scope for Gemini calls. + """ + return "ACCESS_TOKEN_SCOPE_INSUFFICIENT" in str(error) + + +def get_gemini_oauth_token() -> Optional[dict]: # noqa: PLR0915 + """ + Returns the Gemini OAuth token and metadata. + Check: + 1. GEMINI_OAUTH_TOKEN env var + 2. GEMINI_CREDENTIALS_PATH env var + 3. ~/.config/litellm/gemini_oauth/oauth_creds.json + 4. ~/.gemini/oauth_creds.json (gemini-cli default) + 5. ~/.config/gcloud/application_default_credentials.json (Standard ADC) + """ + import json + import time + from pathlib import Path + + def _is_expired(creds_data: dict) -> bool: + expires_at = creds_data.get("expires_at") + if isinstance(expires_at, (int, float)): + return expires_at <= (time.time() + 60) + + expiry = creds_data.get("expiry") or creds_data.get("expires_on") + if isinstance(expiry, str): + try: + expires_dt = datetime.datetime.fromisoformat( + expiry.replace("Z", "+00:00") + ) + return expires_dt.timestamp() <= (time.time() + 60) + except Exception: + return False + + return False + + # 1. Check GEMINI_OAUTH_TOKEN + token = get_secret_str("GEMINI_OAUTH_TOKEN") + if token: + return {"token": token} + + # 2. Check GEMINI_CREDENTIALS_PATH or default paths + paths_to_check = [] + env_path = get_secret_str("GEMINI_CREDENTIALS_PATH") + if env_path: + paths_to_check.append(Path(env_path).expanduser()) + + paths_to_check.extend( + [ + Path("~/.config/litellm/gemini_oauth/oauth_creds.json").expanduser(), + Path("~/.gemini/oauth_creds.json").expanduser(), + Path("~/.config/gcloud/application_default_credentials.json").expanduser(), + ] + ) + + for creds_path in paths_to_check: + if creds_path.exists(): + try: + # Try to use google-auth if available + try: + from google.oauth2 import credentials + from google.auth.transport.requests import Request + + creds = credentials.Credentials.from_authorized_user_file( + str(creds_path) + ) + if not creds.valid: + verbose_logger.debug( + f"Refreshing Gemini token for {creds_path}" + ) + creds.refresh(Request()) + if creds.token: + result = {"token": creds.token} + # Extract project ID if present + with open(creds_path, "r") as f: + creds_data = json.load(f) + project_id = creds_data.get( + "quota_project_id" + ) or creds_data.get("project_id") + if project_id: + result["project_id"] = project_id + return result + except Exception: + # Fallback to manual reading + with open(creds_path, "r") as f: + creds_data = json.load(f) + if _is_expired(creds_data): + verbose_logger.warning( + "Gemini OAuth token appears expired in %s. " + "Run `litellm-proxy gemini login` to refresh credentials.", + creds_path, + ) + continue + token = creds_data.get("access_token") + if not token and "token" in creds_data: + token = creds_data["token"].get("accessToken") + + if token: + result = {"token": token} + project_id = creds_data.get( + "quota_project_id" + ) or creds_data.get("project_id") + if project_id: + result["project_id"] = project_id + return result + except Exception as e: + verbose_logger.error( + f"Error reading Gemini credentials from {creds_path}: {e}" + ) + + return None + + class GoogleAIStudioTokenCounter(BaseTokenCounter): """Token counter implementation for Google AI Studio provider.""" diff --git a/litellm/llms/gemini/fallback_handler.py b/litellm/llms/gemini/fallback_handler.py new file mode 100644 index 00000000000..60343b884cd --- /dev/null +++ b/litellm/llms/gemini/fallback_handler.py @@ -0,0 +1,47 @@ +from typing import Any, Awaitable, Callable, Dict + +from litellm._logging import verbose_logger +from litellm.llms.gemini.common_utils import should_fallback_to_google_code_assist +from litellm.llms.google_code_assist.chat import GoogleCodeAssistChat + + +async def run_gemini_acompletion_with_code_assist_fallback( + primary_call: Awaitable[Any], + fallback_kwargs: Dict[str, Any], +) -> Any: + """ + Execute Gemini async completion and fallback to Google Code Assist when + OAuth scope is insufficient. + """ + try: + return await primary_call + except Exception as e: + if not should_fallback_to_google_code_assist(e): + raise e + + verbose_logger.warning( + "Gemini request failed with ACCESS_TOKEN_SCOPE_INSUFFICIENT. " + "Falling back to google_code_assist." + ) + return await GoogleCodeAssistChat().acompletion(**fallback_kwargs) + + +def run_gemini_completion_with_code_assist_fallback( + primary_call: Callable[[], Any], + fallback_kwargs: Dict[str, Any], +) -> Any: + """ + Execute Gemini sync completion and fallback to Google Code Assist when + OAuth scope is insufficient. + """ + try: + return primary_call() + except Exception as e: + if not should_fallback_to_google_code_assist(e): + raise e + + verbose_logger.warning( + "Gemini request failed with ACCESS_TOKEN_SCOPE_INSUFFICIENT. " + "Falling back to google_code_assist." + ) + return GoogleCodeAssistChat().completion(**fallback_kwargs) diff --git a/litellm/llms/google_code_assist/chat.py b/litellm/llms/google_code_assist/chat.py new file mode 100644 index 00000000000..af6fa7abc01 --- /dev/null +++ b/litellm/llms/google_code_assist/chat.py @@ -0,0 +1,226 @@ +import httpx +from typing import Any, Optional + +import litellm +from litellm._logging import verbose_logger +from litellm.llms.custom_httpx.http_handler import _get_httpx_client, AsyncHTTPHandler +from .transformation import GoogleCodeAssistConfig, GoogleCodeAssistError + + +class GoogleCodeAssistChat: + """ + Handler for Google Code Assist API. + Provides both synchronous and asynchronous completion methods. + """ + + def __init__(self) -> None: + self.config = GoogleCodeAssistConfig() + + def completion( + self, + model: str, + messages: list, + model_response: litellm.utils.ModelResponse, + print_verbose: Any, + logging_obj: Any, + optional_params: dict, + litellm_params: dict, + logger_fn=None, + ) -> litellm.utils.ModelResponse: + """ + Synchronous completion for Google Code Assist. + """ + try: + from litellm.llms.gemini.common_utils import get_gemini_oauth_token + + # 1. Get OAuth data + gemini_auth_data = get_gemini_oauth_token() + if not gemini_auth_data: + raise GoogleCodeAssistError( + status_code=401, + message="Missing Gemini OAuth token. Run 'litellm-proxy gemini login' or set GEMINI_OAUTH_TOKEN.", + ) + + token = gemini_auth_data.get("token") + initial_project_id = gemini_auth_data.get("project_id") + + client = _get_httpx_client() + + # 2. MANDATORY HANDSHAKE: loadCodeAssist + final_project_id = self._handle_handshake(client, token, initial_project_id) + litellm_params["google_code_assist_project"] = final_project_id + + # 3. Transform request + data = self.config.transform_request( + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + ) + + # 4. Call Completion API + url = "https://cloudcode-pa.googleapis.com/v1internal:generateContent" + headers = self._get_headers(token) + + response = client.post( + url=url, + headers=headers, + json=data, + ) + response.raise_for_status() + + # 5. Transform response + return self.config.transform_response( + model=model, + raw_response=response, + model_response=model_response, + logging_obj=logging_obj, + request_data=data, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + encoding=None, + ) + + except Exception as e: + raise self._handle_error(e) + + async def acompletion( + self, + model: str, + messages: list, + model_response: litellm.utils.ModelResponse, + print_verbose: Any, + logging_obj: Any, + optional_params: dict, + litellm_params: dict, + logger_fn=None, + ) -> litellm.utils.ModelResponse: + """ + Asynchronous completion for Google Code Assist. + """ + try: + from litellm.llms.gemini.common_utils import get_gemini_oauth_token + + gemini_auth_data = get_gemini_oauth_token() + if not gemini_auth_data: + raise GoogleCodeAssistError( + status_code=401, + message="Missing Gemini OAuth token. Run 'litellm-proxy gemini login' or set GEMINI_OAUTH_TOKEN.", + ) + + token = gemini_auth_data.get("token") + initial_project_id = gemini_auth_data.get("project_id") + + async_handler = AsyncHTTPHandler() + + final_project_id = await self._ahandle_handshake( + async_handler, token, initial_project_id + ) + litellm_params["google_code_assist_project"] = final_project_id + + data = self.config.transform_request( + model, messages, optional_params, litellm_params + ) + url = "https://cloudcode-pa.googleapis.com/v1internal:generateContent" + headers = self._get_headers(token) + + response = await async_handler.post(url=url, headers=headers, json=data) + response.raise_for_status() + + return self.config.transform_response( + model=model, + raw_response=response, + model_response=model_response, + logging_obj=logging_obj, + request_data=data, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + encoding=None, + ) + + except Exception as e: + raise self._handle_error(e) + + def _handle_handshake( + self, client, token: str, initial_project_id: Optional[str] + ) -> Optional[str]: + """Performs the loadCodeAssist handshake to establish session context.""" + load_url = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist" + load_headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "User-Agent": "GeminiCLI/litellm", + } + load_payload = { + "metadata": { + "ideType": "IDE_UNSPECIFIED", + "platform": "PLATFORM_UNSPECIFIED", + "pluginType": "GEMINI", + } + } + if initial_project_id: + load_payload["cloudaicompanionProject"] = initial_project_id + load_payload["metadata"]["duetProject"] = initial_project_id + + try: + load_resp = client.post(load_url, headers=load_headers, json=load_payload) + load_resp.raise_for_status() + return load_resp.json().get("cloudaicompanionProject") or initial_project_id + except Exception as e: + verbose_logger.debug(f"Gemini Code Assist handshake failed: {e}") + return initial_project_id + + async def _ahandle_handshake( + self, + async_handler: AsyncHTTPHandler, + token: str, + initial_project_id: Optional[str], + ) -> Optional[str]: + """Async version of loadCodeAssist handshake for non-blocking async calls.""" + load_url = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist" + load_headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "User-Agent": "GeminiCLI/litellm", + } + load_payload = { + "metadata": { + "ideType": "IDE_UNSPECIFIED", + "platform": "PLATFORM_UNSPECIFIED", + "pluginType": "GEMINI", + } + } + if initial_project_id: + load_payload["cloudaicompanionProject"] = initial_project_id + load_payload["metadata"]["duetProject"] = initial_project_id + + try: + load_resp = await async_handler.post( + url=load_url, headers=load_headers, json=load_payload + ) + load_resp.raise_for_status() + return load_resp.json().get("cloudaicompanionProject") or initial_project_id + except Exception as e: + verbose_logger.debug(f"Gemini Code Assist async handshake failed: {e}") + return initial_project_id + + def _get_headers(self, token: str) -> dict: + """Returns standard headers for Code Assist API calls.""" + return { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "User-Agent": "GeminiCLI/litellm", + "X-Goog-Api-Client": "litellm-google-code-assist", + } + + def _handle_error(self, e: Exception) -> Exception: + """Centralized error mapping for Code Assist.""" + if isinstance(e, httpx.HTTPStatusError): + return GoogleCodeAssistError( + status_code=e.response.status_code, message=e.response.text + ) + if isinstance(e, GoogleCodeAssistError): + return e + return GoogleCodeAssistError(status_code=500, message=str(e)) diff --git a/litellm/llms/google_code_assist/transformation.py b/litellm/llms/google_code_assist/transformation.py new file mode 100644 index 00000000000..5de7dd725ed --- /dev/null +++ b/litellm/llms/google_code_assist/transformation.py @@ -0,0 +1,195 @@ +import json +import uuid +import copy +import httpx +from typing import Any, List, Optional + +from litellm.llms.base_llm.chat.transformation import BaseLLMException +from litellm.types.utils import ModelResponse +from ..vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexGeminiConfig + + +class GoogleCodeAssistError(BaseLLMException): + """ + Exception raised for errors in the Google Code Assist API. + """ + + def __init__(self, status_code, message): + super().__init__(status_code=status_code, message=message) + + +class ParsedJSONResponseAdapter: + """ + Adapter that provides the subset of httpx.Response surface used by + VertexGeminiConfig.transform_response. + """ + + def __init__(self, json_data: dict): + self._json = json_data + self.status_code = 200 + self.text = json.dumps(json_data) + self.headers = httpx.Headers({"content-type": "application/json"}) + + def json(self): + return self._json + + +class GoogleCodeAssistConfig(VertexGeminiConfig): + """ + Reference: https://cloud.google.com/gemini/docs/api/reference/rest/v1internal/projects.locations.codeAssist/generateContent + + The class `GoogleCodeAssistConfig` provides configuration for the Google Code Assist API. + It inherits from `VertexGeminiConfig` to provide consistent parameter mapping. + + - `temperature` (float): This controls the degree of randomness in token selection. + - `max_output_tokens` (integer): This sets the limitation for the maximum amount of token in the text output. + - `top_p` (float): The tokens are selected from the most probable to the least probable until the sum of their probabilities equals the `top_p` value. + - `top_k` (integer): The value of `top_k` determines how many of the most probable tokens are considered in the selection. + - `stop_sequences` (List[str]): The set of character sequences that will stop output generation. + """ + + def __init__( + self, + temperature: Optional[float] = None, + max_output_tokens: Optional[int] = None, + top_p: Optional[float] = None, + top_k: Optional[int] = None, + stop_sequences: Optional[list] = None, + ) -> None: + super().__init__( + temperature=temperature, + max_output_tokens=max_output_tokens, + top_p=top_p, + top_k=top_k, + stop_sequences=stop_sequences, + ) + + def get_supported_openai_params(self, model: str) -> List[str]: + return super().get_supported_openai_params(model) + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + messages: list, + ) -> dict: + return super().map_openai_params( + non_default_params, optional_params, model, messages + ) + + def transform_request( + self, + model: str, + messages: list, + optional_params: dict, + litellm_params: dict, + ) -> dict: + """ + Transforms standard LiteLLM request to Code Assist API format. + Matches gemini-cli 'toGenerateContentRequest'. + """ + # 1. Map messages to Gemini format + from ..vertex_ai.gemini.transformation import ( + _gemini_convert_messages_with_history, + _transform_system_message, + ) + + # Create a copy to avoid mutating the original list + messages_copy = copy.deepcopy(messages) + + # Separate system instruction + system_instruction, filtered_messages = _transform_system_message( + supports_system_message=True, messages=messages_copy + ) + + # Convert the rest of messages + contents = _gemini_convert_messages_with_history( + messages=filtered_messages, model=model + ) + + # 2. Build vertex-style nested request + generation_config = {} + # Handle parameter mapping + base_params = self.map_openai_params( + {}, optional_params.copy(), model, messages + ) + + for key in ["temperature", "topP", "topK", "maxOutputTokens", "stopSequences"]: + if key in base_params: + generation_config[key] = base_params.pop(key) + + # Support thinkingConfig + if "thinkingConfig" in base_params: + generation_config["thinkingConfig"] = base_params.pop("thinkingConfig") + elif "include_thoughts" in base_params: + generation_config["thinkingConfig"] = { + "includeThoughts": base_params.pop("include_thoughts") + } + elif "thinkingConfig" in optional_params: + generation_config["thinkingConfig"] = optional_params["thinkingConfig"] + elif "include_thoughts" in optional_params: + generation_config["thinkingConfig"] = { + "includeThoughts": optional_params["include_thoughts"] + } + + vertex_request = { + "contents": contents, + "session_id": litellm_params.get("session_id", str(uuid.uuid4())), + } + + if system_instruction: + vertex_request["systemInstruction"] = { + "role": "system", + "parts": system_instruction["parts"], + } + + if generation_config: + vertex_request["generationConfig"] = generation_config + + # 3. Wrap in Code Assist envelope (matches verified gemini-cli structure) + user_prompt_id = f"litellm-{uuid.uuid4()}"[:13] + model_name = model.split("/")[-1] + + ca_request = { + "model": model_name, + "user_prompt_id": user_prompt_id, + "request": vertex_request, + } + + # Add project ID if available + if litellm_params.get("google_code_assist_project"): + ca_request["project"] = litellm_params["google_code_assist_project"] + + return ca_request + + def transform_response( + self, + model: str, + raw_response: Any, + model_response: ModelResponse, + logging_obj: Any, + request_data: dict, + messages: list, + optional_params: dict, + litellm_params: dict, + encoding: Any, + ) -> ModelResponse: + """ + Transforms Code Assist API response to standard LiteLLM format. + """ + data = raw_response.json() + # Code Assist wraps the response in a "response" key + gemini_response = data.get("response", data) + + return super().transform_response( + model=model, + raw_response=ParsedJSONResponseAdapter(gemini_response), + model_response=model_response, + logging_obj=logging_obj, + request_data=request_data, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + encoding=encoding, + ) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 5895a91f3aa..9bd2e1f45f7 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -338,6 +338,7 @@ def _get_gemini_url( model: str, stream: Optional[bool], gemini_api_key: Optional[str], + gemini_oauth_token: Optional[str] = None, ) -> Tuple[str, str]: from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( VertexGeminiConfig, @@ -348,32 +349,17 @@ def _get_gemini_url( "v1alpha" if VertexGeminiConfig._is_gemini_3_or_newer(model) else "v1beta" ) + endpoint = "generateContent" if mode == "chat": 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 - ) - else: - url = "https://generativelanguage.googleapis.com/{}/{}:{}?key={}".format( - api_version, _gemini_model_name, endpoint, gemini_api_key - ) elif mode == "embedding": endpoint = "embedContent" - url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format( - _gemini_model_name, endpoint, gemini_api_key - ) elif mode == "batch_embedding": endpoint = "batchEmbedContents" - url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format( - _gemini_model_name, endpoint, gemini_api_key - ) elif mode == "count_tokens": endpoint = "countTokens" - url = "https://generativelanguage.googleapis.com/v1beta/{}:{}?key={}".format( - _gemini_model_name, endpoint, gemini_api_key - ) elif mode == "image_generation": raise ValueError( "LiteLLM's `gemini/` route does not support image generation yet. Let us know if you need this feature by opening an issue at https://github.com/BerriAI/litellm/issues" @@ -381,6 +367,21 @@ def _get_gemini_url( else: raise ValueError(f"Unsupported mode: {mode}") + base_url = "https://generativelanguage.googleapis.com/{}/{}:{}".format( + api_version if mode == "chat" else "v1beta", _gemini_model_name, endpoint + ) + + params = [] + if gemini_api_key and not gemini_oauth_token: + params.append(f"key={gemini_api_key}") + if stream: + params.append("alt=sse") + + if params: + url = f"{base_url}?{'&'.join(params)}" + else: + url = base_url + return url, endpoint diff --git a/litellm/llms/vertex_ai/vertex_llm_base.py b/litellm/llms/vertex_ai/vertex_llm_base.py index 1a29ba82eac..2ffe40d6185 100644 --- a/litellm/llms/vertex_ai/vertex_llm_base.py +++ b/litellm/llms/vertex_ai/vertex_llm_base.py @@ -394,12 +394,30 @@ class VertexBase: "Model parameter is required for Gemini custom API base URLs" ) url = "{}/models/{}:{}".format(api_base, model, endpoint) + + gemini_auth_data = None if gemini_api_key is None: - raise ValueError( - "Missing gemini_api_key, please set `GEMINI_API_KEY`" + from litellm.llms.gemini.common_utils import ( + get_gemini_oauth_token, ) - if gemini_api_key is not None: + + gemini_auth_data = get_gemini_oauth_token() + gemini_oauth_token = ( + gemini_auth_data.get("token") if gemini_auth_data else None + ) + + if gemini_oauth_token: + auth_header = {"Authorization": f"Bearer {gemini_oauth_token}"} + if gemini_auth_data and gemini_auth_data.get("project_id"): + auth_header["x-goog-user-project"] = gemini_auth_data[ + "project_id" + ] + elif gemini_api_key is not None: auth_header = {"x-goog-api-key": gemini_api_key} # type: ignore[assignment] + else: + raise ValueError( + "Missing gemini_api_key. Please set `GEMINI_API_KEY` or `GEMINI_OAUTH_TOKEN`." + ) else: # For Vertex AI if use_psc_endpoint_format: @@ -453,13 +471,30 @@ class VertexBase: """ version: Optional[Literal["v1beta1", "v1"]] = None if custom_llm_provider == "gemini": + gemini_auth_data = None + if gemini_api_key is None: + from litellm.llms.gemini.common_utils import get_gemini_oauth_token + + gemini_auth_data = get_gemini_oauth_token() + gemini_oauth_token = ( + gemini_auth_data.get("token") if gemini_auth_data else None + ) + url, endpoint = _get_gemini_url( mode=mode, model=model, stream=stream, gemini_api_key=gemini_api_key, + gemini_oauth_token=gemini_oauth_token, ) - auth_header = None # this field is not used for gemin + if gemini_oauth_token: + auth_header = {"Authorization": f"Bearer {gemini_oauth_token}"} + if gemini_auth_data and gemini_auth_data.get("project_id"): + auth_header["x-goog-user-project"] = gemini_auth_data["project_id"] + else: + auth_header = ( + None # this field is not used for gemini when using api key + ) else: vertex_location = self.get_vertex_region( vertex_region=vertex_location, diff --git a/litellm/main.py b/litellm/main.py index 81319bc432f..41140e0cae8 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -101,6 +101,11 @@ from litellm.llms.cohere.common_utils import CohereModelInfo from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai_like.json_loader import JSONProviderRegistry +from litellm.llms.google_code_assist.chat import GoogleCodeAssistChat +from litellm.llms.gemini.fallback_handler import ( + run_gemini_acompletion_with_code_assist_fallback, + run_gemini_completion_with_code_assist_fallback, +) from litellm.llms.vertex_ai.common_utils import ( VertexAIModelRoute, get_vertex_ai_model_route, @@ -197,7 +202,9 @@ from .llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler 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.gemini.common_utils import get_api_key_from_env +from .llms.gemini.common_utils import ( + get_api_key_from_env, +) from .llms.groq.chat.handler import GroqChatCompletion from .llms.heroku.chat.transformation import HerokuChatConfig from .llms.huggingface.embedding.handler import HuggingFaceEmbedding @@ -3436,27 +3443,74 @@ def completion( # type: ignore # noqa: PLR0915 api_base = api_base or litellm.api_base or get_secret("GEMINI_API_BASE") new_params = safe_deep_copy(optional_params or {}) - response = vertex_chat_completion.completion( # type: ignore - model=model, - messages=messages, - model_response=model_response, - print_verbose=print_verbose, - optional_params=new_params, - litellm_params=litellm_params, # type: ignore - logger_fn=logger_fn, - encoding=_get_encoding(), - vertex_location=vertex_ai_location, - vertex_project=vertex_ai_project, - vertex_credentials=vertex_credentials, - gemini_api_key=gemini_api_key, - logging_obj=logging, - acompletion=acompletion, - timeout=timeout, - custom_llm_provider=custom_llm_provider, # type: ignore - client=client, - api_base=api_base, - extra_headers=headers, - ) + if acompletion is True: + response = run_gemini_acompletion_with_code_assist_fallback( + primary_call=vertex_chat_completion.completion( # type: ignore + model=model, + messages=messages, + model_response=model_response, + print_verbose=print_verbose, + optional_params=new_params, + litellm_params=litellm_params, # type: ignore + logger_fn=logger_fn, + encoding=_get_encoding(), + vertex_location=vertex_ai_location, + vertex_project=vertex_ai_project, + vertex_credentials=vertex_credentials, + gemini_api_key=gemini_api_key, + logging_obj=logging, + acompletion=True, + timeout=timeout, + custom_llm_provider=custom_llm_provider, # type: ignore + client=client, + api_base=api_base, + extra_headers=headers, + ), + fallback_kwargs={ + "model": model, + "messages": messages, + "model_response": model_response, + "print_verbose": print_verbose, + "optional_params": optional_params, + "litellm_params": litellm_params, # type: ignore + "logging_obj": logging, + "logger_fn": logger_fn, + }, + ) + else: + response = run_gemini_completion_with_code_assist_fallback( + primary_call=lambda: vertex_chat_completion.completion( # type: ignore + model=model, + messages=messages, + model_response=model_response, + print_verbose=print_verbose, + optional_params=new_params, + litellm_params=litellm_params, # type: ignore + logger_fn=logger_fn, + encoding=_get_encoding(), + vertex_location=vertex_ai_location, + vertex_project=vertex_ai_project, + vertex_credentials=vertex_credentials, + gemini_api_key=gemini_api_key, + logging_obj=logging, + acompletion=False, + timeout=timeout, + custom_llm_provider=custom_llm_provider, # type: ignore + client=client, + api_base=api_base, + extra_headers=headers, + ), + fallback_kwargs={ + "model": model, + "messages": messages, + "model_response": model_response, + "print_verbose": print_verbose, + "optional_params": optional_params, + "litellm_params": litellm_params, # type: ignore + "logging_obj": logging, + "logger_fn": logger_fn, + }, + ) elif custom_llm_provider == "vertex_ai": vertex_ai_project = ( @@ -3632,6 +3686,33 @@ def completion( # type: ignore # noqa: PLR0915 ) return response response = model_response + + elif custom_llm_provider == "google_code_assist": + google_code_assist_chat = GoogleCodeAssistChat() + if acompletion is True: + response = google_code_assist_chat.acompletion( + model=model, + messages=messages, + model_response=model_response, + print_verbose=print_verbose, + optional_params=optional_params, + litellm_params=litellm_params, # type: ignore + logging_obj=logging, + logger_fn=logger_fn, + ) + else: + model_response = google_code_assist_chat.completion( + model=model, + messages=messages, + model_response=model_response, + print_verbose=print_verbose, + optional_params=optional_params, + litellm_params=litellm_params, # type: ignore + logging_obj=logging, + logger_fn=logger_fn, + ) + response = model_response + elif custom_llm_provider == "predibase": tenant_id = ( optional_params.pop("tenant_id", None) @@ -7533,9 +7614,7 @@ def stream_chunk_builder( # noqa: PLR0915 # the final chunk. all_annotations: list = [] for ac in annotation_chunks: - all_annotations.extend( - ac["choices"][0]["delta"]["annotations"] - ) + all_annotations.extend(ac["choices"][0]["delta"]["annotations"]) response["choices"][0]["message"]["annotations"] = all_annotations audio_chunks = [ diff --git a/litellm/proxy/client/cli/README.md b/litellm/proxy/client/cli/README.md index 880b0b39720..b83e5b3cd63 100644 --- a/litellm/proxy/client/cli/README.md +++ b/litellm/proxy/client/cli/README.md @@ -29,6 +29,18 @@ litellm-proxy --version litellm-proxy -v ``` +## Gemini OAuth Login + +Authenticate locally for Gemini OAuth-based providers (for example `google_code_assist/*`): + +```bash +export GEMINI_OAUTH_CLIENT_ID="" +export GEMINI_OAUTH_CLIENT_SECRET="" +litellm-proxy gemini login +``` + +This starts a browser-based loopback OAuth flow and stores credentials for reuse. + ## Commands ### Models Management diff --git a/litellm/proxy/client/cli/commands/gemini.py b/litellm/proxy/client/cli/commands/gemini.py new file mode 100644 index 00000000000..f88e593fe53 --- /dev/null +++ b/litellm/proxy/client/cli/commands/gemini.py @@ -0,0 +1,19 @@ +import click +from litellm.llms.gemini.authenticator import GeminiAuthenticator + + +@click.group(name="gemini") +def gemini(): + """Gemini-specific management commands""" + pass + + +@gemini.command(name="login") +def login(): + """Login to Gemini using OAuth Device Flow (Loopback)""" + try: + auth = GeminiAuthenticator() + auth.get_token() + click.echo("✅ Successfully authenticated with Gemini.") + except Exception as e: + raise click.ClickException(f"Gemini authentication failed: {e}") diff --git a/litellm/proxy/client/cli/main.py b/litellm/proxy/client/cli/main.py index 744acf38382..36c62ca3722 100644 --- a/litellm/proxy/client/cli/main.py +++ b/litellm/proxy/client/cli/main.py @@ -12,6 +12,7 @@ from .commands.chat import chat from .commands.credentials import credentials from .commands.http import http from .commands.keys import keys +from .commands.gemini import gemini # local imports from .commands.models import models @@ -105,6 +106,7 @@ cli.add_command(chat) cli.add_command(http) # Add the keys command group cli.add_command(keys) +cli.add_command(gemini) # Add the teams command group cli.add_command(teams) # Add the users command group diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 38425c7ac4a..e7e95dc3149 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3151,6 +3151,7 @@ class LlmProviders(str, Enum): VERTEX_AI = "vertex_ai" VERTEX_AI_BETA = "vertex_ai_beta" GEMINI = "gemini" + GOOGLE_CODE_ASSIST = "google_code_assist" AI21 = "ai21" BASETEN = "baseten" BLACK_FOREST_LABS = "black_forest_labs" diff --git a/litellm/utils.py b/litellm/utils.py index 81d749ab821..71279e23f71 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7981,6 +7981,10 @@ class ProviderConfigManager: LlmProviders.COMETAPI: (lambda: litellm.CometAPIConfig(), False), LlmProviders.DATAROBOT: (lambda: litellm.DataRobotConfig(), False), LlmProviders.GEMINI: (lambda: litellm.GoogleAIStudioGeminiConfig(), False), + LlmProviders.GOOGLE_CODE_ASSIST: ( + lambda: ProviderConfigManager._get_google_code_assist_config(), + False, + ), LlmProviders.AI21: (lambda: litellm.AI21ChatConfig(), False), LlmProviders.AI21_CHAT: (lambda: litellm.AI21ChatConfig(), False), LlmProviders.AZURE_TEXT: (lambda: litellm.AzureOpenAITextConfig(), False), @@ -8100,6 +8104,15 @@ class ProviderConfigManager: return LangGraphConfig() + @staticmethod + def _get_google_code_assist_config() -> BaseConfig: + """Get Google Code Assist config.""" + from litellm.llms.google_code_assist.transformation import ( + GoogleCodeAssistConfig, + ) + + return GoogleCodeAssistConfig() + @staticmethod def get_provider_chat_config( # noqa: PLR0915 model: str, provider: LlmProviders diff --git a/poetry.lock b/poetry.lock index 591d0c270e4..be87e51f801 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "a2a-sdk" @@ -7,11 +7,11 @@ description = "A2A Python SDK" optional = false python-versions = ">=3.10" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385"}, {file = "a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d"}, ] +markers = {main = "python_version >= \"3.10\" and extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] google-api-core = ">=1.26.0" @@ -385,6 +385,7 @@ files = [ {file = "azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b"}, {file = "azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] requests = ">=2.21.0" @@ -405,6 +406,7 @@ files = [ {file = "azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651"}, {file = "azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] azure-core = ">=1.31.0" @@ -598,7 +600,7 @@ files = [ {file = "cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace"}, {file = "cachetools-6.2.2.tar.gz", hash = "sha256:8e6d266b25e539df852251cfd6f990b4bc3a141db73b939058d809ebd2590fc6"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "certifi" @@ -705,7 +707,7 @@ files = [ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] -markers = {main = "platform_python_implementation != \"PyPy\" or extra == \"proxy\"", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} +markers = {main = "(platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} @@ -1055,6 +1057,7 @@ files = [ {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] +markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\") or extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} @@ -1837,11 +1840,11 @@ description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.14\"" files = [ {file = "google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7"}, {file = "google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300"}, ] +markers = {main = "python_version >= \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1869,7 +1872,7 @@ files = [ {file = "google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c"}, {file = "google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8"}, ] -markers = {main = "(python_version >= \"3.10\" or extra == \"google\" or extra == \"extra-proxy\") and python_version < \"3.14\"", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} +markers = {main = "python_version < \"3.14\" and (extra == \"extra-proxy\" or extra == \"google\")", proxy-dev = "python_version >= \"3.10\" and python_version < \"3.14\""} [package.dependencies] google-auth = ">=2.14.1,<3.0.0" @@ -1906,7 +1909,7 @@ files = [ {file = "google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16"}, {file = "google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] cachetools = ">=2.0.0,<7.0" @@ -2078,11 +2081,11 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0.dev0" +proto-plus = ">=1.22.3,<2.0.0.dev0" +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [[package]] name = "google-cloud-resource-manager" @@ -2264,7 +2267,7 @@ files = [ {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\") or extra == \"google\" or extra == \"extra-proxy\""} [package.dependencies] grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} @@ -2673,11 +2676,11 @@ description = "Consume Server-Sent Event (SSE) messages with HTTPX." optional = false python-versions = ">=3.9" groups = ["main", "proxy-dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"}, {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"}, ] +markers = {main = "python_version >= \"3.10\" and (extra == \"proxy\" or extra == \"extra-proxy\")", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "huey" @@ -3042,7 +3045,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -3219,15 +3222,15 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.4.56" +version = "0.4.57" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ - {file = "litellm_proxy_extras-0.4.56-py3-none-any.whl", hash = "sha256:52dbe3b5358c790e77e12f1ec5ef8e7508b383c2aaf41299750b6fb400908ee7"}, - {file = "litellm_proxy_extras-0.4.56.tar.gz", hash = "sha256:63ad59baa0defccc5c929cfd933ee7e32a6614b0fc5fa0fc45a12d7608e33f08"}, + {file = "litellm_proxy_extras-0.4.57-py3-none-any.whl", hash = "sha256:04538223cd80318a72d70c6e10f701598e58c763368296a6503c674c92fbdb62"}, + {file = "litellm_proxy_extras-0.4.57.tar.gz", hash = "sha256:ef9b95dc42237614216833bd5d46ebf9dea1caa5ea14ea1a66d7f7842b224ec2"}, ] [[package]] @@ -3713,6 +3716,7 @@ files = [ {file = "msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1"}, {file = "msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] cryptography = ">=2.5,<49" @@ -3733,6 +3737,7 @@ files = [ {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, ] +markers = {main = "extra == \"proxy\" or extra == \"extra-proxy\""} [package.dependencies] msal = ">=1.29,<2" @@ -3983,6 +3988,7 @@ files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +markers = {main = "extra == \"extra-proxy\""} [[package]] name = "numpy" @@ -4105,7 +4111,7 @@ files = [ {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] importlib-metadata = ">=6.0,<8.8.0" @@ -4220,7 +4226,7 @@ files = [ {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4238,7 +4244,7 @@ files = [ {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, ] -markers = {main = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and extra == \"mlflow\""} [package.dependencies] opentelemetry-api = "1.39.1" @@ -4722,6 +4728,7 @@ files = [ {file = "prisma-0.11.0-py3-none-any.whl", hash = "sha256:22bb869e59a2968b99f3483bb417717273ffbc569fd1e9ceed95e5614cbaf53a"}, {file = "prisma-0.11.0.tar.gz", hash = "sha256:3f2f2fd2361e1ec5ff655f2a04c7860c2f2a5bc4c91f78ca9c5c6349735bf693"}, ] +markers = {main = "extra == \"extra-proxy\""} [package.dependencies] click = ">=7.1.2" @@ -4895,7 +4902,7 @@ files = [ {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] protobuf = ">=3.19.0,<7.0.0" @@ -4923,7 +4930,7 @@ files = [ {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\""} [[package]] name = "psutil" @@ -5083,7 +5090,7 @@ files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [[package]] name = "pyasn1-modules" @@ -5096,7 +5103,7 @@ files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" @@ -5124,7 +5131,7 @@ files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] -markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} +markers = {main = "implementation_name != \"PyPy\" and (platform_python_implementation != \"PyPy\" or extra == \"proxy\") and (python_version >= \"3.10\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"proxy\" or extra == \"extra-proxy\" or extra == \"mlflow\")", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"", proxy-dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""} [[package]] name = "pydantic" @@ -5347,6 +5354,7 @@ files = [ {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, ] +markers = {main = "(python_version <= \"3.13\" or extra == \"proxy\" or extra == \"extra-proxy\") and (extra == \"extra-proxy\" or extra == \"proxy\")"} [package.dependencies] cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} @@ -6290,7 +6298,7 @@ files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] -markers = {main = "extra == \"google\" or extra == \"extra-proxy\" or python_version >= \"3.10\"", proxy-dev = "python_version >= \"3.10\""} +markers = {main = "python_version >= \"3.10\" and (extra == \"extra-proxy\" or extra == \"google\" or extra == \"mlflow\") or extra == \"google\" or extra == \"extra-proxy\"", proxy-dev = "python_version >= \"3.10\""} [package.dependencies] pyasn1 = ">=0.1.3" @@ -6336,10 +6344,10 @@ files = [ ] [package.dependencies] -botocore = ">=1.37.4,<2.0a.0" +botocore = ">=1.37.4,<2.0a0" [package.extras] -crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a0)"] [[package]] name = "scikit-learn" @@ -6492,9 +6500,9 @@ tornado = ">=6.4.2,<7" urllib3 = ">=1.26,<3" [package.extras] -all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.00)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] +all = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)", "cohere (>=5.9.4,<6.0)", "dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\"", "google-cloud-aiplatform (>=1.45.0,<2)", "ipykernel (>=6.25.0,<7)", "llama-cpp-python (>=0.2.28,<0.2.86) ; python_version < \"3.13\"", "mistralai (>=0.0.12,<0.1.0)", "mypy (>=1.7.1,<2)", "ollama (>=0.1.7)", "pillow (>=10.2.0,<11.0.0) ; python_version < \"3.13\"", "pinecone[asyncio] (>=7.0.0,<8.0.0)", "psycopg[binary] (>=3.1.0,<4)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "qdrant-client (>=1.11.1,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "sentence-transformers (>=5.0.0) ; python_version < \"3.13\"", "tokenizers (>=0.19) ; python_version < \"3.13\"", "torch (>=2.6.0) ; python_version < \"3.13\"", "torchvision (>=0.17.0) ; python_version < \"3.13\"", "transformers (>=4.36.2) ; python_version < \"3.13\"", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] bedrock = ["boto3 (>=1.34.98,<2)", "botocore (>=1.34.110,<2)"] -cohere = ["cohere (>=5.9.4,<6.00)"] +cohere = ["cohere (>=5.9.4,<6.0)"] dev = ["dagger-io (>=0.1.1) ; python_version >= \"3.11\"", "ipykernel (>=6.25.0,<7)", "mypy (>=1.7.1,<2)", "pytest (>=8.2,<9.0)", "pytest-asyncio (>=0.24.0,<0.25)", "pytest-cov (>=4.1.0,<5)", "pytest-mock (>=3.12.0,<4)", "pytest-timeout", "pytest-xdist (>=3.5.0,<4)", "python-dotenv (>=1.0.0,<2)", "requests-mock (>=1.12.1,<2)", "ruff (>=0.11.2,<0.12)", "types-pyyaml (>=6.0.12.12,<7)", "types-requests (>=2.31.0,<3)"] docs = ["pydoc-markdown (>=4.8.2) ; python_version < \"3.12\""] fastembed = ["fastembed (>=0.3.0,<0.4) ; python_version < \"3.13\""] @@ -7222,6 +7230,7 @@ files = [ {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, ] +markers = {main = "extra == \"extra-proxy\""} [[package]] name = "tornado" @@ -7994,4 +8003,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "1f3bbf967451633fb6290ba88980bdf4fbf83420024b14e862d1da717d903684" +content-hash = "22cdc8096e0c8296827734393f5ab6e66088f397b5295caa1d277466d1fde1e8" diff --git a/tests/test_litellm/llms/google_code_assist/test_google_code_assist.py b/tests/test_litellm/llms/google_code_assist/test_google_code_assist.py new file mode 100644 index 00000000000..61bda83eb21 --- /dev/null +++ b/tests/test_litellm/llms/google_code_assist/test_google_code_assist.py @@ -0,0 +1,127 @@ +import pytest +from unittest.mock import MagicMock, patch +import httpx +import json +from litellm.llms.google_code_assist.chat import GoogleCodeAssistChat +from litellm.types.utils import ModelResponse + + +class TestGoogleCodeAssist: + @patch("litellm.llms.google_code_assist.chat._get_httpx_client") + @patch("litellm.llms.gemini.common_utils.get_gemini_oauth_token") + def test_completion_basic(self, mock_get_token, mock_get_client): + """ + Test basic completion with mocked handshake and API call. + """ + mock_get_token.return_value = { + "token": "test-token", + "project_id": "test-project", + } + + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + # Mock handshake response + handshake_data = {"cloudaicompanionProject": "final-project"} + mock_handshake_resp = httpx.Response( + status_code=200, + content=json.dumps(handshake_data).encode(), + request=httpx.Request("POST", "https://handshake"), + ) + + # Mock completion response + completion_data = { + "response": { + "candidates": [ + { + "content": { + "role": "model", + "parts": [{"text": "Hello world"}], + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 5, + "candidatesTokenCount": 2, + "totalTokenCount": 7, + }, + } + } + mock_completion_resp = httpx.Response( + status_code=200, + content=json.dumps(completion_data).encode(), + request=httpx.Request("POST", "https://completion"), + ) + + mock_client.post.side_effect = [mock_handshake_resp, mock_completion_resp] + + handler = GoogleCodeAssistChat() + response = handler.completion( + model="google_code_assist/gemini-1.5-flash", + messages=[{"role": "user", "content": "hi"}], + model_response=ModelResponse(), + print_verbose=False, + logging_obj=MagicMock(), + optional_params={}, + litellm_params={}, + ) + + assert response.choices[0].message.content == "Hello world" + assert response.usage.total_tokens == 7 + + @pytest.mark.asyncio + @patch("litellm.llms.google_code_assist.chat.AsyncHTTPHandler.post") + @patch("litellm.llms.gemini.common_utils.get_gemini_oauth_token") + async def test_acompletion_basic(self, mock_get_token, mock_async_post): + """ + Test async completion. + """ + mock_get_token.return_value = {"token": "test-token"} + + # Handshake (async) + handshake_data = {"cloudaicompanionProject": "final-project"} + mock_handshake_resp = httpx.Response( + status_code=200, + content=json.dumps(handshake_data).encode(), + request=httpx.Request("POST", "https://handshake"), + ) + + # Completion (async) + completion_data = { + "response": { + "candidates": [ + { + "content": { + "role": "model", + "parts": [{"text": "Async success"}], + }, + "finishReason": "STOP", + } + ], + "usageMetadata": { + "promptTokenCount": 5, + "candidatesTokenCount": 2, + "totalTokenCount": 7, + }, + } + } + mock_completion_resp = httpx.Response( + status_code=200, + content=json.dumps(completion_data).encode(), + request=httpx.Request("POST", "https://completion"), + ) + mock_async_post.side_effect = [mock_handshake_resp, mock_completion_resp] + + handler = GoogleCodeAssistChat() + response = await handler.acompletion( + model="google_code_assist/gemini-1.5-flash", + messages=[{"role": "user", "content": "hi"}], + model_response=ModelResponse(), + print_verbose=False, + logging_obj=MagicMock(), + optional_params={}, + litellm_params={}, + ) + + assert response.choices[0].message.content == "Async success"