diff --git a/litellm/llms/vertex_ai/vertex_llm_base.py b/litellm/llms/vertex_ai/vertex_llm_base.py index 8b7f8c63625..ca6d6c680e4 100644 --- a/litellm/llms/vertex_ai/vertex_llm_base.py +++ b/litellm/llms/vertex_ai/vertex_llm_base.py @@ -127,27 +127,48 @@ class VertexBase: ) -> tuple[_VertexCredentialsObject | None, str]: if credentials is not None: if isinstance(credentials, str): - _is_path: Final = os.path.exists( - credentials - ) # credentials is from server config (litellm_params), not user input verbose_logger.debug( - "Vertex: Loading vertex credentials, is_file_path=%s, current dir %s", - _is_path, + "Vertex: Loading vertex credentials, current dir %s", os.getcwd(), ) - try: - if _is_path: + # Decide by value shape, not os.path.exists(): exists() + # swallows every OSError (missing, unreadable, transient), + # which previously misreported all of them as invalid JSON. + if credentials.lstrip().startswith("{"): + try: + json_obj = json.loads(credentials) + except json.JSONDecodeError as e: + raise Exception( + "Unable to load vertex credentials from environment. " + "Ensure the JSON is valid (check for unescaped newlines in private_key). " + f"Parse error: {type(e).__name__}: {e}" + ) from e + else: + try: with open(credentials) as f: json_obj = json.load(f) - else: - json_obj = json.loads(credentials) - except Exception as e: - raise Exception( - "Unable to load vertex credentials from environment. " - "Ensure the JSON is valid (check for unescaped newlines in private_key). " - f"Parse error: {type(e).__name__}" - ) + except FileNotFoundError as e: + raise Exception( + f"Unable to load vertex credentials from file path: {credentials}. File not found. ({e})" + ) from e + except PermissionError as e: + raise Exception( + "Unable to load vertex credentials from file path: " + f"{credentials}. File not readable (permission denied). ({e})" + ) from e + except OSError as e: + raise Exception( + "Unable to load vertex credentials from file path: " + f"{credentials}. Unable to read file. ({e})" + ) from e + except (json.JSONDecodeError, UnicodeDecodeError) as e: + raise Exception( + "Unable to load vertex credentials from file path: " + f"{credentials}. Ensure the JSON is valid " + "(check for unescaped newlines in private_key). " + f"Parse error: {type(e).__name__}: {e}" + ) from e elif isinstance(credentials, dict): json_obj = credentials else: diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_llm_base.py b/tests/test_litellm/llms/vertex_ai/test_vertex_llm_base.py index a4d67606698..3906df60de3 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_llm_base.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_llm_base.py @@ -2193,3 +2193,36 @@ class TestVertexBase: assert token == "cached-token" assert not mock_get_lock.called, "Fast path should not acquire lock" + + +class TestLoadAuthCredentialFileErrors: + def test_missing_credential_file_names_path(self): + vertex_base = VertexBase() + with pytest.raises(Exception, match="File not found"): + vertex_base.load_auth( + credentials="/nonexistent/vertexai.json", project_id="p" + ) + + def test_unreadable_credential_file_names_path(self): + vertex_base = VertexBase() + with patch( + "builtins.open", side_effect=PermissionError(13, "Permission denied") + ): + with pytest.raises(Exception, match="not readable"): + vertex_base.load_auth( + credentials="/some/vertexai.json", project_id="p" + ) + + def test_malformed_credential_file_keeps_json_advice(self, tmp_path): + bad_file = tmp_path / "vertexai.json" + bad_file.write_text("{not json") + vertex_base = VertexBase() + with pytest.raises(Exception, match="Ensure the JSON is valid"): + vertex_base.load_auth( + credentials=str(bad_file), project_id="p" + ) + + def test_malformed_inline_json_keeps_environment_message(self): + vertex_base = VertexBase() + with pytest.raises(Exception, match="from environment"): + vertex_base.load_auth(credentials="{not json", project_id="p")