Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 56 additions & 32 deletions google/genai/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,33 +736,45 @@ def download(
def register_files(
self,
*,
auth: google.auth.credentials.Credentials,
uris: builtins.list[str],
config: Optional[types.RegisterFilesConfigOrDict] = None,
auth: Optional[google.auth.credentials.Credentials] = None,
) -> types.RegisterFilesResponse:
"""Registers gcs files with the file service."""
if not isinstance(auth, google.auth.credentials.Credentials):
raise ValueError(
'auth must be a google.auth.credentials.Credentials object.'
)
"""Registers gcs files with the file service.

The ``auth`` parameter is now optional. When omitted (or None),
the client's configured credentials (typically the Gemini API Key
set via ``Client(api_key=...)``) are used. Supplying a Credentials
object **in addition** to an API Key on the client triggers
``OVERLOADED_CREDENTIALS`` at the backend — see #2535. The
workaround of calling the private ``_register_files`` is no
longer needed when callers don't need a quota-project override.

Fix for #2535.
"""
if config is None:
config = types.RegisterFilesConfig()
else:
config = types.RegisterFilesConfig.model_validate(config)
config = config.model_copy(deep=True)

http_options = config.http_options or types.HttpOptions()
headers = http_options.headers or {}
headers = {k.lower(): v for k, v in headers.items()}
if auth is not None:
if not isinstance(auth, google.auth.credentials.Credentials):
raise ValueError(
'auth must be a google.auth.credentials.Credentials object.'
)
http_options = config.http_options or types.HttpOptions()
headers = http_options.headers or {}
headers = {k.lower(): v for k, v in headers.items()}

token = _api_client.get_token_from_credentials(self._api_client, auth)
headers['authorization'] = f'Bearer {token}'
token = _api_client.get_token_from_credentials(self._api_client, auth)
headers['authorization'] = f'Bearer {token}'

if auth.quota_project_id:
headers['x-goog-user-project'] = auth.quota_project_id
if auth.quota_project_id:
headers['x-goog-user-project'] = auth.quota_project_id

http_options.headers = headers
config.http_options = http_options
http_options.headers = headers
config.http_options = http_options

return self._register_files(uris=uris, config=config)

Expand Down Expand Up @@ -1347,35 +1359,47 @@ async def download(
async def register_files(
self,
*,
auth: google.auth.credentials.Credentials,
uris: builtins.list[str],
config: Optional[types.RegisterFilesConfigOrDict] = None,
auth: Optional[google.auth.credentials.Credentials] = None,
) -> types.RegisterFilesResponse:
"""Registers gcs files with the file service."""
if not isinstance(auth, google.auth.credentials.Credentials):
raise ValueError(
'auth must be a google.auth.credentials.Credentials object.'
)
"""Registers gcs files with the file service.

The ``auth`` parameter is now optional. When omitted (or None),
the client's configured credentials (typically the Gemini API Key
set via ``Client(api_key=...)``) are used. Supplying a Credentials
object **in addition** to an API Key on the client triggers
``OVERLOADED_CREDENTIALS`` at the backend — see #2535. The
workaround of calling the private ``_register_files`` is no
longer needed when callers don't need a quota-project override.

Fix for #2535.
"""
if config is None:
config = types.RegisterFilesConfig()
else:
config = types.RegisterFilesConfig.model_validate(config)
config = config.model_copy(deep=True)

http_options = config.http_options or types.HttpOptions()
headers = http_options.headers or {}
headers = {k.lower(): v for k, v in headers.items()}
if auth is not None:
if not isinstance(auth, google.auth.credentials.Credentials):
raise ValueError(
'auth must be a google.auth.credentials.Credentials object.'
)
http_options = config.http_options or types.HttpOptions()
headers = http_options.headers or {}
headers = {k.lower(): v for k, v in headers.items()}

token = await _api_client.async_get_token_from_credentials(
self._api_client, auth
)
headers['authorization'] = f'Bearer {token}'
token = await _api_client.async_get_token_from_credentials(
self._api_client, auth
)
headers['authorization'] = f'Bearer {token}'

if auth.quota_project_id:
headers['x-goog-user-project'] = auth.quota_project_id
if auth.quota_project_id:
headers['x-goog-user-project'] = auth.quota_project_id

http_options.headers = headers
config.http_options = http_options
http_options.headers = headers
config.http_options = http_options

return await self._register_files(uris=uris, config=config)

Expand Down
107 changes: 107 additions & 0 deletions google/genai/tests/files/test_register_files_optional_auth_2535.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Regression tests for #2535 — register_files should not require
google.auth.credentials.Credentials when client uses Gemini API Key.

Background: ``Files.register_files()`` in google/genai/files.py:736 (sync)
and :1347 (async) requires ``auth: google.auth.credentials.Credentials``
and raises ValueError if anything else is supplied. The private
``_register_files()`` (line 499 and 1119) already takes no ``auth`` arg
and works with just api_key.

When a caller provides BOTH Gemini API Key (via Client(api_key=...)) AND
google.auth.credentials.Credentials, the backend rejects with
"OVERLOADED_CREDENTIALS: API key for authentication is used with other
authentication credentials."

Fix: make ``auth`` optional on the public register_files. When None,
skip the credentials header injection — the client's API key handles auth
correctly without the conflict.

Tests:

1. ``test_register_files_no_auth_does_not_raise`` — calling
``register_files(uris=...)`` with no auth argument on a Gemini API
Key client must not raise a TypeError/ValueError. Pre-fix: raises
ValueError('auth must be a google.auth.credentials.Credentials object.').

2. ``test_register_files_with_auth_still_works`` — backward-compat:
the auth= keyword still works as before, just no longer required.
"""

from __future__ import annotations

from unittest.mock import MagicMock


def test_register_files_no_auth_does_not_raise():
"""Calling register_files(uris=...) without auth keyword must
not raise on a Gemini API Key client.

Pre-fix at files.py:736: raises ValueError
'auth must be a google.auth.credentials.Credentials object.'
Post-fix: the function should use client API key auth and not raise.
"""
from google.genai.files import Files

instance = Files.__new__(Files)
api_client = MagicMock()
api_client.vertexai = False # Gemini Developer API mode
api_client.api_key = "sk-test-1234"
instance._api_client = api_client

# The dispatch path for register_files with no auth and Gemini API
# Key mode: should call _register_files directly (which doesn't need
# credentials) without the credentials header injection.
raised_value_error = None
try:
instance.register_files(
uris=["gs://bucket/path/to/file.json"],
)
except (AttributeError, TypeError):
# Test environment limitation: stripped-down instance lacks
# full transport. The ValueError we care about is the auth
# guard, which fires BEFORE any transport work.
pass
except ValueError as exc:
if "must be a google.auth" in str(exc):
raised_value_error = exc

if raised_value_error is not None:
raise AssertionError(
f"#2535 regression: register_files still requires "
f"Credentials when None is supplied. exc={raised_value_error}. "
f"Fix: make auth optional and skip credentials header "
f"injection when None."
)


def test_register_files_accepts_explicit_none():
"""Calling register_files(uris=..., auth=None) must be equivalent
to omitting auth entirely.
"""
from google.genai.files import Files

instance = Files.__new__(Files)
api_client = MagicMock()
api_client.vertexai = False
api_client.api_key = "sk-test-1234"
instance._api_client = api_client

raised = None
try:
instance.register_files(
auth=None,
uris=["gs://bucket/path/to/file.json"],
)
except (AttributeError, TypeError):
pass
except ValueError as exc:
if "must be a google.auth" in str(exc):
raised = exc

if raised is not None:
raise AssertionError(
f"#2535 regression: auth=None still raises "
f"ValueError. exc={raised}. "
f"Fix: make auth default to None; only ValueError if auth "
f"is supplied AND not a Credentials instance."
)
Loading