Skip to content
2 changes: 2 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
GCP_SA_KEY: str = ""
GCS_AUDIO_BUCKET: str = ""

GEMINI_API_KEY: str = ""
GEMINI_DEFAULT_INFERENCE_ROUTE: str = "aistudio"
# RabbitMQ configuration for Celery broker
RABBITMQ_HOST: str = "localhost"
RABBITMQ_PORT: int = 5672
Expand Down
2 changes: 2 additions & 0 deletions backend/app/models/llm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Provider(StrEnum):
ELEVENLABS = "elevenlabs"
ANTHROPIC = "anthropic"
GOOGLE_AISTUDIO = "google-aistudio"
GOOGLE_VERTEX = "google-vertex"
PROXY = "proxy"


Expand Down Expand Up @@ -47,6 +48,7 @@ class Provider(StrEnum):
"elevenlabs-native",
"anthropic-native",
"google-aistudio-native",
"google-vertex-native",
]


Expand Down
7 changes: 6 additions & 1 deletion backend/app/services/llm/providers/google_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ def __init__(
self.gcs_bucket = gcs_bucket or settings.GCS_AUDIO_BUCKET

def endpoint(self, model: str) -> str:
host = N
host = ""
if self.location == "global":
host = "aiplatform.googleapis.com"
else:
host = f"{self.location}-aiplatform.googleapis.com"

return (
f"https://{host}/v1"
f"/projects/{self.project_id}/locations/{self.location}"
Expand Down
6 changes: 4 additions & 2 deletions backend/app/services/llm/providers/google_aistudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from app.models.llm.response import AudioOutput, AudioContent
from app.services.llm.providers.base import BaseProvider, ContentPart, MultiModalInput
from app.services.llm.mappers import BCP47_LOCALE_TO_GEMINI_LANG
from app.core.config import settings
from app.core.audio_utils import (
AudioRef,
convert_pcm_to_mp3,
Expand All @@ -55,9 +56,10 @@ def __init__(self, client: genai.Client):

@staticmethod
def create_client(credentials: dict[str, Any]) -> Any:
if "api_key" not in credentials:
api_key = credentials.get("api_key") or settings.GEMINI_API_KEY
if not api_key:
raise ValueError("API Key for Google Gemini Not Set")
return genai.Client(api_key=credentials["api_key"])
return genai.Client(api_key=api_key)
Comment on lines +59 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

Aistudio platform fallback contradicts PR's stated "no fallback" behavior.

The PR objectives state that for the aistudio route, missing user credentials should raise an error "and there are no platform-level fallbacks." But this create_client falls back to settings.GEMINI_API_KEY whenever credentials["api_key"] is absent, and test_google_aistudio_route_falls_back_to_platform_defaults explicitly asserts this fallback succeeds for platform-routed "google" traffic. Please confirm which behavior is intended — if the PR description is authoritative, this fallback (and its supporting test) should be removed/gated so platform-routed aistudio without stored credentials raises instead of silently using a shared platform key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/llm/providers/google_aistudio.py` around lines 59 - 62,
The Google Aistudio client creation flow currently falls back to
settings.GEMINI_API_KEY in create_client, which conflicts with the intended
no-fallback behavior for the aistudio route. Update create_client so it only
uses the stored credentials["api_key"] and raises when that is missing, and
adjust or remove the test_google_aistudio_route_falls_back_to_platform_defaults
expectation so platform-routed Google traffic does not silently use a shared
platform key.


@staticmethod
def format_parts(
Expand Down
40 changes: 31 additions & 9 deletions backend/app/services/llm/providers/registry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from sqlmodel import Session

from app.core.config import settings
from app.services.llm.providers.base import BaseProvider
from app.services.llm.providers.open_ai import OpenAIProvider
from app.services.llm.providers.google_aistudio import GoogleAIProvider
from app.services.llm.providers.google_ai import GoogleVertexAIProvider
from app.services.llm.providers.sarvam_ai import SarvamAIProvider
from app.services.llm.providers.eleven_ai import ElevenlabsAIProvider
from app.services.llm.providers.claude import ClaudeProvider
Expand All @@ -15,34 +17,46 @@ class LLMProvider:
OPENAI = "openai"
SARVAMAI = "sarvamai"
ELEVENLABS = "elevenlabs"
GOOGLE = "google"
ANTHROPIC = "anthropic"

# Platform-routed Google. Resolved via GEMINI_DEFAULT_INFERENCE_ROUTE.
GOOGLE = "google"
GOOGLE_NATIVE = "google-native"
# Explicit Google backends. Bypass env routing; use caller's own creds.
GOOGLE_VERTEX = "google-vertex"
GOOGLE_VERTEX_NATIVE = "google-vertex-native"
GOOGLE_AISTUDIO = "google-aistudio"
GOOGLE_AISTUDIO_NATIVE = "google-aistudio-native"

OPENAI_NATIVE = "openai-native"
GOOGLE_NATIVE = "google-native"
SARVAMAI_NATIVE = "sarvamai-native"
ELEVENLABS_NATIVE = "elevenlabs-native"
ANTHROPIC_NATIVE = "anthropic-native"

_registry: dict[str, type[BaseProvider]] = {
OPENAI: OpenAIProvider,
GOOGLE: GoogleAIProvider,
SARVAMAI: SarvamAIProvider,
ELEVENLABS: ElevenlabsAIProvider,
ANTHROPIC: ClaudeProvider,
GOOGLE: GoogleVertexAIProvider, # placeholder; env decides at resolve time
GOOGLE_NATIVE: GoogleVertexAIProvider,
GOOGLE_VERTEX: GoogleVertexAIProvider,
GOOGLE_VERTEX_NATIVE: GoogleVertexAIProvider,
GOOGLE_AISTUDIO: GoogleAIProvider,
GOOGLE_AISTUDIO_NATIVE: GoogleAIProvider,
OPENAI_NATIVE: OpenAIProvider,
GOOGLE_NATIVE: GoogleAIProvider,
SARVAMAI_NATIVE: SarvamAIProvider,
ELEVENLABS_NATIVE: ElevenlabsAIProvider,
ANTHROPIC_NATIVE: ClaudeProvider,
}

_GOOGLE_ROUTED = {GOOGLE, GOOGLE_NATIVE}

@classmethod
def get_provider_class(cls, provider_type: str) -> type[BaseProvider]:
"""Return the provider class for a given name."""
if provider_type in cls._GOOGLE_ROUTED:
route = settings.GEMINI_DEFAULT_INFERENCE_ROUTE
return GoogleAIProvider if route == "aistudio" else GoogleVertexAIProvider
provider = cls._registry.get(provider_type)
if not provider:
raise ValueError(
Expand All @@ -53,7 +67,6 @@ def get_provider_class(cls, provider_type: str) -> type[BaseProvider]:

@classmethod
def supported_providers(cls) -> list[str]:
"""Return a list of supported provider names."""
return list(cls._registry.keys())


Expand All @@ -63,8 +76,16 @@ def get_llm_provider(
from app.crud.credentials import get_provider_credential

provider_class = LLMProvider.get_provider_class(provider_type)
is_platform_routed = provider_type in LLMProvider._GOOGLE_ROUTED

credential_provider = provider_type.replace("-native", "")
if is_platform_routed:
credential_provider = (
LLMProvider.GOOGLE_AISTUDIO
if provider_class is GoogleAIProvider
else LLMProvider.GOOGLE_VERTEX
)
else:
credential_provider = provider_type.replace("-native", "")

credentials = get_provider_credential(
session=session,
Expand All @@ -73,16 +94,17 @@ def get_llm_provider(
org_id=organization_id,
)

if not credentials:
if not credentials and not is_platform_routed:
raise ValueError(
f"Credentials for provider '{credential_provider}' not configured for this project."
)

credentials = credentials or {}

try:
client = provider_class.create_client(credentials=credentials)
return provider_class(client=client)
except ValueError:
# Re-raise ValueError for credential/configuration errors
raise
except Exception as e:
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
Expand Down
Loading
Loading