BerriAI/litellm · error · HTTPException

Gemini managed-agent endpoints require a caller-supplied Gem

Error message

Gemini managed-agent endpoints require a caller-supplied Gemini api_key (via 'litellm_params_template'). Falling back to the proxy's GOOGLE_API_KEY / GEMINI_API_KEY env vars is only permitted for proxy admins.

What it means

Security guard on the Gemini managed-agent CRUD routes (POST/GET/DELETE /v1beta/agents...). These routes are reachable by any authenticated proxy key but are NOT routed through model_list, so the only credential sources are the per-request litellm_params_template or the proxy's GOOGLE_API_KEY/GEMINI_API_KEY env fallback. To stop ordinary users from creating/deleting agents in the operator's Gemini project with the operator's key, non-admin callers who did not supply their own api_key get HTTP 401.

Source

Thrown at litellm/proxy/google_endpoints/agents_endpoints.py:62

) -> None:
    """
    SECURITY: refuse to use the proxy's shared GOOGLE_API_KEY / GEMINI_API_KEY
    env fallback for non-admin callers on Gemini managed-agent CRUD endpoints.

    These endpoints are part of ``llm_api_routes`` so any authenticated LLM key
    can reach them, but unlike ``/v1beta/models/...:generateContent`` they are
    *not* routed through ``model_list`` — the only credential source is either
    the per-request ``litellm_params_template`` or the env var fallback. Without
    this guard, any ordinary proxy user could list, create, or delete managed
    agents inside the operator's Gemini project using the operator's key.

    Proxy admins (master key) keep the env-fallback convenience for ops use.
    """
    if _is_proxy_admin(user_api_key_dict):
        return
    if data.get("api_key"):
        return
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=(
            "Gemini managed-agent endpoints require a caller-supplied "
            "Gemini api_key (via 'litellm_params_template'). Falling back to "
            "the proxy's GOOGLE_API_KEY / GEMINI_API_KEY env vars is only "
            "permitted for proxy admins."
        ),
    )


def _merge_query_params_into_data(data: dict, request: Request) -> dict:
    """
    For GET/DELETE endpoints that cannot carry a JSON body, read a
    JSON-encoded ``litellm_params_template`` query parameter and merge its
    contents into *data*, without overwriting keys that are already present
    (e.g. path params like ``name`` or the fixed ``custom_llm_provider``).

    This mirrors the ``litellm_params_template`` handling in

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Include the caller's own Gemini key: on POST send litellm_params_template: {"api_key": "AIza..."} in the JSON body; on GET/DELETE pass ?litellm_params_template=%7B%22api_key%22%3A%22AIza...%22%7D (URL-encoded JSON)
  2. Or perform the operation with the master key / a PROXY_ADMIN role key, which may use the env fallback
  3. If every user should use their own Google credentials, configure per-key/per-team litellm_params so api_key is injected into data before the guard runs
  4. Never pass the key as a bare ?api_key= query parameter — it is both unsupported and leaks into access logs

Example fix

# before (non-admin key, no credential)
curl http://localhost:4000/v1beta/agents -H 'Authorization: Bearer sk-user-key'

# after
curl -X POST http://localhost:4000/v1beta/agents \
  -H 'Authorization: Bearer sk-user-key' \
  -H 'Content-Type: application/json' \
  -d '{"litellm_params_template": {"api_key": "AIza..."}, ...agent payload...}'
Defensive patterns

Strategy: validation

Validate before calling

def gemini_agent_headers_and_body(api_key: str, payload: dict) -> dict:
    """Non-admin calls must embed a Gemini key in litellm_params_template."""
    payload["litellm_params_template"] = {"api_key": api_key}
    return payload

# GET/DELETE: encode the template in the query string instead
# from urllib.parse import urlencode
# qs = urlencode({"litellm_params_template": json.dumps({"api_key": api_key})})

Type guard

def agent_call_is_authorized(is_admin: bool, data: dict) -> bool:
    """Non-admin needs data['api_key'] set (merged from litellm_params_template)."""
    return is_admin or bool(data.get("api_key"))

Try / catch

try:
    r = requests.post(f"{proxy}/v1beta/agents", json=body, headers=h, timeout=30)
    r.raise_for_status()
except requests.HTTPError as e:
    if e.response is not None and e.response.status_code == 401 and "litellm_params_template" in e.response.text:
        raise PermissionError("Supply the caller's Gemini api_key via litellm_params_template") from e
    raise

Prevention

When it happens

Trigger: A non-admin virtual key calling POST /v1beta/agents (or GET/DELETE variants) without an api_key inside the litellm_params_template body field, or for GET/DELETE without the JSON-encoded litellm_params_template query parameter containing api_key.

Common situations: Multi-tenant proxies where users were previously calling /v1beta/models/...:generateContent successfully via the shared env key and assume agent endpoints work the same way; passing ?api_key=... as a flat query param (unsupported and insecure) instead of the template; admin testing with a non-master key by mistake.

Understand the failure class

Background: "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries — this error's family across 16 libraries.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/962dd7ccfc26d938. Report an issue: GitHub.