From 8b68c3cd0925b95f8e7b8cbe605245313620733b Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Fri, 25 Sep 2026 18:32:21 -0400 Subject: [PATCH] fix(vertex_ai): keep legacy bucket_name in credential resolution and add GCS_BATCH_BUCKET_NAME env var (#42803) * fix(vertex_ai): map legacy bucket_name to gcs_bucket_name and add GCS_BATCH_BUCKET_NAME env var * refactor(router): keep legacy bucket_name as a credential field instead of a validator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(vertex_ai): pass the RAG corpus bucket to the file upload instead of hopping through GCS_BUCKET_NAME * fix(vertex_ai): accept existing_file_id in the RAG Engine store step so ingest() runs end to end --------- Co-authored-by: Mubashir Osmani Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> --- litellm/llms/vertex_ai/files/handler.py | 14 ++-- .../llms/vertex_ai/files/transformation.py | 5 +- .../llms/vertex_ai/rag_engine/ingestion.py | 49 +++++------- litellm/types/router.py | 1 + .../files/test_vertex_ai_files_handler.py | 35 +++++++++ .../test_vertex_ai_files_transformation.py | 11 +++ .../llms/vertex_ai/rag_engine/__init__.py | 0 .../vertex_ai/rag_engine/test_ingestion.py | 76 +++++++++++++++++++ tests/unit/test_router/test_router.py | 45 +++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 + 10 files changed, 203 insertions(+), 37 deletions(-) create mode 100644 tests/unit/llms/vertex_ai/rag_engine/__init__.py create mode 100644 tests/unit/llms/vertex_ai/rag_engine/test_ingestion.py diff --git a/litellm/llms/vertex_ai/files/handler.py b/litellm/llms/vertex_ai/files/handler.py index ac95d1348f9..f2da04a7db7 100644 --- a/litellm/llms/vertex_ai/files/handler.py +++ b/litellm/llms/vertex_ai/files/handler.py @@ -53,14 +53,18 @@ class VertexAIFilesHandler(GCSBucketBase): Sources them from the deployment's ``litellm_params`` (``gcs_bucket_name`` / ``bucket_name`` and ``vertex_credentials``), mirroring the write path in - ``VertexAIFilesConfig._get_configured_bucket_name``, and falls back to the global - ``GCS_BUCKET_NAME`` / ``GCS_PATH_SERVICE_ACCOUNT`` env vars. This lets Vertex batch - run entirely at the model-group level, so output written to a per-model bucket is - readable without setting the global env vars. + ``VertexAIFilesConfig._get_configured_bucket_name``, and falls back to the + ``GCS_BATCH_BUCKET_NAME`` then ``GCS_BUCKET_NAME`` / ``GCS_PATH_SERVICE_ACCOUNT`` + env vars. This lets Vertex batch run entirely at the model-group level, so output + written to a per-model bucket is readable without setting the global env vars. """ params: Final[Mapping[str, object]] = litellm_params or {} bucket_candidate: Final = params.get("gcs_bucket_name") or params.get("bucket_name") - configured_bucket_name = bucket_candidate if isinstance(bucket_candidate, str) else os.getenv("GCS_BUCKET_NAME") + configured_bucket_name = ( + bucket_candidate + if isinstance(bucket_candidate, str) + else os.getenv("GCS_BATCH_BUCKET_NAME") or os.getenv("GCS_BUCKET_NAME") + ) credentials: Final = params.get("vertex_credentials") or vertex_credentials if isinstance(credentials, dict): diff --git a/litellm/llms/vertex_ai/files/transformation.py b/litellm/llms/vertex_ai/files/transformation.py index dbb41b57348..2b0694697a4 100644 --- a/litellm/llms/vertex_ai/files/transformation.py +++ b/litellm/llms/vertex_ai/files/transformation.py @@ -961,7 +961,10 @@ class VertexAIFilesConfig(VertexBase, BaseFilesConfig): def _get_configured_bucket_name(self, litellm_params: dict) -> str: bucket_name: Final = ( - litellm_params.get("gcs_bucket_name") or litellm_params.get("bucket_name") or os.getenv("GCS_BUCKET_NAME") + litellm_params.get("gcs_bucket_name") + or litellm_params.get("bucket_name") + or os.getenv("GCS_BATCH_BUCKET_NAME") + or os.getenv("GCS_BUCKET_NAME") ) if not bucket_name: raise ValueError("GCS bucket_name is required") diff --git a/litellm/llms/vertex_ai/rag_engine/ingestion.py b/litellm/llms/vertex_ai/rag_engine/ingestion.py index d9916209a14..c10bac595b6 100644 --- a/litellm/llms/vertex_ai/rag_engine/ingestion.py +++ b/litellm/llms/vertex_ai/rag_engine/ingestion.py @@ -122,41 +122,26 @@ class VertexAIRAGIngestion(BaseRAGIngestion): """ import litellm - # Set GCS_BUCKET_NAME env var for litellm.files.create_file - # The handler uses this to determine where to upload - original_bucket: Final = os.environ.get("GCS_BUCKET_NAME") - if self.gcs_bucket: - os.environ["GCS_BUCKET_NAME"] = self.gcs_bucket + file_tuple: Final = (filename, file_content, content_type) - try: - # Create file tuple for litellm.files.acreate_file - file_tuple: Final = (filename, file_content, content_type) + verbose_logger.debug( + "Uploading file to GCS via litellm.files.acreate_file: %s (bucket: %s)", filename, self.gcs_bucket + ) - verbose_logger.debug( - "Uploading file to GCS via litellm.files.acreate_file: %s (bucket: %s)", filename, self.gcs_bucket - ) + response: Final = await litellm.acreate_file( + file=file_tuple, + purpose="assistants", + custom_llm_provider="vertex_ai", + gcs_bucket_name=self.gcs_bucket, + vertex_project=self.vertex_project, + vertex_location=self.vertex_location, + vertex_credentials=self.vertex_credentials, + ) - # Upload to GCS using LiteLLM's file upload - response: Final = await litellm.acreate_file( - file=file_tuple, - purpose="assistants", # Purpose for file storage - custom_llm_provider="vertex_ai", - vertex_project=self.vertex_project, - vertex_location=self.vertex_location, - vertex_credentials=self.vertex_credentials, - ) + gcs_uri: Final = response.id + verbose_logger.info("Uploaded file to GCS: %s", gcs_uri) - # The response.id should be the GCS URI - gcs_uri: Final = response.id - verbose_logger.info("Uploaded file to GCS: %s", gcs_uri) - - return gcs_uri - finally: - # Restore original env var - if original_bucket is not None: - os.environ["GCS_BUCKET_NAME"] = original_bucket - elif "GCS_BUCKET_NAME" in os.environ: - del os.environ["GCS_BUCKET_NAME"] + return gcs_uri async def _import_file_to_corpus_via_sdk( self, @@ -259,6 +244,7 @@ class VertexAIRAGIngestion(BaseRAGIngestion): content_type: str | None, chunks: list[str], embeddings: list[list[float]] | None, + existing_file_id: str | None = None, ) -> tuple[str | None, str | None]: """ Store content in Vertex AI RAG corpus. @@ -274,6 +260,7 @@ class VertexAIRAGIngestion(BaseRAGIngestion): content_type: MIME type chunks: Ignored - Vertex AI handles chunking embeddings: Ignored - Vertex AI handles embedding + existing_file_id: Existing provider file ID, unsupported for Vertex AI RAG Engine Returns: Tuple of (corpus_id, gcs_uri) diff --git a/litellm/types/router.py b/litellm/types/router.py index b72809f625f..d545f7ae639 100644 --- a/litellm/types/router.py +++ b/litellm/types/router.py @@ -345,6 +345,7 @@ class CredentialLiteLLMParams(BaseModel): ## OBJECT STORAGE (files / batches) ## gcs_bucket_name: str | None = None + bucket_name: str | None = None ## AWS BEDROCK / SAGEMAKER ## aws_access_key_id: str | None = None diff --git a/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_handler.py b/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_handler.py index e0f0b7e5c0b..9b7cd127b83 100644 --- a/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_handler.py +++ b/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_handler.py @@ -208,6 +208,7 @@ class TestVertexAIFilesHandler: assert service_account == "/model/sa.json" def test_resolve_read_gcs_config_falls_back_to_env(self, monkeypatch): + monkeypatch.delenv("GCS_BATCH_BUCKET_NAME", raising=False) monkeypatch.setenv("GCS_BUCKET_NAME", "env-default-bucket") monkeypatch.setenv("GCS_PATH_SERVICE_ACCOUNT", "/env/sa.json") @@ -216,6 +217,40 @@ class TestVertexAIFilesHandler: assert bucket == "env-default-bucket" assert service_account == "/env/sa.json" + def test_resolve_read_gcs_config_prefers_batch_env_over_logging_env(self, monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + monkeypatch.setenv("GCS_BUCKET_NAME", "logging-bucket") + + bucket, _ = self.handler._resolve_read_gcs_config(litellm_params={}, vertex_credentials=None) + + assert bucket == "batch-bucket" + + def test_resolve_read_gcs_config_prefers_per_model_bucket_over_batch_env(self, monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + + bucket, _ = self.handler._resolve_read_gcs_config( + litellm_params={"gcs_bucket_name": "my-model-bucket"}, + vertex_credentials=None, + ) + + assert bucket == "my-model-bucket" + + def test_resolve_read_gcs_config_prefers_gcs_bucket_name_over_legacy(self): + bucket, _ = self.handler._resolve_read_gcs_config( + litellm_params={"gcs_bucket_name": "my-model-bucket", "bucket_name": "legacy-bucket"}, + vertex_credentials=None, + ) + + assert bucket == "my-model-bucket" + + def test_resolve_read_gcs_config_accepts_legacy_bucket_name_alone(self): + bucket, _ = self.handler._resolve_read_gcs_config( + litellm_params={"bucket_name": "legacy-bucket"}, + vertex_credentials=None, + ) + + assert bucket == "legacy-bucket" + def test_resolve_read_gcs_config_serializes_dict_credentials(self, monkeypatch): monkeypatch.delenv("GCS_PATH_SERVICE_ACCOUNT", raising=False) diff --git a/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_transformation.py b/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_transformation.py index 7434eae72a4..6f18a391f7b 100644 --- a/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_transformation.py +++ b/tests/unit/llms/vertex_ai/files/test_vertex_ai_files_transformation.py @@ -1186,10 +1186,21 @@ class TestConfiguredBucketNameResolution: assert config._get_configured_bucket_name({"gcs_bucket_name": "new", "bucket_name": "legacy"}) == "new" def test_should_fall_back_to_env(self, config, monkeypatch): + monkeypatch.delenv("GCS_BATCH_BUCKET_NAME", raising=False) monkeypatch.setenv("GCS_BUCKET_NAME", "env-bucket") assert config._get_configured_bucket_name({}) == "env-bucket" + def test_should_prefer_batch_env_over_logging_env(self, config, monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + monkeypatch.setenv("GCS_BUCKET_NAME", "logging-bucket") + assert config._get_configured_bucket_name({}) == "batch-bucket" + + def test_should_prefer_litellm_params_over_batch_env(self, config, monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + assert config._get_configured_bucket_name({"gcs_bucket_name": "per-model-bucket"}) == "per-model-bucket" + def test_should_raise_when_no_bucket_anywhere(self, config, monkeypatch): + monkeypatch.delenv("GCS_BATCH_BUCKET_NAME", raising=False) monkeypatch.delenv("GCS_BUCKET_NAME", raising=False) with pytest.raises(ValueError, match="GCS bucket_name is required"): config._get_configured_bucket_name({}) diff --git a/tests/unit/llms/vertex_ai/rag_engine/__init__.py b/tests/unit/llms/vertex_ai/rag_engine/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/vertex_ai/rag_engine/test_ingestion.py b/tests/unit/llms/vertex_ai/rag_engine/test_ingestion.py new file mode 100644 index 00000000000..3acabc4d14e --- /dev/null +++ b/tests/unit/llms/vertex_ai/rag_engine/test_ingestion.py @@ -0,0 +1,76 @@ +import asyncio +import sys +from types import ModuleType, SimpleNamespace + +import litellm +from litellm.litellm_core_utils.get_litellm_params import get_litellm_params +from litellm.llms.vertex_ai.files.transformation import VertexAIFilesConfig +from litellm.llms.vertex_ai.rag_engine.ingestion import VertexAIRAGIngestion + + +def _ingestion_for_bucket(bucket: str) -> VertexAIRAGIngestion: + return VertexAIRAGIngestion( + { + "vector_store": { + "custom_llm_provider": "vertex_ai", + "vector_store_id": "corpus-123", + "vertex_project": "test-project", + "vertex_location": "us-central1", + "gcs_bucket": bucket, + } + } + ) + + +def test_upload_lands_in_the_corpus_bucket_when_batch_bucket_env_is_set(monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + monkeypatch.setenv("GCS_BUCKET_NAME", "logging-bucket") + resolver = VertexAIFilesConfig() + + async def acreate_file_through_real_bucket_resolver(**kwargs): + bucket = resolver._get_configured_bucket_name(get_litellm_params(**kwargs)) + return SimpleNamespace(id=f"gs://{bucket}/{kwargs['file'][0]}") + + monkeypatch.setattr(litellm, "acreate_file", acreate_file_through_real_bucket_resolver) + + uri = asyncio.run(_ingestion_for_bucket("rag-bucket")._upload_file_to_gcs(b"doc", "doc.txt", "text/plain")) + + assert uri == "gs://rag-bucket/doc.txt" + + +def _vertexai_sdk_stub(import_calls: list[dict[str, object]]) -> ModuleType: + rag = ModuleType("vertexai.rag") + rag.TransformationConfig = lambda chunking_config: chunking_config + rag.ChunkingConfig = lambda chunk_size, chunk_overlap: (chunk_size, chunk_overlap) + + def import_files(**kwargs): + import_calls.append(kwargs) + return SimpleNamespace(imported_rag_files_count=1) + + rag.import_files = import_files + vertexai = ModuleType("vertexai") + vertexai.init = lambda project, location: None + vertexai.rag = rag + return vertexai + + +def test_ingest_runs_end_to_end_through_the_base_pipeline(monkeypatch): + monkeypatch.setenv("GCS_BATCH_BUCKET_NAME", "batch-bucket") + resolver = VertexAIFilesConfig() + import_calls: list[dict[str, object]] = [] + stub = _vertexai_sdk_stub(import_calls) + monkeypatch.setitem(sys.modules, "vertexai", stub) + monkeypatch.setitem(sys.modules, "vertexai.rag", stub.rag) + + async def acreate_file_through_real_bucket_resolver(**kwargs): + bucket = resolver._get_configured_bucket_name(get_litellm_params(**kwargs)) + return SimpleNamespace(id=f"gs://{bucket}/{kwargs['file'][0]}") + + monkeypatch.setattr(litellm, "acreate_file", acreate_file_through_real_bucket_resolver) + + result = asyncio.run(_ingestion_for_bucket("rag-bucket").ingest(file_data=("doc.txt", b"doc", "text/plain"))) + + assert (result["status"], result["vector_store_id"], result["file_id"]) == ("completed", "corpus-123", "gs://rag-bucket/doc.txt") + assert [(c["corpus_name"], c["paths"]) for c in import_calls] == [ + ("projects/test-project/locations/us-central1/ragCorpora/corpus-123", ["gs://rag-bucket/doc.txt"]) + ] diff --git a/tests/unit/test_router/test_router.py b/tests/unit/test_router/test_router.py index 10669de9cc8..3393c2f0d3c 100644 --- a/tests/unit/test_router/test_router.py +++ b/tests/unit/test_router/test_router.py @@ -6470,6 +6470,51 @@ def test_get_deployment_credentials_with_provider_includes_bucket_name(): assert credentials["custom_llm_provider"] == "vertex_ai" +def test_get_deployment_credentials_with_provider_keeps_legacy_bucket_name(): + router = litellm.Router( + model_list=[ + { + "model_name": "vertex-gemini", + "litellm_params": { + "model": "vertex_ai/gemini-3.5-flash", + "vertex_project": "my-project", + "vertex_location": "global", + "bucket_name": "my-legacy-bucket", + }, + } + ], + ) + + credentials = router.get_deployment_credentials_with_provider(model_id="vertex-gemini") + + assert credentials is not None + assert credentials["bucket_name"] == "my-legacy-bucket" + assert "gcs_bucket_name" not in credentials + + +def test_get_deployment_credentials_with_provider_keeps_both_bucket_keys(): + router = litellm.Router( + model_list=[ + { + "model_name": "vertex-gemini", + "litellm_params": { + "model": "vertex_ai/gemini-3.5-flash", + "vertex_project": "my-project", + "vertex_location": "global", + "gcs_bucket_name": "new-bucket", + "bucket_name": "legacy-bucket", + }, + } + ], + ) + + credentials = router.get_deployment_credentials_with_provider(model_id="vertex-gemini") + + assert credentials is not None + assert credentials["gcs_bucket_name"] == "new-bucket" + assert credentials["bucket_name"] == "legacy-bucket" + + def test_get_deployment_credentials_with_provider_resolves_credential_name(): """ Test that get_deployment_credentials_with_provider correctly resolves diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 6126733095b..be7094f7f6c 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -32945,6 +32945,8 @@ export interface components { azure_username?: string | null; /** Bedrock Tags */ bedrock_tags?: unknown[] | null; + /** Bucket Name */ + bucket_name?: string | null; /** Budget Duration */ budget_duration?: string | null; /** Cache Creation Input Audio Token Cost */ @@ -46730,6 +46732,8 @@ export interface components { azure_username?: string | null; /** Bedrock Tags */ bedrock_tags?: unknown[] | null; + /** Bucket Name */ + bucket_name?: string | null; /** Budget Duration */ budget_duration?: string | null; /** Cache Creation Input Audio Token Cost */