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
51 changes: 51 additions & 0 deletions docs/gemini-cli-manual-code-assist-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Manual Google Cloud Code Assist Setup for Gemini CLI

> **⚠️ PLACEHOLDER - To Be Completed by Mirrowel**
>
> This document will contain step-by-step instructions for manually enabling the Code Assist API in Google Cloud Console.

## Overview

Some Gemini accounts require manual creation of a Google Cloud Console project with the Code Assist API enabled. This guide will walk you through the process.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation file is marked as a placeholder with incomplete steps. However, the code in gemini_auth_base.py:872 directs users to read this file during manual setup.

Consider completing the documentation before merging, or remove this file from the PR and add it in a follow-up once the steps are documented.


Comment on lines +1 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Placeholder document shipped with no content and referenced at runtime

This documentation file is incomplete — every section contains a [Instructions to be added...] or [To be added...] placeholder. However, it is actively referenced in the interactive prompt shown to users when auto-discovery fails in _prompt_for_manual_setup (in gemini_auth_base.py):

"[bold]Please follow the manual setup instructions:[/bold]\n"
"[cyan]docs/gemini-cli-manual-code-assist-setup.md[/cyan]\n\n"
"After following the instructions, enter your Project ID below."

A user whose auto-discovery fails will be directed to this file for help and find it completely empty. Either the document should be completed before merging, or the prompt should reference the ⚠️ PLACEHOLDER note more clearly so the user isn't left stranded.

## Prerequisites

- A Google account with Gemini CLI access
- Access to Google Cloud Console

## Steps

### Step 1: Create a Google Cloud Project

[Instructions to be added...]

### Step 2: Enable the Code Assist API

[Instructions to be added...]

### Step 3: Configure API Permissions

[Instructions to be added...]

### Step 4: Obtain Your Project ID

[Instructions to be added...]

## Troubleshooting

### Common Issues

[To be added...]

### Error Messages

[To be added...]

## Additional Resources

