From 5fcfefce14ae9cfd575a2317ceb70c34f1897ec0 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Wed, 26 Aug 2026 23:59:36 +0530 Subject: [PATCH 1/4] fix: platform fallback for google-gcp if not BYOK --- .../app/services/llm/providers/registry.py | 12 +++- .../services/llm/providers/test_registry.py | 58 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) 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.""" From cf69e293307fb61fdb189682ac25bf1207bab334 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Thu, 27 Aug 2026 11:25:41 +0530 Subject: [PATCH 2/4] test cases for evaluation and evaluation v2 --- backend/app/tests/api/routes/test_evaluation_iteration_v2.py | 4 ++-- backend/app/tests/services/evaluations/test_iteration.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/tests/api/routes/test_evaluation_iteration_v2.py b/backend/app/tests/api/routes/test_evaluation_iteration_v2.py index a56131233..ff07c7d43 100644 --- a/backend/app/tests/api/routes/test_evaluation_iteration_v2.py +++ b/backend/app/tests/api/routes/test_evaluation_iteration_v2.py @@ -13,7 +13,7 @@ from app.core.config import settings from app.models import Config, EvaluationDataset from app.models.evaluation_iteration import EvaluationIterationRun -from app.models.llm.request import ConfigBlob, KaapiCompletionConfig +from app.models.llm.request import ConfigBlob, KaapiTextCompletionConfig from app.tests.utils.auth import TestAuthContext from app.tests.utils.test_data import ( create_test_config, @@ -34,7 +34,7 @@ def _make_dataset(*, db: Session, user_api_key: TestAuthContext) -> EvaluationDa def _make_text_config(db: Session, project_id: int) -> Config: blob = ConfigBlob( - completion=KaapiCompletionConfig( + completion=KaapiTextCompletionConfig( provider="openai", type="text", params={"model": "gpt-4o-iter-route-test", "temperature": 0.7}, diff --git a/backend/app/tests/services/evaluations/test_iteration.py b/backend/app/tests/services/evaluations/test_iteration.py index 8f283daf7..595770c3f 100644 --- a/backend/app/tests/services/evaluations/test_iteration.py +++ b/backend/app/tests/services/evaluations/test_iteration.py @@ -24,7 +24,7 @@ EvaluationIterationRun, EvaluationIterationStatusEnum, ) -from app.models.llm.request import ConfigBlob, KaapiCompletionConfig +from app.models.llm.request import ConfigBlob, KaapiTextCompletionConfig from app.services.evaluations.iteration import ( compute_round_scores, validate_and_start_evaluation_iteration, @@ -100,7 +100,7 @@ def _make_dataset(*, db: Session, user_api_key: TestAuthContext) -> EvaluationDa def _make_text_config(db: Session, project_id: int) -> Config: blob = ConfigBlob( - completion=KaapiCompletionConfig( + completion=KaapiTextCompletionConfig( provider="openai", type="text", params={"model": "gpt-4o-iteration-test", "temperature": 0.7}, From 63f287852811c92e0000fbf841e95f48a58d8f59 Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Thu, 27 Aug 2026 13:09:37 +0530 Subject: [PATCH 3/4] chore: db migration scrip to see google-gcp in global schema --- .../083_seed_google_gcp_model_config.py | 60 +++++++++++++++++++ docs/wiki/modules/llm-call.md | 1 + 2 files changed, 61 insertions(+) create mode 100644 backend/app/alembic/versions/083_seed_google_gcp_model_config.py diff --git a/backend/app/alembic/versions/083_seed_google_gcp_model_config.py b/backend/app/alembic/versions/083_seed_google_gcp_model_config.py new file mode 100644 index 000000000..609684b5b --- /dev/null +++ b/backend/app/alembic/versions/083_seed_google_gcp_model_config.py @@ -0,0 +1,60 @@ +"""seed google-gcp model_config rows, mirroring google and google-aistudio + +Revision ID: 083 +Revises: 082 +Create Date: 2026-08-27 00:00:00.000000 + +077 first seeded these rows and added 'google-gcp' to global.provider_enum, +but 079 reverted the seed (while leaving the enum value in place, since +Postgres cannot drop an enum member) as part of unwinding an unrelated +feature. GoogleGCPProvider now supports text, stt, and tts (077 only mirrored +'google', which was stt/tts-only at the time), so this seeds from both +'google' and 'google-aistudio' to also pick up any text-capable rows. +""" + +from alembic import op + +revision = "083" +down_revision = "082" +branch_labels = None +depends_on = None + + +def upgrade(): + # Enum value already exists from 077 and survives forever (Postgres can't + # drop enum members) — ADD VALUE IF NOT EXISTS kept only for a from-scratch + # DB that never ran 077. + with op.get_context().autocommit_block(): + op.execute( + "ALTER TYPE global.provider_enum ADD VALUE IF NOT EXISTS 'google-gcp'" + ) + + op.execute( + """ + INSERT INTO global.model_config + (provider, model_name, completion_type, config, input_modalities, + output_modalities, pricing, is_active, inserted_at, updated_at) + SELECT 'google-gcp', model_name, completion_type, config, input_modalities, + output_modalities, pricing, is_active, NOW(), NOW() + FROM global.model_config + WHERE provider = 'google' + ON CONFLICT (provider, model_name) DO NOTHING + """ + ) + op.execute( + """ + INSERT INTO global.model_config + (provider, model_name, completion_type, config, input_modalities, + output_modalities, pricing, is_active, inserted_at, updated_at) + SELECT 'google-gcp', model_name, completion_type, config, input_modalities, + output_modalities, pricing, is_active, NOW(), NOW() + FROM global.model_config + WHERE provider = 'google-aistudio' + ON CONFLICT (provider, model_name) DO NOTHING + """ + ) + + +def downgrade(): + # PostgreSQL cannot remove a value from an enum; drop only the seeded rows. + op.execute("DELETE FROM global.model_config WHERE provider = 'google-gcp'") 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. From 1b51223206f87beee96d06fc352dc1c43ab8befe Mon Sep 17 00:00:00 2001 From: Prajna1999 Date: Thu, 27 Aug 2026 16:16:03 +0530 Subject: [PATCH 4/4] fix: remove google_gcp script --- .../083_seed_google_gcp_model_config.py | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 backend/app/alembic/versions/083_seed_google_gcp_model_config.py diff --git a/backend/app/alembic/versions/083_seed_google_gcp_model_config.py b/backend/app/alembic/versions/083_seed_google_gcp_model_config.py deleted file mode 100644 index 609684b5b..000000000 --- a/backend/app/alembic/versions/083_seed_google_gcp_model_config.py +++ /dev/null @@ -1,60 +0,0 @@ -"""seed google-gcp model_config rows, mirroring google and google-aistudio - -Revision ID: 083 -Revises: 082 -Create Date: 2026-08-27 00:00:00.000000 - -077 first seeded these rows and added 'google-gcp' to global.provider_enum, -but 079 reverted the seed (while leaving the enum value in place, since -Postgres cannot drop an enum member) as part of unwinding an unrelated -feature. GoogleGCPProvider now supports text, stt, and tts (077 only mirrored -'google', which was stt/tts-only at the time), so this seeds from both -'google' and 'google-aistudio' to also pick up any text-capable rows. -""" - -from alembic import op - -revision = "083" -down_revision = "082" -branch_labels = None -depends_on = None - - -def upgrade(): - # Enum value already exists from 077 and survives forever (Postgres can't - # drop enum members) — ADD VALUE IF NOT EXISTS kept only for a from-scratch - # DB that never ran 077. - with op.get_context().autocommit_block(): - op.execute( - "ALTER TYPE global.provider_enum ADD VALUE IF NOT EXISTS 'google-gcp'" - ) - - op.execute( - """ - INSERT INTO global.model_config - (provider, model_name, completion_type, config, input_modalities, - output_modalities, pricing, is_active, inserted_at, updated_at) - SELECT 'google-gcp', model_name, completion_type, config, input_modalities, - output_modalities, pricing, is_active, NOW(), NOW() - FROM global.model_config - WHERE provider = 'google' - ON CONFLICT (provider, model_name) DO NOTHING - """ - ) - op.execute( - """ - INSERT INTO global.model_config - (provider, model_name, completion_type, config, input_modalities, - output_modalities, pricing, is_active, inserted_at, updated_at) - SELECT 'google-gcp', model_name, completion_type, config, input_modalities, - output_modalities, pricing, is_active, NOW(), NOW() - FROM global.model_config - WHERE provider = 'google-aistudio' - ON CONFLICT (provider, model_name) DO NOTHING - """ - ) - - -def downgrade(): - # PostgreSQL cannot remove a value from an enum; drop only the seeded rows. - op.execute("DELETE FROM global.model_config WHERE provider = 'google-gcp'")