feat(gemini): migrate to enterprise client

Keep GOOGLE_GENAI_USE_VERTEXAI working while adopting the 2.17 Enterprise client and SDK-native proxy transport.
This commit is contained in:
silveroxides
2026-08-10 01:39:15 +02:00
parent 2226b1c69a
commit fe5a5ab4a7
5 changed files with 234 additions and 168 deletions
+6 -5
View File
@@ -32,7 +32,7 @@ Special Note: Regarding the error [ERROR]API call error: 'NoneType' object has n
## How to Use
### 1. Configure API Key or Configure Vertex AI
### 1. Configure Gemini API or Enterprise/Vertex AI
### Option A(Gemini API)
@@ -41,12 +41,13 @@ First, you need to obtain a Google Gemini API key:
2. Create an API key (In Google Cloud Console it is recommended to set key restriction to the Generative Language API)
3. In ComfyUI, locate and use the `Configure Gemini API Key` node to enter your API key.
### Option B(Vertex AI) but this is a bit complicated and a guide would be too verbose here
1. But start by visiting [Google Cloud Console Vertex AI API](https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview) and Enable it.
### Option B (Gemini Enterprise Agent Platform, formerly Vertex AI)
1. Start by visiting [Google Cloud Console Vertex AI API](https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview) and enable it.
2. Visit the [Vertex AI Model Garden](https://console.cloud.google.com/vertex-ai/model-garden) and select a Google Gemini model and then "view code" button.
3. Read the instructions for chosen model at "Try (Python)" for the environment variable option or "Try while using express mode (Python)" for API key (Currently unsure if API here is working correctly)
4. Set environment variables before launching: `GOOGLE_CLOUD_PROJECT=your-project-id`, `GOOGLE_CLOUD_LOCATION=global` and `GOOGLE_GENAI_USE_VERTEXAI=True` for variable option.
3. Read the instructions for the chosen model at "Try (Python)" for environment authentication or "Try while using express mode (Python)" for API-key authentication.
4. For environment authentication, set `GOOGLE_CLOUD_PROJECT=your-project-id`, `GOOGLE_CLOUD_LOCATION=global`, and `GOOGLE_GENAI_USE_ENTERPRISE=true` before launching ComfyUI. Existing installations may continue using `GOOGLE_GENAI_USE_VERTEXAI=true`; the node supports both names, with `GOOGLE_GENAI_USE_ENTERPRISE` taking precedence if both are set.
5. In ComfyUI, locate and use the `Configure Gemini API Key` node and set `use_vertexai_env` to `true`.
6. For Enterprise express mode, enter its API key and enable `vertexai_express`; project and location are not required for this mode.
### 2. Text Generation
+67 -153
View File
@@ -16,35 +16,12 @@ import traceback
import threading
import queue
import sys
import importlib
import subprocess
import random
import hashlib
import warnings
from typing import Any, Tuple
def check_and_install_dependencies():
required_packages = {
'requests': 'requests',
'pysocks': 'PySocks',
}
missing_packages = []
for module_name, package_name in required_packages.items():
try:
importlib.import_module(module_name)
except ImportError:
missing_packages.append(package_name)
if missing_packages:
print(f"[WARNING] Missing required dependencies: {', '.join(missing_packages)}.")
print(f"[INFO] Please install them using: pip install {' '.join(missing_packages)}")
print(f"[INFO] Alternatively, install all requirements: pip install -r requirements.txt")
try:
check_and_install_dependencies()
except Exception as e:
print(f"[WARNING] Error checking dependencies: {str(e)}")
class GetKeyAPI(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
@@ -133,7 +110,7 @@ class SSL_GeminiAPIKeyConfig(IO.ComfyNode):
inputs=[
IO.String.Input("api_key", multiline=False, default=""),
IO.Combo.Input("api_version", options=["v1", "v1alpha", "v1beta", "v1beta1", "v2beta"], default="v1alpha", tooltip="Select API version to use. v1alpha, v1beta and v2beta are Gemini API specific while v1beta1 is Vertex AI specific. Both can use v1"),
IO.Boolean.Input("use_vertexai_env", default=False, tooltip="Bypasses rest of config and uses Vertex AI environment variables if set"),
IO.Boolean.Input("use_vertexai_env", default=False, tooltip="Uses Gemini Enterprise Agent Platform (formerly Vertex AI) with GOOGLE_GENAI_USE_ENTERPRISE or the legacy GOOGLE_GENAI_USE_VERTEXAI environment variable."),
IO.Boolean.Input("vertexai_express", default=False),
IO.String.Input("vertexai_project", optional=True),
IO.String.Input("vertexai_location", optional=True),
@@ -171,13 +148,17 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
THINKING_MODELS = [
"gemini-1.5-pro-002", "gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21", "gemini-2.0-flash-thinking-exp-1219",
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25",
"gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"
"gemini-3-flash-preview", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"
]
GEN3_THINKING_MODELS = [
"gemini-pro-latest", "gemini-flash-latest", "gemini-3.1-pro-preview",
"gemini-3-flash-preview", "gemini-3.5-flash-lite", "gemini-3.6-flash"
"gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash"
]
IMAGE_MODELS = [
"gemini-2.5-flash-image-preview", "gemini-2.5-flash-image",
"gemini-3.1-flash-image", "gemini-3.1-flash-lite-image",
"gemini-3-pro-image-preview", "gemini-3-pro-image", "nano-banana-pro-preview",
]
IMAGE_MODELS = ["gemini-2.5-flash-image-preview", "gemini-2.5-flash-image", "gemini-3-pro-image-preview", "nano-banana-pro-preview"]
MEDIA_RES_MODELS = [
"gemini-3.1-flash-lite", "gemini-3-flash-preview", "gemini-3.1-pro-preview",
"gemini-3.5-flash", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"
@@ -199,7 +180,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
cls.GemConfig.Input("config"),
IO.String.Input("prompt", multiline=True),
IO.String.Input("system_instruction", default="You are a helpful AI assistant.", multiline=True),
IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-2.5-flash-image-preview", "nano-banana-pro-preview", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"),
IO.Combo.Input("model", options=["gemini-1.5-pro-002", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.5-flash-preview-04-17", "gemini-2.5-pro-exp-03-25", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-2.5-flash-image-preview", "gemini-3.1-flash-image", "gemini-3.1-flash-lite-image", "gemini-3-pro-image", "nano-banana-pro-preview", "gemini-pro-latest", "gemini-flash-latest", "gemini-flash-lite-latest"], default="gemini-2.5-flash"),
IO.Float.Input("temperature", default=1.0, min=0.0, max=1.0, step=0.01),
IO.Float.Input("top_p", default=0.95, min=0.0, max=1.0, step=0.01),
IO.Int.Input("top_k", default=40, min=1, max=100, step=1),
@@ -431,21 +412,50 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
return actual_seed
@classmethod
def _setup_proxy_env(cls, proxy_host, proxy_port):
def _build_proxy_url(cls, proxy_host, proxy_port):
if not proxy_host.startswith(('http://', 'https://')):
proxy_url = f"http://{proxy_host}:{proxy_port}"
else:
proxy_url = f"{proxy_host}:{proxy_port}"
os.environ['HTTP_PROXY'] = proxy_url
os.environ['HTTPS_PROXY'] = proxy_url
os.environ['http_proxy'] = proxy_url
os.environ['https_proxy'] = proxy_url
os.environ['REQUESTS_CA_BUNDLE'] = ''
print(f"[INFO] Proxy enabled: {proxy_url}")
return proxy_url
@staticmethod
def _parse_boolean_environment_variable(name: str) -> bool | None:
value = os.environ.get(name)
if value is None:
return None
normalized = value.strip().lower()
if normalized not in {"true", "1", "false", "0"}:
raise ValueError(f"{name} must be true, false, 1, or 0")
return normalized in {"true", "1"}
@classmethod
def _resolve_enterprise_environment(cls) -> bool:
enterprise = cls._parse_boolean_environment_variable("GOOGLE_GENAI_USE_ENTERPRISE")
legacy_vertex = cls._parse_boolean_environment_variable("GOOGLE_GENAI_USE_VERTEXAI")
if enterprise is not None and legacy_vertex is not None and enterprise != legacy_vertex:
warnings.warn(
"GOOGLE_GENAI_USE_ENTERPRISE and GOOGLE_GENAI_USE_VERTEXAI conflict; "
"GOOGLE_GENAI_USE_ENTERPRISE takes precedence.",
UserWarning,
stacklevel=2,
)
if enterprise is not None:
return enterprise
if legacy_vertex is not None:
return legacy_vertex
return True
@staticmethod
def _build_http_options(api_version: str, proxy_url: str | None):
client_args = {"proxy": proxy_url} if proxy_url else None
return types.HttpOptions(api_version=api_version, client_args=client_args)
@classmethod
def _build_generate_content_config(cls, model, temperature, top_p, top_k, max_output_tokens, seed,
include_images, response_modalities, aspect_ratio, padded_system_instruction,
@@ -532,12 +542,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"[INFO] Returning cached result for fingerprint {fingerprint}")
return IO.NodeOutput(cached_text, cached_image, cached_seed)
# --- keep most of the original implementation but converted to classmethod usage ---
original_http_proxy = os.environ.get('HTTP_PROXY')
original_https_proxy = os.environ.get('HTTPS_PROXY')
original_http_proxy_lower = os.environ.get('http_proxy')
original_https_proxy_lower = os.environ.get('https_proxy')
print(f"[INFO] Starting generation, model: {model}, temperature: {temperature}")
padded_prompt = prompt
@@ -562,12 +566,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"[INFO] Using cached successful gemini seed {cached_gemini_seed} for input seed {input_seed}")
actual_seed = cached_gemini_seed
# Flatten and simplify nested try/except blocks to ensure correct pairing
original_http_proxy = os.environ.get('HTTP_PROXY')
original_https_proxy = os.environ.get('HTTPS_PROXY')
original_http_proxy_lower = os.environ.get('http_proxy')
original_https_proxy_lower = os.environ.get('https_proxy')
text_output = ""
image_tensor = cls.generate_empty_image()
proxy_url: str | None = None
@@ -575,37 +573,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
try:
if use_proxy:
proxy_url = cls._setup_proxy_env(proxy_host, proxy_port)
# Initialize Gemini client
client_options = {}
if use_proxy:
try:
import google.api_core.http_client # type: ignore[import]
import google.auth.transport.requests # type: ignore[import]
import requests
from requests.adapters import HTTPAdapter
class ProxyAdapter(HTTPAdapter):
def __init__(self, proxy_url, **kwargs):
self.proxy_url = proxy_url
super().__init__(**kwargs)
def add_headers(self, request, **kwargs):
super().add_headers(request, **kwargs)
session = requests.Session()
proxies = {"http": str(proxy_url), "https": str(proxy_url)}
session.proxies.update(proxies)
adapter = ProxyAdapter(proxy_url, max_retries=1)
session.mount('http://', adapter)
session.mount('https://', adapter)
session.verify = False
http_client = google.api_core.http_client.RequestsHttpClient(session=session)
client_options["http_client"] = http_client
except Exception:
# best-effort proxy HTTP client setup; fall back if imports fail
pass
proxy_url = cls._build_proxy_url(proxy_host, proxy_port)
try:
vertexai_express = config.get("vertexai_express", False)
@@ -614,15 +582,16 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
project = config.get("vertexai_project")
location = config.get("vertexai_location")
credentials_path = config.get("google_application_credentials")
http_options = cls._build_http_options(api_version, proxy_url)
if use_vertexai_env:
try:
env_use_value = os.environ.get("GOOGLE_GENAI_USE_VERTEXAI", "true").strip().lower()
if env_use_value not in {"true", "1", "false", "0"}:
raise ValueError("GOOGLE_GENAI_USE_VERTEXAI must be true, false, 1, or 0")
env_use = env_use_value in {"true", "1"}
env_use = cls._resolve_enterprise_environment()
if not env_use:
raise ValueError("GOOGLE_GENAI_USE_VERTEXAI must be enabled when use_vertexai_env is true")
raise ValueError(
"GOOGLE_GENAI_USE_ENTERPRISE or GOOGLE_GENAI_USE_VERTEXAI "
"must be enabled when use_vertexai_env is true"
)
credentials = None
credentials_cache_key = None
@@ -648,12 +617,11 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
client_key = ("vertexai_env", env_use, env_proj, env_loc, api_version, proxy_url, credentials_cache_key)
if client_key not in cls._client_cache:
cls._client_cache[client_key] = genai.Client(
vertexai=env_use,
enterprise=env_use,
credentials=credentials,
project=env_proj,
location=env_loc,
http_options=types.HttpOptions(api_version=api_version),
**client_options
http_options=http_options,
)
print(f"[INFO] Created new genai.Client (vertexai_env)")
is_new_client = True
@@ -672,39 +640,23 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
except ValueError as e:
print(f"Error: {e}")
return IO.NodeOutput(f"Invalid Vertex AI configuration: {e}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
return IO.NodeOutput(f"Invalid Enterprise/Vertex AI configuration: {e}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
elif vertexai_express:
if not project:
try:
project = os.environ["GOOGLE_CLOUD_PROJECT"].strip()
assert project, "GOOGLE_CLOUD_PROJECT is empty"
except KeyError:
print("Missing required environment variable: GOOGLE_CLOUD_PROJECT")
return IO.NodeOutput("Missing environment variable: GOOGLE_CLOUD_PROJECT", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
else:
os.environ.setdefault("GOOGLE_CLOUD_PROJECT", project)
if not location:
try:
location = os.environ["GOOGLE_CLOUD_LOCATION"].strip()
assert location, "GOOGLE_CLOUD_LOCATION is empty"
except KeyError:
print("Missing required environment variable: GOOGLE_CLOUD_LOCATION")
return IO.NodeOutput("Missing environment variable: GOOGLE_CLOUD_LOCATION", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
else:
os.environ.setdefault("GOOGLE_CLOUD_LOCATION", location)
api_key = config.get("api_key")
if not api_key:
return IO.NodeOutput(
"Invalid Enterprise express configuration: API key is required",
cls.generate_empty_image(),
actual_seed if actual_seed is not None else 0,
)
api_key_hash = hashlib.sha256(str(api_key).encode("utf-8")).hexdigest() if api_key else None
client_key = ("vertexai_express", api_key_hash, project, location, api_version, proxy_url)
client_key = ("vertexai_express", api_key_hash, api_version, proxy_url)
if client_key not in cls._client_cache:
cls._client_cache[client_key] = genai.Client(
vertexai=True,
enterprise=True,
api_key=api_key,
http_options=types.HttpOptions(api_version=api_version),
**client_options
http_options=http_options,
)
print(f"[INFO] Created new genai.Client (vertexai_express)")
is_new_client = True
@@ -720,8 +672,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
if client_key not in cls._client_cache:
cls._client_cache[client_key] = genai.Client(
api_key=api_key,
http_options=types.HttpOptions(api_version=api_version),
**client_options
http_options=http_options,
)
print(f"[INFO] Created new genai.Client (standard)")
is_new_client = True
@@ -968,43 +919,6 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"[INFO] Cached successful gemini seed {final_actual_seed} for input seed {input_seed}")
try:
return IO.NodeOutput(text_output, image_tensor, final_actual_seed)
finally:
if original_http_proxy:
os.environ['HTTP_PROXY'] = original_http_proxy
else:
if 'HTTP_PROXY' in os.environ:
os.environ.pop('HTTP_PROXY')
if original_https_proxy:
os.environ['HTTPS_PROXY'] = original_https_proxy
else:
if 'HTTPS_PROXY' in os.environ:
os.environ.pop('HTTPS_PROXY')
if original_http_proxy_lower:
os.environ['http_proxy'] = original_http_proxy_lower
else:
if 'http_proxy' in os.environ:
os.environ.pop('http_proxy')
if original_https_proxy_lower:
os.environ['https_proxy'] = original_https_proxy_lower
else:
if 'https_proxy' in os.environ:
os.environ.pop('https_proxy')
if 'REQUESTS_CA_BUNDLE' in os.environ:
os.environ.pop('REQUESTS_CA_BUNDLE')
try:
import requests
if hasattr(requests, 'Session'):
clean_session = requests.Session()
if hasattr(requests, 'session'):
requests.session = lambda: clean_session
except:
pass
return IO.NodeOutput(text_output, image_tensor, final_actual_seed)
# V3 uses ComfyExtension entrypoint in __init__.py to expose nodes
+1 -1
View File
@@ -3,7 +3,7 @@ name = "ComfyUI_Gemini_Expanded_API"
description = "Node for usage of Gemini API"
version = "1.3.8"
license = {file = "LICENSE"}
dependencies = ["google-genai", "numpy", "torch", "requests", "PySocks"]
dependencies = ["google-genai>=2.17.0", "numpy", "torch"]
[project.urls]
Repository = "https://github.com/silveroxides/ComfyUI_Gemini_Expanded_API"
+4 -6
View File
@@ -1,6 +1,4 @@
google-genai>=1.72.0
numpy
torch
Pillow
requests
PySocks
google-genai>=2.17.0
numpy
torch
Pillow
+156 -3
View File
@@ -4,6 +4,7 @@ from io import BytesIO
from types import SimpleNamespace
import google.auth
import pytest
import torch
from PIL import Image
@@ -27,12 +28,57 @@ def _execute_kwargs(config):
}
def _config(**overrides):
config = {
"api_key": "",
"api_version": "v1",
"use_vertexai_env": False,
"vertexai_express": False,
"vertexai_project": "",
"vertexai_location": "",
"google_application_credentials": "",
}
config.update(overrides)
return config
def _install_success_client(monkeypatch, captured):
class FakeModels:
@staticmethod
def generate_content(**kwargs):
part = SimpleNamespace(text="ok", inline_data=None)
content = SimpleNamespace(parts=[part])
return SimpleNamespace(candidates=[SimpleNamespace(content=content)])
class FakeClient:
def __init__(self, **kwargs):
captured.update(kwargs)
self.models = FakeModels()
monkeypatch.setattr(gemini_nodes.genai, "Client", FakeClient)
gemini_nodes.SSL_GeminiTextPrompt._client_cache.clear()
def test_timeout_fallback_is_last_input():
schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema()
assert schema.inputs[-1].id == "timeout_fallback_text"
def test_vertex_config_socket_ids_remain_stable():
schema = gemini_nodes.SSL_GeminiAPIKeyConfig.define_schema()
assert [input_.id for input_ in schema.inputs] == [
"api_key",
"api_version",
"use_vertexai_env",
"vertexai_express",
"vertexai_project",
"vertexai_location",
"google_application_credentials",
]
def test_image_inputs_use_clean_autogrow_socket_ids():
schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema()
image_inputs = next(input_ for input_ in schema.inputs if input_.id == "image_inputs")
@@ -120,6 +166,7 @@ def test_all_ordered_images_are_sent_to_gemini_2_and_3_before_prompt(monkeypatch
assert output[0] == "ok"
assert_ordered_parts()
monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False)
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
@@ -191,6 +238,7 @@ def test_api_key_is_hashed_in_fingerprint():
def test_false_vertex_environment_value_is_rejected(monkeypatch):
monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False)
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false")
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
@@ -212,8 +260,8 @@ def test_false_vertex_environment_value_is_rejected(monkeypatch):
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**_execute_kwargs(config))
assert output[0] == (
"Invalid Vertex AI configuration: GOOGLE_GENAI_USE_VERTEXAI must be enabled "
"when use_vertexai_env is true"
"Invalid Enterprise/Vertex AI configuration: GOOGLE_GENAI_USE_ENTERPRISE or "
"GOOGLE_GENAI_USE_VERTEXAI must be enabled when use_vertexai_env is true"
)
@@ -223,6 +271,7 @@ def test_adc_path_is_loaded_and_passed_to_vertex_client(monkeypatch, tmp_path):
credentials = object()
captured = {}
monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False)
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
@@ -259,11 +308,115 @@ def test_adc_path_is_loaded_and_passed_to_vertex_client(monkeypatch, tmp_path):
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**_execute_kwargs(config))
assert output[0] == "ok"
assert captured["vertexai"] is True
assert captured["enterprise"] is True
assert "vertexai" not in captured
assert captured["credentials"] is credentials
assert captured["project"] == "credentials-project"
def test_enterprise_environment_takes_precedence_over_legacy_vertex(monkeypatch):
captured = {}
monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "true")
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false")
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
_install_success_client(monkeypatch, captured)
with pytest.warns(UserWarning, match="GOOGLE_GENAI_USE_ENTERPRISE takes precedence"):
output = gemini_nodes.SSL_GeminiTextPrompt.execute(
**_execute_kwargs(_config(use_vertexai_env=True))
)
assert output[0] == "ok"
assert captured["enterprise"] is True
assert captured["project"] == "test-project"
assert captured["location"] == "global"
def test_vertex_environment_mode_defaults_to_enterprise_when_selector_is_absent(monkeypatch):
captured = {}
monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False)
monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False)
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
_install_success_client(monkeypatch, captured)
output = gemini_nodes.SSL_GeminiTextPrompt.execute(
**_execute_kwargs(_config(use_vertexai_env=True))
)
assert output[0] == "ok"
assert captured["enterprise"] is True
def test_malformed_enterprise_environment_value_is_rejected(monkeypatch):
monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "yes")
monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False)
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
monkeypatch.setattr(
gemini_nodes.genai,
"Client",
lambda **kwargs: (_ for _ in ()).throw(AssertionError("Client must not be created")),
)
output = gemini_nodes.SSL_GeminiTextPrompt.execute(
**_execute_kwargs(_config(use_vertexai_env=True))
)
assert output[0] == (
"Invalid Enterprise/Vertex AI configuration: "
"GOOGLE_GENAI_USE_ENTERPRISE must be true, false, 1, or 0"
)
def test_enterprise_express_requires_only_api_key(monkeypatch):
captured = {}
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False)
_install_success_client(monkeypatch, captured)
output = gemini_nodes.SSL_GeminiTextPrompt.execute(
**_execute_kwargs(_config(api_key="express-key", vertexai_express=True))
)
assert output[0] == "ok"
assert captured["enterprise"] is True
assert captured["api_key"] == "express-key"
assert "project" not in captured
assert "location" not in captured
def test_proxy_uses_sdk_http_options_without_mutating_environment(monkeypatch):
captured = {}
monkeypatch.setenv("HTTP_PROXY", "http://existing-proxy:9000")
_install_success_client(monkeypatch, captured)
kwargs = _execute_kwargs(_config(api_key="test-key"))
kwargs.update(use_proxy=True, proxy_host="proxy.example", proxy_port=8080)
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**kwargs)
assert output[0] == "ok"
assert captured["http_options"].client_args == {
"proxy": "http://proxy.example:8080"
}
assert gemini_nodes.os.environ["HTTP_PROXY"] == "http://existing-proxy:9000"
def test_confirmed_current_models_are_visible_without_removing_legacy_ids():
schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema()
model_input = next(input_ for input_ in schema.inputs if input_.id == "model")
for model in (
"gemini-3.5-flash",
"gemini-3.1-flash-image",
"gemini-3.1-flash-lite-image",
"gemini-3-pro-image",
"gemini-2.5-flash-image-preview",
):
assert model in model_input.options
def test_timeout_returns_custom_fallback_without_caching(monkeypatch):
release_request = threading.Event()