- [Google Cloud Console](https://console.cloud.google.com/)
- [Gemini CLI Documentation](https://goo.gle/gemini-cli-auth-docs)

---

**Note:** This document is a placeholder. Please check back later for complete instructions.
170 changes: 169 additions & 1 deletion src/rotator_library/providers/gemini_auth_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
# Service Usage API for checking enabled APIs
SERVICE_USAGE_API = "https://serviceusage.googleapis.com/v1"

# Known Gemini CLI tiers for manual selection
KNOWN_GEMINI_TIERS = [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KNOWN_GEMINI_TIERS list doesn't include "unknown", but the code uses "unknown" as a fallback (around line 912). Consider either adding "unknown" to the list or documenting it as a special sentinel value.

"free-tier",
"legacy-tier",
"standard",
"premium",
"enterprise",
]

lib_logger = logging.getLogger("rotator_library")

# Headers for Gemini CLI auth/discovery calls (loadCodeAssist, onboardUser, etc.)
Expand Down Expand Up @@ -785,14 +794,173 @@ async def _discover_project_id(
except httpx.RequestError as e:
lib_logger.error(f"Network error while listing GCP projects: {e}")

# Auto-discovery failed - prompt for manual input (interactive mode only)
lib_logger.info(
"Auto-discovery failed. Checking if manual input is available..."
)

# Try to get project_id and tier from user input
try:
manual_result = await self._prompt_for_manual_setup()
if manual_result and manual_result.get("project_id"):
project_id = manual_result["project_id"]
tier = manual_result.get("tier")

lib_logger.info(
f"Using manually provided project ID: {project_id}, tier: {tier}"
)

# Cache the values
self.project_id_cache[credential_path] = project_id
if tier:
self.project_tier_cache[credential_path] = tier

# Persist to credential file
await self._persist_project_metadata(credential_path, project_id, tier)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If _persist_project_metadata fails, the project_id is still returned on line 821. This could cause the user to be prompted again on the next run since the metadata wasn't saved.

Consider adding explicit warning logging if persistence fails.


return project_id
except Exception as e:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try-except block catches all Exception types, which is too broad and could hide bugs during development. Consider catching specific exceptions like EOFError, KeyboardInterrupt, or ValueError that are expected from user input.

lib_logger.debug(f"Manual input not available or failed: {e}")

# If we get here, manual input was not available or failed
raise ValueError(
"Could not auto-discover Gemini project ID. Possible causes:\n"
" 1. The cloudaicompanion.googleapis.com API is not enabled (enable it in Google Cloud Console)\n"
" 2. No active GCP projects exist for this account (create one in Google Cloud Console)\n"
" 3. Account lacks necessary permissions\n"
"To manually specify a project, set GEMINI_CLI_PROJECT_ID in your .env file."
"To manually specify a project, set GEMINI_CLI_PROJECT_ID in your .env file or provide it during interactive setup."
)

async def _prompt_for_manual_setup(self) -> Optional[Dict[str, str]]:
"""
Prompt user for manual project_id and tier selection.

This is called when auto-discovery fails during interactive credential setup.
Only works in interactive mode (stdin available).

Returns:
Dict with 'project_id' and optionally 'tier', or None if not available
"""
import sys

# Check if we're in interactive mode
if not sys.stdin.isatty():
lib_logger.debug("Not in interactive mode, skipping manual prompt")
return None

# Import Rich for console output (deferred import to avoid circular dependency)
try:
from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.panel import Panel
from rich.text import Text
except ImportError:
lib_logger.debug("Rich not available for manual prompt")
return None

console = Console()

# Display instructions
console.print(
Panel(
Text.from_markup(
"[bold yellow]Automatic project discovery failed.[/bold yellow]\n\n"
"This usually means:\n"
"1. The Cloud AI Companion API is not enabled in your Google Cloud project\n"
"2. You need to manually create a project\n\n"
"[bold]Please follow the manual setup instructions:[/bold]\n"
"[cyan]docs/gemini-cli-manual-code-assist-setup.md[/cyan]\n\n"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard-coded relative path assumes the user is running from the repository root. Consider using a path relative to the module using Path(__file__) for a more robust solution.

"After following the instructions, enter your Project ID below."
),
title="Manual Project Setup Required",
style="yellow",
)
)

# Prompt for project_id
project_id = Prompt.ask(
"[bold]Enter your Google Cloud Project ID[/bold] [dim](or press Enter to skip)[/dim]",
default="",
)

if not project_id.strip():
console.print(
"[dim]Skipping manual setup. You can set GEMINI_CLI_PROJECT_ID environment variable later.[/dim]"
)
return None

project_id = project_id.strip()

# Prompt for tier selection
console.print("\n[bold]Select your tier:[/bold]")
console.print("[dim]If unsure, select 'free-tier' or 'legacy-tier'[/dim]\n")

for i, tier in enumerate(KNOWN_GEMINI_TIERS, 1):
console.print(f" {i}. {tier}")
console.print(f" {len(KNOWN_GEMINI_TIERS) + 1}. Other (unknown)")

tier_choice = Prompt.ask(
"\n[bold]Enter tier number[/bold]",
default="1",
)

try:
choice_idx = int(tier_choice) - 1
if 0 <= choice_idx < len(KNOWN_GEMINI_TIERS):
selected_tier = KNOWN_GEMINI_TIERS[choice_idx]
else:
selected_tier = "unknown"
except ValueError:
selected_tier = "unknown"

console.print(
Panel(
f"Project ID: [cyan]{project_id}[/cyan]\n"
f"Tier: [green]{selected_tier}[/green]",
style="green",
title="Manual Setup Complete",
)
)

return {"project_id": project_id, "tier": selected_tier}

async def _persist_project_metadata(
self, credential_path: str, project_id: str, tier: Optional[str]
):
Comment on lines +927 to +929
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 TypeError: _persist_project_metadata() called with too many arguments

_persist_project_metadata is defined with the signature (self, credential_path: str, project_id: str, tier: Optional[str]) (3 parameters), but is called at lines 520–524 and 700–702 with 4 positional arguments — passing discovered_tier_full as a fourth argument:

await self._persist_project_metadata(
    credential_path,
    project_id,
    discovered_tier,
    discovered_tier_full,  # ← 4th arg, no matching parameter
)

This will raise TypeError: _persist_project_metadata() takes 4 positional arguments but 5 were given at runtime every time a credential successfully completes loadCodeAssist or onboarding discovery, completely breaking project metadata persistence for those credentials. The function signature needs to be updated to accept and store the tier_full value, e.g.:

async def _persist_project_metadata(
    self, credential_path: str, project_id: str, tier: Optional[str], tier_full: Optional[str] = None
):
    ...
    if tier_full:
        creds["_proxy_metadata"]["tier_full"] = tier_full

"""Persists project ID and tier to the credential file for faster future startups."""
# Skip persistence for env:// paths (environment-based credentials)
credential_index = self._parse_env_credential_path(credential_path)
if credential_index is not None:
lib_logger.debug(
f"Skipping project metadata persistence for env:// credential path: {credential_path}"
)
return

try:
# Load current credentials
with open(credential_path, "r") as f:
creds = json.load(f)

# Update metadata
if "_proxy_metadata" not in creds:
creds["_proxy_metadata"] = {}

creds["_proxy_metadata"]["project_id"] = project_id
if tier:
creds["_proxy_metadata"]["tier"] = tier

# Save back using the existing save method (handles atomic writes and permissions)
await self._save_credentials(credential_path, creds)

lib_logger.debug(
f"Persisted project_id and tier to credential file: {credential_path}"
)
except Exception as e:
lib_logger.warning(
f"Failed to persist project metadata to credential file: {e}"
)
# Non-fatal - just means slower startup next time

# =========================================================================
# CREDENTIAL MANAGEMENT OVERRIDES
# =========================================================================
Expand Down
Loading