feat(google): support service-account auth for TTS + Imagen, fix false availability

Google's TTS and Imagen tools advertised service-account auth
(GOOGLE_APPLICATION_CREDENTIALS) but only ever authenticated with an API
key string, so users with a service-account JSON could not use either tool.
google_tts.get_status() also over-reported availability when the JSON was
set, then failed at execute() — a silent-availability bug.

Separately, both hand-rolled _load_dotenv parsers kept inline comments as
values, so after `cp .env.example .env` every keyed tool falsely reported
"available" with no real credentials.

Changes:
- Add tools/google_credentials.py: lazy google-auth Bearer-token helper.
- google_tts: authenticate via Cloud TTS Bearer token when only a service
  account is configured; make get_status() honest.
- google_imagen: route service-account auth to Vertex AI
  ({location}-aiplatform.googleapis.com) with project/location resolution,
  alongside the existing AI Studio API-key path.
- Fix both _load_dotenv parsers to strip inline comments (quote-aware).
- Add google-auth to requirements; document the new env vars in .env.example.
- .gitignore: never commit GCP service-account key files.

Verified locally with a real service account: TTS produced a valid MP3 and
Imagen produced a valid 1408x768 PNG via Vertex AI. Existing test suite
passes (2 unrelated pre-existing failures only).

Closes #131

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shubham
2026-06-22 17:53:10 +05:30
parent 9066dcb2e3
commit 806f7ee1ac
8 changed files with 216 additions and 31 deletions
+38 -11
View File
@@ -24,6 +24,7 @@ from tools.base_tool import (
ToolStatus,
ToolTier,
)
from tools.google_credentials import get_access_token, service_account_configured
class GoogleTTS(BaseTool):
@@ -39,9 +40,11 @@ class GoogleTTS(BaseTool):
dependencies = []
install_instructions = (
"Set GOOGLE_API_KEY to your Google Cloud API key with Text-to-Speech enabled.\n"
"Auth option A — API key: set GOOGLE_API_KEY (or GEMINI_API_KEY) to a\n"
" Google Cloud API key with Text-to-Speech enabled.\n"
" Enable the API at https://console.cloud.google.com/apis/library/texttospeech.googleapis.com\n"
" Or use GOOGLE_APPLICATION_CREDENTIALS for service account auth."
"Auth option B — service account: set GOOGLE_APPLICATION_CREDENTIALS to the\n"
" path of a service-account JSON key (needs the 'google-auth' package)."
)
fallback = "openai_tts"
fallback_tools = ["openai_tts", "elevenlabs_tts", "piper_tts"]
@@ -130,7 +133,9 @@ class GoogleTTS(BaseTool):
return os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
def get_status(self) -> ToolStatus:
if self._get_api_key() or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"):
# Available via either an API key or a service-account JSON. Both paths
# are honoured by execute() — so this no longer over-reports.
if self._get_api_key() or service_account_configured():
return ToolStatus.AVAILABLE
return ToolStatus.UNAVAILABLE
@@ -159,16 +164,26 @@ class GoogleTTS(BaseTool):
return round(char_count * rate_per_char, 4)
def execute(self, inputs: dict[str, Any]) -> ToolResult:
# Prefer an API key (cheapest path); otherwise mint a Bearer token from
# the service-account JSON. This is what makes
# GOOGLE_APPLICATION_CREDENTIALS actually work for TTS.
api_key = self._get_api_key()
bearer_token: str | None = None
if not api_key:
return ToolResult(
success=False,
error="No Google API key found. " + self.install_instructions,
)
if service_account_configured():
try:
bearer_token, _ = get_access_token()
except RuntimeError as exc:
return ToolResult(success=False, error=str(exc))
else:
return ToolResult(
success=False,
error="No Google credentials found. " + self.install_instructions,
)
start = time.time()
try:
result = self._generate(inputs, api_key)
result = self._generate(inputs, api_key=api_key, bearer_token=bearer_token)
except Exception as exc:
return ToolResult(success=False, error=f"Google TTS failed: {exc}")
@@ -176,7 +191,12 @@ class GoogleTTS(BaseTool):
result.cost_usd = self.estimate_cost(inputs)
return result
def _generate(self, inputs: dict[str, Any], api_key: str) -> ToolResult:
def _generate(
self,
inputs: dict[str, Any],
api_key: str | None = None,
bearer_token: str | None = None,
) -> ToolResult:
import requests
text = inputs["text"]
@@ -203,10 +223,17 @@ class GoogleTTS(BaseTool):
api_version = "v1beta1" if self._needs_beta_api(voice_name) else "v1"
url = f"https://texttospeech.googleapis.com/{api_version}/text:synthesize"
headers = {"Content-Type": "application/json"}
params: dict[str, str] = {}
if bearer_token:
headers["Authorization"] = f"Bearer {bearer_token}"
else:
params["key"] = api_key
response = requests.post(
url,
headers={"Content-Type": "application/json"},
params={"key": api_key},
headers=headers,
params=params,
json=payload,
timeout=120,
)