feat(gemini): expand image and auth support

This commit is contained in:
silveroxides
2026-08-09 15:29:08 +02:00
parent 0e9efbc7f6
commit a9a589d11e
3 changed files with 437 additions and 29 deletions
+19
View File
@@ -0,0 +1,19 @@
# Agent Instructions
Use `$comfyui-custom-node-development` for all development, testing, review, and packaging work in this repository.
If the skill is unavailable, stop and tell the user that it must be installed before continuing.
## Local ComfyUI Context
<!-- comfyui-custom-node-context:start -->
comfyui_root: C:\Users\ishim\Tools\ComfyUI
custom_nodes_root: C:\Users\ishim\Tools\ComfyUI\custom_nodes
repository_root: C:\Users\ishim\Tools\ComfyUI\custom_nodes\ComfyUI_Gemini_Expanded_API
virtual_environment_root: C:\Users\ishim\Tools\ComfyUI\.venv
python_executable: C:\Users\ishim\Tools\ComfyUI\.venv\Scripts\python.exe
<!-- comfyui-custom-node-context:end -->
## Repository-Specific Instructions
Add only rules that apply specifically to this custom-node repository below this heading.
+112 -29
View File
@@ -137,6 +137,7 @@ class SSL_GeminiAPIKeyConfig(IO.ComfyNode):
IO.Boolean.Input("vertexai_express", default=False),
IO.String.Input("vertexai_project", optional=True),
IO.String.Input("vertexai_location", optional=True),
IO.String.Input("google_application_credentials", default="", optional=True, multiline=False, tooltip="Optional absolute path to an application default credentials JSON file for Vertex AI."),
],
outputs=[
cls.GemConfig.Output("config")
@@ -144,8 +145,18 @@ class SSL_GeminiAPIKeyConfig(IO.ComfyNode):
)
@classmethod
def execute(cls, api_key: str, api_version: str, use_vertexai_env: bool, vertexai_express: bool, vertexai_project: str | None = "", vertexai_location: str | None = "") -> IO.NodeOutput:
config = {"api_key": api_key, "api_version": api_version, "use_vertexai_env": use_vertexai_env, "vertexai_express": vertexai_express, "vertexai_project": vertexai_project, "vertexai_location": vertexai_location}
def execute(cls, api_key: str, api_version: str, use_vertexai_env: bool, vertexai_express: bool,
vertexai_project: str | None = "", vertexai_location: str | None = "",
google_application_credentials: str | None = "") -> IO.NodeOutput:
config = {
"api_key": api_key,
"api_version": api_version,
"use_vertexai_env": use_vertexai_env,
"vertexai_express": vertexai_express,
"vertexai_project": vertexai_project,
"vertexai_location": vertexai_location,
"google_application_credentials": google_application_credentials,
}
return IO.NodeOutput(config)
@@ -174,6 +185,12 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
@classmethod
def define_schema(cls) -> IO.Schema:
image_names = [f"image_{index}" for index in range(1, 101)]
image_template = IO.Autogrow.TemplateNames(
IO.Image.Input("image_1", optional=True),
names=image_names,
min=0,
)
return IO.Schema(
node_id="SSL_GeminiTextPrompt",
display_name="Expanded Gemini Text/Image",
@@ -191,8 +208,16 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
IO.Combo.Input("aspect_ratio", options=["None", "1:1", "9:16", "16:9", "3:4", "4:3", "3:2", "2:3", "5:4", "4:5", "21:9"], default="None"),
IO.Combo.Input("bypass_mode", options=["None", "system_instruction", "prompt", "both"], default="None"),
IO.Int.Input("thinking_budget", default=0, min=-1, max=24576, step=1, tooltip="0 disables thinking mode, -1 will activate it as default dynamic thinking and anything above 0 sets specific budget"),
IO.Image.Input("input_image", optional=True),
IO.Image.Input("input_image_2", optional=True),
IO.Autogrow.Input(
"image_inputs",
template=image_template,
optional=True,
tooltip=(
"Ordered Gemini image parts growing from image_1 through image_100. Images inside a batch "
"are sent consecutively before the next socket. Provider request-size and model-specific "
"reference limits still apply."
),
),
IO.Boolean.Input("use_proxy", default=False),
IO.String.Input("proxy_host", default="127.0.0.1"),
IO.Int.Input("proxy_port", default=7890, min=1, max=65535),
@@ -204,6 +229,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
IO.Combo.Input("media_resolution", options=["unspecified", "low", "medium", "high"], default="unspecified", tooltip="Set input media resolution for image, video and pdf. This changes tokens consumed."),
IO.String.Input("retry_pattern", default="", optional=True, multiline=False, tooltip="Regex pattern to match in response text. If matched, retry with new seed. Leave empty to disable."),
IO.Int.Input("max_retries", default=3, min=0, max=10, step=1, tooltip="Maximum number of retry attempts when pattern matches. 0 disables retry."),
IO.String.Input("timeout_fallback_text", default="", optional=True, multiline=True, tooltip="Text returned when the Gemini request times out. Leave empty to return the standard timeout message."),
],
outputs=[
IO.String.Output("text"),
@@ -270,10 +296,33 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
tensor = torch.from_numpy(empty_image).unsqueeze(0)
return tensor
@classmethod
def _flatten_image_inputs(cls, image_inputs: IO.Autogrow.Type | None):
if not image_inputs:
return []
def image_index(name):
match = re.fullmatch(r"image_(\d+)", name)
return int(match.group(1)) if match else 101
flattened = []
for _, image_batch in sorted(
image_inputs.items(), key=lambda item: (image_index(item[0]), item[0])
):
if image_batch is None:
continue
if not torch.is_tensor(image_batch) or image_batch.ndim != 4:
raise ValueError("Gemini image inputs must be BHWC image tensors.")
flattened.extend(
image_batch[index : index + 1]
for index in range(image_batch.shape[0])
)
return flattened
@classmethod
def _compute_fingerprint_and_check_cache(cls, config, prompt, system_instruction, model, temperature, top_p, top_k, max_output_tokens,
include_images, aspect_ratio, bypass_mode, thinking_budget, use_seed, seed,
input_image=None, input_image_2=None,
image_inputs: IO.Autogrow.Type | None = None,
use_proxy=False, proxy_host="127.0.0.1", proxy_port=7890, timeout=30,
include_thoughts=False, thinking_level=None, media_resolution=None,
retry_pattern="", max_retries=3):
@@ -288,8 +337,10 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"[WARNING] Cache hashing failed for image: {e}")
return "Error"
image_1_hash = get_tensor_hash(input_image)
image_2_hash = get_tensor_hash(input_image_2)
image_hashes = tuple(
get_tensor_hash(image)
for image in cls._flatten_image_inputs(image_inputs)
)
# 2. Determine Effective Parameters based on Model
# This ensures we don't cache-miss if an irrelevant parameter changes
@@ -319,8 +370,13 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
# If none of the above, all effective thinking/image params remain default/ignored
sanitized_config = dict(config)
api_key = sanitized_config.get("api_key")
if api_key:
sanitized_config["api_key"] = f"sha256:{hashlib.sha256(str(api_key).encode('utf-8')).hexdigest()}"
fingerprint = (
str(config),
str(sanitized_config),
prompt,
system_instruction,
model,
@@ -334,8 +390,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
eff_thinking_budget, # EFFECTIVE thinking_budget
use_seed,
int(seed) if use_seed else 0, # Only use seed in cache if use_seed is True
image_1_hash,
image_2_hash,
image_hashes,
bool(use_proxy),
str(proxy_host),
int(proxy_port),
@@ -456,16 +511,17 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
@classmethod
def execute(cls, config, prompt, system_instruction, model, temperature, top_p, top_k, max_output_tokens,
include_images, aspect_ratio, bypass_mode, thinking_budget, input_image=None, input_image_2=None,
include_images, aspect_ratio, bypass_mode, thinking_budget,
image_inputs: IO.Autogrow.Type | None = None,
use_proxy=False, proxy_host="127.0.0.1", proxy_port=7890, use_seed=False, seed=0, timeout=30,
include_thoughts=False, thinking_level=None, media_resolution=None,
retry_pattern="", max_retries=3) -> IO.NodeOutput:
retry_pattern="", max_retries=3, timeout_fallback_text="") -> IO.NodeOutput:
print(f"[INFO] SSL_GeminiTextPrompt execute called, model: {model}")
fingerprint, cached = cls._compute_fingerprint_and_check_cache(
config, prompt, system_instruction, model, temperature, top_p, top_k, max_output_tokens,
include_images, aspect_ratio, bypass_mode, thinking_budget, use_seed, seed,
input_image, input_image_2,
image_inputs,
use_proxy, proxy_host, proxy_port, timeout,
include_thoughts, thinking_level, media_resolution,
retry_pattern, max_retries
@@ -515,6 +571,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
text_output = ""
image_tensor = cls.generate_empty_image()
proxy_url: str | None = None
timed_out = False
try:
if use_proxy:
@@ -556,22 +613,43 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
api_version = config.get("api_version")
project = config.get("vertexai_project")
location = config.get("vertexai_location")
credentials_path = config.get("google_application_credentials")
if use_vertexai_env:
try:
env_use = os.environ["GOOGLE_GENAI_USE_VERTEXAI"].strip() if "GOOGLE_GENAI_USE_VERTEXAI" in os.environ else "True"
assert env_use, "GOOGLE_GENAI_USE_VERTEXAI is empty"
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"}
if not env_use:
raise ValueError("GOOGLE_GENAI_USE_VERTEXAI must be enabled when use_vertexai_env is true")
env_proj = os.environ["GOOGLE_CLOUD_PROJECT"].strip() if "GOOGLE_CLOUD_PROJECT" in os.environ else project
credentials = None
credentials_cache_key = None
credential_project = None
if credentials_path:
import google.auth
absolute_credentials_path = os.path.abspath(os.path.expanduser(credentials_path))
if not os.path.isfile(absolute_credentials_path):
raise ValueError(f"Application default credentials file not found: {absolute_credentials_path}")
credentials, credential_project = google.auth.load_credentials_from_file(
absolute_credentials_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
credentials_cache_key = absolute_credentials_path
env_proj = os.environ["GOOGLE_CLOUD_PROJECT"].strip() if "GOOGLE_CLOUD_PROJECT" in os.environ else (project or credential_project)
assert env_proj, "GOOGLE_CLOUD_PROJECT is empty"
env_loc = os.environ["GOOGLE_CLOUD_LOCATION"].strip() if "GOOGLE_CLOUD_LOCATION" in os.environ else location
assert env_loc, "GOOGLE_CLOUD_LOCATION is empty"
client_key = ("vertexai_env", env_use, env_proj, env_loc, api_version, proxy_url)
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,
credentials=credentials,
project=env_proj,
location=env_loc,
http_options=types.HttpOptions(api_version=api_version),
@@ -592,6 +670,10 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"Error: {e}")
return IO.NodeOutput(f"Invalid environment variable: {e}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
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)
elif vertexai_express:
if not project:
@@ -614,11 +696,13 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
else:
os.environ.setdefault("GOOGLE_CLOUD_LOCATION", location)
client_key = ("vertexai_express", config.get("api_key"), project, location, api_version, proxy_url)
api_key = config.get("api_key")
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)
if client_key not in cls._client_cache:
cls._client_cache[client_key] = genai.Client(
vertexai=True,
api_key=config.get("api_key"),
api_key=api_key,
http_options=types.HttpOptions(api_version=api_version),
**client_options
)
@@ -630,10 +714,12 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
client = cls._client_cache[client_key]
else:
client_key = ("standard", config.get("api_key"), api_version, proxy_url)
api_key = config.get("api_key")
api_key_hash = hashlib.sha256(str(api_key).encode("utf-8")).hexdigest() if api_key else None
client_key = ("standard", api_key_hash, api_version, proxy_url)
if client_key not in cls._client_cache:
cls._client_cache[client_key] = genai.Client(
api_key=config.get("api_key"),
api_key=api_key,
http_options=types.HttpOptions(api_version=api_version),
**client_options
)
@@ -657,11 +743,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
return IO.NodeOutput(f"Gemini client initialization failed: {str(e)}", cls.generate_empty_image(), actual_seed if actual_seed is not None else 0)
# Prepare contents (images + prompt)
images_to_process = []
if input_image is not None:
images_to_process.append(input_image)
if input_image_2 is not None:
images_to_process.append(input_image_2)
images_to_process = cls._flatten_image_inputs(image_inputs)
if images_to_process:
try:
@@ -859,7 +941,8 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
print(f"[WARNING] Invalid retry regex pattern: {regex_err}")
except queue.Empty:
text_output = f"Gemini API request/processing timed out, waited {timeout} seconds."
timed_out = True
text_output = timeout_fallback_text or f"Gemini API request/processing timed out, waited {timeout} seconds."
except Exception as e:
print(f"[ERROR] Unhandled error in generate method: {str(e)}")
@@ -869,7 +952,7 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
final_actual_seed = actual_seed if actual_seed is not None else 0
is_success = not text_output.startswith("API call/processing error:") and not text_output.startswith("Gemini API request/processing timed out")
is_success = not timed_out and not text_output.startswith("API call/processing error:") and not text_output.startswith("Gemini API request/processing timed out")
# Cache the result
if use_seed and is_success:
@@ -924,4 +1007,4 @@ class SSL_GeminiTextPrompt(IO.ComfyNode):
except:
pass
# V3 uses ComfyExtension entrypoint in __init__.py to expose nodes
# V3 uses ComfyExtension entrypoint in __init__.py to expose nodes
+306
View File
@@ -0,0 +1,306 @@
import hashlib
import threading
from io import BytesIO
from types import SimpleNamespace
import google.auth
import torch
from PIL import Image
from custom_nodes.ComfyUI_Gemini_Expanded_API import gemini_nodes
def _execute_kwargs(config):
return {
"config": config,
"prompt": "test prompt",
"system_instruction": "test system instruction",
"model": "gemini-2.0-flash",
"temperature": 1.0,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 128,
"include_images": False,
"aspect_ratio": "None",
"bypass_mode": "None",
"thinking_budget": 0,
}
def test_timeout_fallback_is_last_input():
schema = gemini_nodes.SSL_GeminiTextPrompt.define_schema()
assert schema.inputs[-1].id == "timeout_fallback_text"
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")
assert image_inputs.template.min == 0
assert image_inputs.template.names[:3] == [
"image_1",
"image_2",
"image_3",
]
assert image_inputs.template.names[-1] == "image_100"
assert not {"input_image", "input_image_2"} & {input_.id for input_ in schema.inputs}
def test_image_inputs_flatten_in_numeric_socket_and_batch_order():
first_batch = torch.stack(
[
torch.full((2, 2, 3), 0.1),
torch.full((2, 2, 3), 0.2),
]
)
third_socket = torch.full((1, 2, 2, 3), 0.3)
images = gemini_nodes.SSL_GeminiTextPrompt._flatten_image_inputs(
{
"image_3": third_socket,
"image_1": first_batch,
}
)
assert [round(float(image[0, 0, 0, 0]), 1) for image in images] == [0.1, 0.2, 0.3]
def test_all_ordered_images_are_sent_to_gemini_2_and_3_before_prompt(monkeypatch):
captured = {}
class FakeModels:
@staticmethod
def generate_content(**kwargs):
captured.update(kwargs)
part = SimpleNamespace(text="ok", inline_data=None)
content = SimpleNamespace(parts=[part])
return SimpleNamespace(candidates=[SimpleNamespace(content=content)])
class FakeClient:
def __init__(self, **kwargs):
self.models = FakeModels()
monkeypatch.setattr(gemini_nodes.genai, "Client", FakeClient)
gemini_nodes.SSL_GeminiTextPrompt._client_cache.clear()
config = {
"api_key": "test-key",
"api_version": "v1",
"use_vertexai_env": False,
"vertexai_express": False,
"vertexai_project": "",
"vertexai_location": "",
"google_application_credentials": "",
}
kwargs = _execute_kwargs(config)
kwargs["image_inputs"] = {
"image_3": torch.full((1, 2, 2, 3), 0.3),
"image_1": torch.stack(
[
torch.full((2, 2, 3), 0.1),
torch.full((2, 2, 3), 0.2),
]
),
}
def assert_ordered_parts():
assert len(captured["contents"]) == 4
assert captured["contents"][-1] == {"text": "test prompt"}
pixels = [
Image.open(BytesIO(part["inline_data"]["data"])).getpixel((0, 0))[0]
for part in captured["contents"][:-1]
]
assert pixels == [25, 51, 76]
for model in ("gemini-2.0-flash", "gemini-3-flash-preview"):
captured.clear()
kwargs["model"] = model
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**kwargs)
assert output[0] == "ok"
assert_ordered_parts()
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project")
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
gemini_nodes.SSL_GeminiTextPrompt._client_cache.clear()
kwargs["config"] = config | {"use_vertexai_env": True, "api_key": ""}
kwargs["model"] = "gemini-2.5-flash"
captured.clear()
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**kwargs)
assert output[0] == "ok"
assert_ordered_parts()
def test_fingerprint_hashes_every_ordered_image():
common = {
"config": {"api_key": "key", "api_version": "v1"},
"prompt": "prompt",
"system_instruction": "system",
"model": "gemini-2.0-flash",
"temperature": 1.0,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 128,
"include_images": False,
"aspect_ratio": "None",
"bypass_mode": "None",
"thinking_budget": 0,
"use_seed": True,
"seed": 1,
}
first = torch.zeros((1, 2, 2, 3))
second = torch.ones((1, 2, 2, 3))
fingerprint_a, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache(
**common,
image_inputs={"image_1": first, "image_2": second},
)
fingerprint_b, _ = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache(
**common,
image_inputs={"image_1": second, "image_2": first},
)
assert fingerprint_a != fingerprint_b
def test_api_key_is_hashed_in_fingerprint():
api_key = "secret-api-key"
fingerprint, cached = gemini_nodes.SSL_GeminiTextPrompt._compute_fingerprint_and_check_cache(
config={"api_key": api_key, "api_version": "v1"},
prompt="prompt",
system_instruction="system",
model="gemini-2.0-flash",
temperature=1.0,
top_p=0.95,
top_k=40,
max_output_tokens=128,
include_images=False,
aspect_ratio="None",
bypass_mode="None",
thinking_budget=0,
use_seed=True,
seed=1,
)
assert cached is None
assert api_key not in fingerprint[0]
assert hashlib.sha256(api_key.encode("utf-8")).hexdigest() in fingerprint[0]
def test_false_vertex_environment_value_is_rejected(monkeypatch):
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "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")),
)
config = {
"api_key": "",
"api_version": "v1",
"use_vertexai_env": True,
"vertexai_express": False,
"vertexai_project": "",
"vertexai_location": "",
"google_application_credentials": "",
}
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"
)
def test_adc_path_is_loaded_and_passed_to_vertex_client(monkeypatch, tmp_path):
credentials_path = tmp_path / "application_default_credentials.json"
credentials_path.write_text("{}", encoding="utf-8")
credentials = object()
captured = {}
monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "true")
monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False)
monkeypatch.setenv("GOOGLE_CLOUD_LOCATION", "global")
monkeypatch.setattr(
google.auth,
"load_credentials_from_file",
lambda path, scopes: (credentials, "credentials-project"),
)
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()
config = {
"api_key": "",
"api_version": "v1",
"use_vertexai_env": True,
"vertexai_express": False,
"vertexai_project": "",
"vertexai_location": "",
"google_application_credentials": str(credentials_path),
}
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**_execute_kwargs(config))
assert output[0] == "ok"
assert captured["vertexai"] is True
assert captured["credentials"] is credentials
assert captured["project"] == "credentials-project"
def test_timeout_returns_custom_fallback_without_caching(monkeypatch):
release_request = threading.Event()
class BlockingModels:
@staticmethod
def generate_content(**kwargs):
release_request.wait(timeout=1)
raise RuntimeError("request released")
class FakeClient:
def __init__(self, **kwargs):
self.models = BlockingModels()
monkeypatch.setattr(gemini_nodes.genai, "Client", FakeClient)
gemini_nodes.SSL_GeminiTextPrompt._client_cache.clear()
gemini_nodes.SSL_GeminiTextPrompt._cache.clear()
config = {
"api_key": "test-key",
"api_version": "v1",
"use_vertexai_env": False,
"vertexai_express": False,
"vertexai_project": "",
"vertexai_location": "",
"google_application_credentials": "",
}
kwargs = _execute_kwargs(config)
kwargs.update(
use_seed=True,
seed=123,
timeout=0.01,
timeout_fallback_text="configured fallback",
)
try:
output = gemini_nodes.SSL_GeminiTextPrompt.execute(**kwargs)
finally:
release_request.set()
assert output[0] == "configured fallback"
assert gemini_nodes.SSL_GeminiTextPrompt._cache == {}