diff --git a/backend/app/services/llm/providers/registry.py b/backend/app/services/llm/providers/registry.py index b6ed5b882..f683479cd 100644 --- a/backend/app/services/llm/providers/registry.py +++ b/backend/app/services/llm/providers/registry.py @@ -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, @@ -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) diff --git a/backend/app/tests/services/llm/providers/test_registry.py b/backend/app/tests/services/llm/providers/test_registry.py index c1307802c..16e585c3f 100644 --- a/backend/app/tests/services/llm/providers/test_registry.py +++ b/backend/app/tests/services/llm/providers/test_registry.py @@ -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.""" diff --git a/docs/wiki/modules/llm-call.md b/docs/wiki/modules/llm-call.md index 462fbc048..9bb07a7ee 100644 --- a/docs/wiki/modules/llm-call.md +++ b/docs/wiki/modules/llm-call.md @@ -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.