Skip to content

[Bug]: GoogleBatchEmbeddings.batch_embeddings() doesn't include auth_header when using custom api_base for Gemini embeddings #18000

@qdrddr

Description

@qdrddr

What happened?

A bug happened!

Bug Summary

When using the LiteLLM SDK's embedding() function with Gemini models and a custom api_base parameter (e.g., for Cloudflare AI Gateway routing), the authentication header is not included in the HTTP request, causing authentication failures.

================================================================================
BUG #1: Missing Authorization Header (x-goog-api-key)

EXPECTED BEHAVIOR:
When api_key is provided, request should include:
headers = {
'Content-Type': 'application/json; charset=utf-8',
'x-goog-api-key': 'AIzaSyAS0bSJ2TaEvFte...' ← SHOULD BE SENT
}

ACTUAL BEHAVIOR:
Request only includes:
headers = {
'Content-Type': 'application/json; charset=utf-8'
# ← x-goog-api-key is MISSING!
}

Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm._turn_on_debug()'.

✅ BUG CONFIRMED: Request failed with authentication error
Error: litellm.BadRequestError: GeminiException BadRequestError - {
"error": {
"code": 403,
"message": "Method doesn't allow unregistered callers (...

================================================================================
BUG #2: Ignored Extra Headers Parameter

EXPECTED BEHAVIOR:
When extra_headers is provided, request should include:
headers = {
'Content-Type': 'application/json; charset=utf-8',
'X-Custom-Header': 'test-value', ← SHOULD BE SENT
'Authorization': 'Bearer token' ← SHOULD BE SENT
}

ACTUAL BEHAVIOR:
extra_headers parameter is completely ignored
Request only includes Content-Type header

Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm._turn_on_debug()'.

✅ BUG CONFIRMED: extra_headers were ignored
If extra_headers worked, Authorization header would be sent
Error: litellm.BadRequestError: GeminiException BadRequestError - {
"error": {
"code": 403,
"message": "Method doesn't allow unregistered callers (...

================================================================================
ROOT CAUSE

File: litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py
Method: GoogleBatchEmbeddings.batch_embeddings()

Lines 60-62 create headers WITHOUT including auth_header:
headers = {'Content-Type': 'application/json; charset=utf-8'}

← auth_header is retrieved but NEVER added!

← extra_headers parameter is NEVER checked!

================================================================================
FIX (2 lines)

headers = {'Content-Type': 'application/json; charset=utf-8'}
if auth_header:
headers.update(auth_header)

Environment

  • LiteLLM Version: 1.80.10 (latest as of 2025-12-15)
  • Python Version: 3.13
  • Provider: Google Gemini (gemini/gemini-embedding-001)
  • Use Case: Routing through Cloudflare AI Gateway (CFAIG)

Steps to Reproduce

from litellm import embedding
import os

# Cloudflare AI Gateway URL (noauth mode)
cfaig_url = "https://gateway.ai.cloudflare.com/v1/{account_id}/noauth/google-ai-studio/v1beta"

response = embedding(
    model="gemini/gemini-embedding-001",
    input=["What is the meaning of life?"],
    api_key=os.getenv("GEMINI_API_KEY"),
    api_base=cfaig_url,
)

Expected Behavior

The request should include the x-goog-api-key header with the Gemini API key, similar to how the completion() function handles Gemini models with custom api_base.

Actual Behavior

The request is sent without any authentication header, resulting in:

httpx.HTTPStatusError: Client error '401 Unauthorized'
litellm.exceptions.AuthenticationError: GeminiException - {"success":false,"result":[],"messages":[],"error":[{"code":2009,"message":"Unauthorized"}]}

Root Cause

File: litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py

Method: GoogleBatchEmbeddings.batch_embeddings()

The bug is in the batch_embeddings() method:

  1. Line 26-38: The _get_token_and_url() method correctly retrieves auth_header containing the authentication credentials
  2. Line 60-62: Creates a headers dict but never includes the auth_header
  3. Line 87-91: Sends the HTTP request with incomplete headers (missing authentication)

Problematic Code:

# Line 26-38: Gets auth_header correctly
auth_header, url = self._get_token_and_url(
    model=model,
    auth_header=_auth_header,
    gemini_api_key=api_key,
    ...
    api_base=api_base,
    ...
)

# Line 60-62: Creates headers WITHOUT including auth_header ❌
headers = {
    "Content-Type": "application/json; charset=utf-8",
}
# ❌ BUG: auth_header is never added to headers!

# Line 87-91: Sends request with incomplete headers
response = sync_handler.post(
    url=url,
    headers=headers,  # Missing authentication!
    data=json.dumps(request_data),
)

Proposed Fix

Add the auth_header to the headers dict before sending the request:

# Line 60-66: Include auth_header in headers ✅
headers = {
    "Content-Type": "application/json; charset=utf-8",
}

# ✅ FIX: Add auth_header if it exists
if auth_header:
    headers.update(auth_header)

Additional Context

  • The completion() function handles this correctly for Gemini models with custom api_base
  • The _check_custom_proxy() method (line 46) correctly sets auth_header = {"x-goog-api-key": gemini_api_key} when api_base is provided
  • This bug affects all use cases where Gemini embeddings are routed through a proxy/gateway with custom api_base

Impact

This bug prevents users from:

  • Routing Gemini embeddings through Cloudflare AI Gateway for caching/analytics
  • Using any custom proxy/gateway for Gemini embeddings
  • Benefiting from edge caching and cost optimization features

Related Issues

This appears to be related to the fix in PR #16085 which fixed direct Gemini API access, but the fix didn't cover the case when api_base is set.

Relevant log output

================================================================================
BUG #1: Missing Authorization Header (x-goog-api-key)
================================================================================

EXPECTED BEHAVIOR:
  When api_key is provided, request should include:
    headers = {
      'Content-Type': 'application/json; charset=utf-8',
      'x-goog-api-key': 'AIzaSyAS0bSJ2TaEvFte...'  ← SHOULD BE SENT
    }

ACTUAL BEHAVIOR:
  Request only includes:
    headers = {
      'Content-Type': 'application/json; charset=utf-8'
      # ← x-goog-api-key is MISSING!
    }


Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm._turn_on_debug()'.

✅ BUG CONFIRMED: Request failed with authentication error
   Error: litellm.BadRequestError: GeminiException BadRequestError - {
  "error": {
    "code": 403,
    "message": "Method doesn't allow unregistered callers (...

================================================================================
BUG #2: Ignored Extra Headers Parameter
================================================================================

EXPECTED BEHAVIOR:
  When extra_headers is provided, request should include:
    headers = {
      'Content-Type': 'application/json; charset=utf-8',
      'X-Custom-Header': 'test-value',  ← SHOULD BE SENT
      'Authorization': 'Bearer token'   ← SHOULD BE SENT
    }

ACTUAL BEHAVIOR:
  extra_headers parameter is completely ignored
  Request only includes Content-Type header


Give Feedback / Get Help: https://github.com/BerriAI/litellm/issues/new
LiteLLM.Info: If you need to debug this error, use `litellm._turn_on_debug()'.

✅ BUG CONFIRMED: extra_headers were ignored
   If extra_headers worked, Authorization header would be sent
   Error: litellm.BadRequestError: GeminiException BadRequestError - {
  "error": {
    "code": 403,
    "message": "Method doesn't allow unregistered callers (...

================================================================================
ROOT CAUSE
================================================================================
File: litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py
Method: GoogleBatchEmbeddings.batch_embeddings()

Lines 60-62 create headers WITHOUT including auth_header:
  headers = {'Content-Type': 'application/json; charset=utf-8'}
  # ← auth_header is retrieved but NEVER added!
  # ← extra_headers parameter is NEVER checked!

================================================================================
FIX (2 lines)
================================================================================
headers = {'Content-Type': 'application/json; charset=utf-8'}
if auth_header:
    headers.update(auth_header)
================================================================================

Are you a ML Ops Team?

No

What LiteLLM version are you on ?

v1.80.10

Twitter / LinkedIn details

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions