Skip to content
Merged
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
12 changes: 10 additions & 2 deletions backend/app/services/llm/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class LLMProvider:
ELEVENLABS_NATIVE = "elevenlabs-native"
ANTHROPIC_NATIVE = "anthropic-native"

_platform_fallback = {GOOGLE_GCP, GOOGLE_GCP_NATIVE}

_registry: dict[str, type[BaseProvider]] = {
OPENAI: OpenAIProvider,
GOOGLE: GoogleAIProvider,
Expand Down Expand Up @@ -79,9 +81,15 @@ def get_llm_provider(
)

if not credentials:
raise ValueError(
f"Credentials for provider '{credential_provider}' not configured for this project."
if credential_provider not in LLMProvider._platform_fallback:
raise ValueError(
f"Credentials for provider '{credential_provider}' not configured for this project."
)
logger.info(
f"[get_llm_provider] No stored credentials for '{credential_provider}', "
f"falling back to platform-shared credentials."
)
credentials = {}

try:
client = provider_class.create_client(credentials=credentials)
Expand Down
58 changes: 58 additions & 0 deletions backend/app/tests/services/llm/providers/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,64 @@ def test_get_llm_provider_with_missing_credentials(self, db: Session):

assert "not configured for this project" in str(exc_info.value)

def test_get_llm_provider_google_gcp_falls_back_to_platform_credentials(
self, db: Session
) -> None:
"""No stored ``google-gcp`` credential row shouldn't raise — the
provider's create_client() falls back to platform-shared settings."""
from app.services.llm.providers.google_gcp import GoogleGCPProvider

project = get_project(db)

with (
patch("app.crud.credentials.get_provider_credential") as mock_get_creds,
patch("app.services.llm.providers.google_gcp.settings") as mock_settings,
):
mock_get_creds.return_value = None
mock_settings.GCP_VERTEX_API_KEY = "platform-key"
mock_settings.GCP_PROJECT_ID = "platform-project"
mock_settings.GCP_VERTEX_LOCATION = "us-central1"
mock_settings.GCS_AUDIO_BUCKET = "platform-bucket"
mock_settings.GCP_SA_KEY = '{"type": "service_account"}'

provider = get_llm_provider(
session=db,
provider_type="google-gcp",
project_id=project.id,
organization_id=project.organization_id,
)

assert isinstance(provider, GoogleGCPProvider)
assert provider.client.api_key == "platform-key"

def test_get_llm_provider_google_gcp_raises_when_platform_credentials_missing(
self, db: Session
) -> None:
"""Platform fallback still surfaces a ValueError if settings are
also unconfigured, instead of silently constructing a broken client."""
project = get_project(db)

with (
patch("app.crud.credentials.get_provider_credential") as mock_get_creds,
patch("app.services.llm.providers.google_gcp.settings") as mock_settings,
):
mock_get_creds.return_value = None
mock_settings.GCP_VERTEX_API_KEY = None
mock_settings.GCP_PROJECT_ID = None
mock_settings.GCP_VERTEX_LOCATION = None
mock_settings.GCS_AUDIO_BUCKET = None
mock_settings.GCP_SA_KEY = None

with pytest.raises(ValueError) as exc_info:
get_llm_provider(
session=db,
provider_type="google-gcp",
project_id=project.id,
organization_id=project.organization_id,
)

assert "missing required fields" in str(exc_info.value)

def test_google_native_routes_to_aistudio(self, db: Session):
"""``google`` / ``google-native`` currently route to GoogleAIProvider
(AI Studio). Vertex routing is temporarily disabled."""
Expand Down
1 change: 1 addition & 0 deletions docs/wiki/modules/llm-call.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ All paths relative to `backend/app/`.
## Gotchas
- Saved config resolution = `config_id` + `version` pinned; ad-hoc blob never persisted.
- `type=proxy` auto-injects `provider="proxy"` (ConfigBlob validator).
- Missing project credentials raise, except `google-gcp`/`google-gcp-native`, which fall back to platform-shared credentials (`services/llm/providers/registry.py`).
- Feature needs an LLM config? Spec `LLMCallConfig` whole (never a bespoke params + prompt pair), and prefer an optional per-request field over a per-project binding table — saved references already give durable versioned config via `config`/`config_version`.
- `PromptTemplate.template` is a plain prompt string; `{{input}}` interpolation is llm-chain-only — features that assemble their own inputs don't use it.
Loading