V 2.0.0 - Universal api #45 - BFL

This commit is contained in:
DESKTOP-TVBJISQ\Primere
2026-03-02 14:36:41 +01:00
parent 037b3c92ca
commit aebc2ae8bb
8 changed files with 270 additions and 26 deletions
+24 -1
View File
@@ -127,6 +127,20 @@ class PrimereApiProcessor:
if api_provider == "Gemini":
gemini_image_data = Image.open(TEMP_FILE_REF)
img_binary_api.append(gemini_image_data)
if api_provider == "BlackForest":
encoded_image = None
single_image = source_images[0]
width_original = single_image.shape[2]
height_original = single_image.shape[1]
image_np_bf = (single_image.numpy() * 255).astype(np.uint8)
img_bf = Image.fromarray(image_np_bf)
img_byte_arr_bf = io.BytesIO()
img_bf.save(img_byte_arr_bf, format="PNG")
img_byte_arr_bf.seek(0)
encoded_string = base64.b64encode(img_byte_arr_bf.read())
img_binary_api = encoded_string.decode('ascii')
break
elif hasattr(loaded_client_for_upload, "upload_file"):
uploaded_reference = loaded_client_for_upload.upload_file(TEMP_FILE_REF)
img_binary_api.append(uploaded_reference)
@@ -200,11 +214,20 @@ class PrimereApiProcessor:
try:
if rendered.method.upper() == "SDK":
context = {"client": client}
provider_config = config_json.get(api_provider, {}) if isinstance(config_json, dict) else {}
provider_api_key = provider_config.get("APIKEY") if isinstance(provider_config, dict) else None
context = {"client": client, "provider_api_key": provider_api_key}
allowed_roots = {"client"}
imported_context, imported_roots = external_api_backend.load_import_modules(schema_import_modules)
context.update(imported_context)
allowed_roots.update(imported_roots)
auto_context, auto_roots = external_api_backend.build_sdk_context(rendered, client)
for root_name in auto_roots:
if root_name not in context and root_name in auto_context:
context[root_name] = auto_context[root_name]
allowed_roots.add(root_name)
sdk_context = dict(context)
sdk_call_data = rendered.sdk_call if isinstance(rendered.sdk_call, dict) else {}
sdk_args = sdk_call_data.get("args", []) if isinstance(sdk_call_data, dict) else []
+61 -2
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
from typing import Any
import os
from . import external_api_backend
from . import api_helper
def _canonical_param_name(name: str) -> str:
low = name.lower()
@@ -10,12 +12,62 @@ def _canonical_param_name(name: str) -> str:
return "resolution"
if low == "model" or low.endswith("_model"):
return "model"
if "prompt" in low or "contents" in low:
if low in {"prompt", "contents"} or low.endswith("_prompt"):
return "prompt"
if "response_modalities" in low:
return "response_modalities"
return name
def _provider_api_key(spec: dict[str, Any]) -> Any:
provider = str(spec.get("provider") or "").strip()
if not provider:
return None
try:
config = api_helper.get_api_config("apiconfig.json")
except Exception:
return None
entry = config.get(provider, {}) if isinstance(config, dict) else {}
if not isinstance(entry, dict):
return None
value = entry.get("APIKEY")
return value if value not in (None, "") else None
def _secret_placeholder_default(key: str, spec: dict[str, Any]) -> Any:
normalized = str(key or "").strip()
low = normalized.lower()
# If placeholder itself looks like an env-var token, keep token name.
# This preserves schemas that intentionally use:
# {"$call": "os.environ.get", "$args": ["{{BFL_API_KEY}}"]}
# so runtime executes os.environ.get("BFL_API_KEY") correctly.
if normalized.upper() == normalized and "_" in normalized:
if low.endswith(("_key", "_token", "_secret", "_password")):
return normalized
direct_env = os.environ.get(normalized) or os.environ.get(normalized.upper())
if direct_env not in (None, ""):
return direct_env
if low.endswith("_api_key") or low in {"api_key", "apikey", "x_api_key", "provider_api_key", "authorization", "auth_token", "access_token", "bearer_token", "token"}:
return _provider_api_key(spec)
return None
def _marker_default(marker: Any) -> Any:
if not isinstance(marker, str):
return None
value = marker.strip().upper()
if value == "INT":
return 1
if value == "FLOAT":
return 1.0
if value == "STRING":
return ""
if value == "BOOLEAN":
return False
return None
def _default_value(name: str) -> Any:
k = name.lower()
if "prompt" in k:
@@ -62,7 +114,14 @@ def _build_values(spec: dict[str, Any], values: dict[str, Any] | None = None) ->
elif canonical in possible_parameters and isinstance(possible_parameters[canonical], list) and len(possible_parameters[canonical]) > 0:
selected = possible_parameters[canonical][0]
else:
selected = _default_value(canonical)
secret_selected = _secret_placeholder_default(key, spec)
if secret_selected is not None:
selected = secret_selected
else:
marker_selected = _marker_default(possible_parameters.get(key)) if key in possible_parameters else None
if marker_selected is None and canonical in possible_parameters:
marker_selected = _marker_default(possible_parameters.get(canonical))
selected = marker_selected if marker_selected is not None else _default_value(canonical)
resolved[key] = selected
+15
View File
@@ -229,6 +229,20 @@ def _materialize_sdk_value(value: Any, context: dict[str, Any], allowed_roots: s
return {k: _materialize_sdk_value(v, context, allowed_roots) for k, v in value.items()}
return value
def _apply_auth_header_fallback(kwargs: dict[str, Any], context: dict[str, Any]) -> None:
headers = kwargs.get("headers") if isinstance(kwargs, dict) else None
if not isinstance(headers, dict):
return
provider_api_key = context.get("provider_api_key")
if provider_api_key in (None, ""):
return
for auth_key in ("x-key", "x_api_key", "api-key", "authorization", "Authorization"):
if auth_key not in headers:
continue
if headers.get(auth_key) in (None, "", "null"):
headers[auth_key] = provider_api_key
def execute_sdk_request(rendered: RenderResult, context: dict[str, Any], allowed_roots: set[str] | None = None) -> Any:
if rendered.method.upper() != "SDK":
@@ -238,6 +252,7 @@ def execute_sdk_request(rendered: RenderResult, context: dict[str, Any], allowed
args, kwargs = normalize_sdk_call(rendered.sdk_call)
safe_args = [_materialize_sdk_value(a, context, roots) for a in args]
safe_kwargs = {k: _materialize_sdk_value(v, context, roots) for k, v in kwargs.items()}
_apply_auth_header_fallback(safe_kwargs, context)
return fn(*safe_args, **safe_kwargs)
def default_provider_service(node_data):
@@ -0,0 +1,89 @@
from __future__ import annotations
from typing import Any
import json
import requests
import comfy.utils
import numpy as np
import torch
from PIL import Image
import fal_client
from io import BytesIO
import time
from urllib.parse import urlparse
def handle_response(api_result, schema=None, loaded_client=None, response_url=None):
result_image = None
image_list = []
final_batch_img = []
status_accepted = ['Ready', 'Pending', 'Task not found']
parsed_url = urlparse(response_url)
blackforest_api_region = parsed_url.netloc or None
path_parts = [part for part in parsed_url.path.split("/") if part]
blackforest_api_version = path_parts[0] if len(path_parts) > 0 else None
try:
json_object = json.loads(api_result.text)
except ValueError as e:
raise RuntimeError(f"Input object failed: {api_result}")
if 'polling_url' in json_object:
resp = requests.get(json_object['polling_url'])
resp_json_object = json.loads(resp.text)
status = 'Start'
request_tryout = 0
error_tryout = 0
while error_tryout <= 1:
while status != 'Ready' or request_tryout <= 20:
url_res = f"https://{blackforest_api_region}/{blackforest_api_version}/get_result"
querystring = {"id": resp_json_object['id']}
response = requests.request("GET", url_res, params=querystring)
resp_json_object = json.loads(response.text)
status = resp_json_object['status']
if status not in status_accepted:
resp_error = requests.get(json_object['polling_url'])
resp_error_json_object = json.loads(resp_error.text)
error_status = resp_error_json_object['status']
raise RuntimeError(f"Response status: {status}, error status: {error_status}")
if status == 'Ready':
break
time.sleep(2)
request_tryout = request_tryout + 1
time.sleep(1)
if status == 'Ready':
break
error_tryout = error_tryout + 1
if status == 'Ready':
image_url = resp_json_object['result']['sample']
response = requests.get(image_url)
result_image = Image.open(BytesIO(response.content))
if result_image is not None:
result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
final_batch_img.append(result_image)
else:
raise RuntimeError(f"No result image...")
else:
raise RuntimeError(f"No polling_url in response: {json_object}")
if type(final_batch_img).__name__ == "list" and len(final_batch_img) > 1:
image_list = final_batch_img
single_image_start = final_batch_img[0]
batch_count = 0
s = None
for single_image in final_batch_img:
if (batch_count + 1) < len(final_batch_img):
current_single_image = final_batch_img[batch_count + 1]
if single_image_start.shape[1:] != current_single_image.shape[1:]:
current_single_image = comfy.utils.common_upscale(current_single_image.movedim(-1, 1), single_image_start.shape[2], single_image_start.shape[1], "bilinear", "center").movedim(1, -1)
batch_count = batch_count + 1
if s is not None:
single_image = s
s = torch.cat((current_single_image, single_image), dim=0)
result_image = s
return result_image
+8 -8
View File
@@ -385,14 +385,17 @@
}
},
"BlackForest": {
"flux-2-klein-4b": {
"Flux": {
"provider": "BlackForest",
"service": "flux-2-klein-4b",
"service": "Flux",
"response_handler": "Blackforest_Image.py",
"import_modules": [
],
"possible_parameters": {
"safety_tolerance": [0, 1, 2, 3, 4, 5],
"model_name": ["flux-kontext-pro", "flux-kontext-max", "flux-pro-1.0-fill", "flux-pro-1.0-expand", "flux-pro", "flux-pro-1.1", "flux-pro-1.1-ultra", "flux-2-flex", "flux-2-pro", "flux-2-max", "flux-2-klein-4b", "flux-2-klein-9b"],
"regions": ["api.bfl.ai", "api.eu.bfl.ai", "api.us.bfl.ai"],
"version": ["v1"],
"safety_tolerance": [0, 1, 2, 3, 4, 5, 6],
"prompt_upsampling": [false, true],
"guidance": "FLOAT",
"steps": "INT"
@@ -402,23 +405,20 @@
"endpoint": "requests.post",
"sdk_call": {
"args": [
"https://api.bfl.ai/v1/flux-2-klein-4b"
"https://{{regions}}/{{version}}/{{model_name}}"
],
"kwargs": {
"headers": {
"accept": "application/json",
"x-key": {
"$call": "os.environ.get",
"$args": [
"{{BFL_API_KEY}}"
],
"$args": ["{{BFL_API_KEY}}"],
"$kwargs": {}
},
"Content-Type": "application/json"
},
"json": {
"output_format": "png",
"image_prompt": "{{reference_images}}",
"safety_tolerance": "{{safety_tolerance}}",
"prompt": "{{prompt}}",
"seed": "{{seed}}",
+53 -6
View File
@@ -29,8 +29,11 @@ PLACEHOLDER_ALIASES = {"number_of_images": "seed", "aspectRatio": "aspect_ratio"
KNOWN_PARAM_OPTIONS: dict[str, list[str]] = {
"model": ["example-model-1", "example-model-2"],
"resolution": ["1K", "2K", "4K"],
"regions": ["api.bfl.ai", "api.eu.bfl.ai", "api.us.bfl.ai"],
}
TYPE_MARKERS = {"INT", "FLOAT", "STRING", "BOOLEAN"}
INLINE_PLACEHOLDER_RE = re.compile(r"(?<!\{)\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}(?!\})")
EXCLUDED_PARAMETER_KEYS = {"prompt", "response_modalities", "width", "height", "seed", "reference_images", "first_image", "last_image", "negative_prompt"}
DEFAULT_IMPORT_MODULES: dict[str, list[str]] = {
@@ -56,14 +59,18 @@ def placeholder(name: str) -> str:
clean = clean.replace("__", "_")
return f"{{{{{clean.strip('_') or 'value'}}}}}"
def normalize_inline_placeholders(value: str) -> str:
return INLINE_PLACEHOLDER_RE.sub(lambda m: placeholder(m.group(1)), value)
def node_to_template(node: ast.AST, path: str) -> Any:
if isinstance(node, ast.Constant):
if node.value is None or isinstance(node.value, bool):
return node.value
if isinstance(node.value, str):
if node.value in {"INT", "FLOAT", "STRING"}:
if node.value in TYPE_MARKERS:
return placeholder(path)
return normalize_inline_placeholders(node.value)
return node.value
if isinstance(node, ast.Name):
@@ -136,7 +143,7 @@ def _collect_placeholders(node: Any) -> set[str]:
def walk(item: Any) -> None:
if isinstance(item, dict):
for k, v in item.items():
if k in {"$args", "args"}:
if k in {"$args"}:
continue
walk(v)
return
@@ -162,21 +169,59 @@ def _canonical_param_name(name: str) -> str:
return "model"
if low == "number_of_images":
return "seed"
if "prompt" in low or "contents" in low:
if low in {"prompt", "contents"} or low.endswith("_prompt"):
return "prompt"
if "response_modalities" in low:
return "response_modalities"
return name
def _collect_type_markers(node: ast.AST) -> dict[str, str]:
marked: dict[str, str] = {}
def build_possible_parameters(request_schema: dict[str, Any]) -> dict[str, list[str]]:
def walk(item: ast.AST) -> None:
if isinstance(item, ast.Dict):
for key_node, value_node in zip(item.keys, item.values):
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
key = key_node.value
else:
key = ast.unparse(key_node)
if isinstance(value_node, ast.Constant) and isinstance(value_node.value, str) and value_node.value in TYPE_MARKERS:
marked[key] = value_node.value
walk(value_node)
return
if isinstance(item, (ast.List, ast.Tuple, ast.Set)):
for child in item.elts:
walk(child)
return
if isinstance(item, ast.Call):
for child in item.args:
walk(child)
for kw in item.keywords:
walk(kw.value)
walk(node)
return marked
def build_possible_parameters(request_schema: dict[str, Any], type_markers: dict[str, str] | None = None) -> dict[str, Any]:
placeholders = sorted(_collect_placeholders(request_schema))
possible: dict[str, list[str]] = {}
possible: dict[str, Any] = {}
marker_map = {_canonical_param_name(k): v for k, v in (type_markers or {}).items()}
for name in placeholders:
canonical = _canonical_param_name(name)
if canonical in EXCLUDED_PARAMETER_KEYS:
continue
marker = marker_map.get(canonical)
if marker == "BOOLEAN":
possible[canonical] = [False, True]
continue
if marker in {"INT", "FLOAT", "STRING"}:
possible[canonical] = marker
continue
if canonical in KNOWN_PARAM_OPTIONS:
possible[canonical] = KNOWN_PARAM_OPTIONS[canonical]
elif canonical not in possible:
@@ -234,12 +279,14 @@ def build_service_schema(snippet: str, provider: str = DEFAULT_PROVIDER, service
},
}
type_markers = _collect_type_markers(call)
service_schema = {
"provider": provider,
"service": service,
"response_handler": response_handler_filename(provider, service),
"import_modules": build_import_modules(snippet, provider=provider),
"possible_parameters": build_possible_parameters(request_schema),
"possible_parameters": build_possible_parameters(request_schema, type_markers=type_markers),
"request": request_schema,
}
+19 -8
View File
@@ -9,17 +9,28 @@
"from your_provider_sdk import types"
],
"possible_parameters": {
"guidance": [
"fake_guidance_value_1",
"fake_guidance_value_2"
"guidance": "FLOAT",
"model_name": [
"fake_model_name_value_1",
"fake_model_name_value_2"
],
"prompt_upsampling": [
false,
true
],
"regions": [
"api.bfl.ai",
"api.eu.bfl.ai",
"api.us.bfl.ai"
],
"safety_tolerance": [
"fake_safety_tolerance_value_1",
"fake_safety_tolerance_value_2"
],
"steps": [
"fake_steps_value_1",
"fake_steps_value_2"
"steps": "INT",
"version": [
"fake_version_value_1",
"fake_version_value_2"
]
},
"request": {
@@ -27,7 +38,7 @@
"endpoint": "requests.post",
"sdk_call": {
"args": [
"https://api.bfl.ai/v1/flux-2-klein-4b"
"https://{{regions}}/{{version}}/{{model_name}}"
],
"kwargs": {
"headers": {
@@ -49,7 +60,7 @@
"seed": "{{seed}}",
"width": "{{width}}",
"height": "{{height}}",
"prompt_upsampling": "BOOLEAN",
"prompt_upsampling": "{{prompt_upsampling}}",
"guidance": "{{guidance}}",
"steps": "{{steps}}"
}
+1 -1
View File
@@ -1,4 +1,4 @@
response = requests.post("https://api.bfl.ai/v1/flux-2-klein-4b",
response = requests.post("https://{{regions}}/{{version}}/{{model_name}}",
headers={
"accept": "application/json",
"x-key": os.environ.get("BFL_API_KEY"),