@danhachuel/thunderbolt 0.5.27 → 0.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/main.py +18 -0
- package/hermes_ui/api_key_tests.py +7 -0
- package/hermes_ui/media_generation.py +22 -1
- package/hermes_ui/media_providers.py +11 -0
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -7087,6 +7087,24 @@ def _persist_media_cards(settings: dict[str, Any], cards: list[dict[str, Any]],
|
|
|
7087
7087
|
def _fetch_media_models(card: dict[str, Any]) -> list[str]:
|
|
7088
7088
|
"""Consultar modelos do provider, incluindo o catálogo nativo Gemini."""
|
|
7089
7089
|
provider = str(card.get("provider") or "").strip().lower()
|
|
7090
|
+
if provider == "openrouter":
|
|
7091
|
+
base_url = str(card.get("base_url") or "https://openrouter.ai/api/v1").rstrip("/")
|
|
7092
|
+
headers = {"Accept": "application/json", "Authorization": f"Bearer {str(card.get('api_key') or '').strip()}"}
|
|
7093
|
+
discovered: set[str] = set()
|
|
7094
|
+
failures: list[str] = []
|
|
7095
|
+
for suffix in ("/images/models", "/videos/models"):
|
|
7096
|
+
response = requests.get(f"{base_url}{suffix}", headers=headers, timeout=12)
|
|
7097
|
+
if response.status_code >= 400:
|
|
7098
|
+
failures.append(f"HTTP {response.status_code}")
|
|
7099
|
+
continue
|
|
7100
|
+
payload = response.json()
|
|
7101
|
+
entries = payload.get("data") if isinstance(payload, dict) else []
|
|
7102
|
+
if isinstance(entries, list):
|
|
7103
|
+
discovered.update(str(item.get("id") or "").strip() for item in entries if isinstance(item, dict) and str(item.get("id") or "").strip())
|
|
7104
|
+
if not discovered:
|
|
7105
|
+
detail = f" ({', '.join(failures)})" if failures else ""
|
|
7106
|
+
raise ValueError(f"O OpenRouter não devolveu modelos de imagem/vídeo{detail}.")
|
|
7107
|
+
return sorted(discovered, key=str.casefold)
|
|
7090
7108
|
if provider == "nano_banana":
|
|
7091
7109
|
base_url = str(card.get("base_url") or "https://generativelanguage.googleapis.com/v1beta").rstrip("/")
|
|
7092
7110
|
response = requests.get(
|
|
@@ -15,6 +15,7 @@ from urllib.parse import quote, urlsplit, urlunsplit
|
|
|
15
15
|
import requests
|
|
16
16
|
|
|
17
17
|
from app.modules.niche_finder.apify import APIFY_API_BASE
|
|
18
|
+
from integrations.openai_model_discovery import OpenAICompatibleAPIError, validate_openrouter_api_key
|
|
18
19
|
|
|
19
20
|
DEFAULT_TIMEOUT = 20
|
|
20
21
|
|
|
@@ -176,6 +177,12 @@ def test_media_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
176
177
|
return _get(f"{base_url}/v3/users/me", headers={"X-Api-Key": api_key})
|
|
177
178
|
if not model and provider not in {"inferenceport", "cloudflare_workers_ai", "heygen"}:
|
|
178
179
|
return _result("missing", "Complete o modelo antes de testar.")
|
|
180
|
+
if provider == "openrouter":
|
|
181
|
+
try:
|
|
182
|
+
validate_openrouter_api_key(api_key, base_url, model)
|
|
183
|
+
except OpenAICompatibleAPIError as exc:
|
|
184
|
+
return _result("error", str(exc)[:240])
|
|
185
|
+
return _result("success", "OpenRouter API Key e modelo OK")
|
|
179
186
|
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
|
180
187
|
if provider == "cloudflare_workers_ai":
|
|
181
188
|
account_id = str(source.get("account_id") or "").strip()
|
|
@@ -252,6 +252,8 @@ def _image_endpoint(card: Mapping[str, Any]) -> str:
|
|
|
252
252
|
explicit = str(card.get("image_endpoint") or "").strip()
|
|
253
253
|
if explicit:
|
|
254
254
|
return explicit
|
|
255
|
+
if style == "openrouter":
|
|
256
|
+
return f"{base}/images"
|
|
255
257
|
if style in {"openai_compatible", "huggingface", "agnes", "kie"}:
|
|
256
258
|
return f"{base}/images/generations"
|
|
257
259
|
if style == "replicate":
|
|
@@ -309,6 +311,13 @@ def _image_request(card: dict[str, Any], prompt: str, *, topic: str = "", letter
|
|
|
309
311
|
"extra_body": {"response_format": "b64_json"},
|
|
310
312
|
}
|
|
311
313
|
return requests.post(endpoint, headers=_headers(card), json=body, timeout=AGNES_IMAGE_TIMEOUT_SECONDS)
|
|
314
|
+
if style == "openrouter":
|
|
315
|
+
return requests.post(
|
|
316
|
+
endpoint,
|
|
317
|
+
headers=_headers(card),
|
|
318
|
+
json={"model": _model(card), "prompt": constrained_prompt, "n": 1, "aspect_ratio": "16:9"},
|
|
319
|
+
timeout=180,
|
|
320
|
+
)
|
|
312
321
|
body = {"model": _model(card), "prompt": constrained_prompt, "n": 1, "response_format": "b64_json"}
|
|
313
322
|
if provider == "pollinations":
|
|
314
323
|
body["size"] = "1792x1024"
|
|
@@ -480,6 +489,8 @@ def _video_endpoint(card: Mapping[str, Any]) -> str:
|
|
|
480
489
|
if not _model(card):
|
|
481
490
|
raise MediaGenerationError("FAL AI requer o identificador da rota/modelo para gerar vídeo.")
|
|
482
491
|
return f"{base}/{_model(card).lstrip('/')}"
|
|
492
|
+
if style == "openrouter":
|
|
493
|
+
return f"{base}/videos"
|
|
483
494
|
if style in {"openai_compatible", "agnes", "kie"}:
|
|
484
495
|
return f"{base}/videos/generations"
|
|
485
496
|
if style == "replicate":
|
|
@@ -503,6 +514,10 @@ def _video_request(card: dict[str, Any], prompt: str, image_url: str = "") -> An
|
|
|
503
514
|
}
|
|
504
515
|
if image_url:
|
|
505
516
|
body["image_url"] = image_url
|
|
517
|
+
if style == "openrouter":
|
|
518
|
+
body["aspect_ratio"] = str(card.get("aspect_ratio") or INTERNAL_VIDEO_ASPECT_RATIO)
|
|
519
|
+
body["resolution"] = str(card.get("video_size") or INTERNAL_VIDEO_SIZE)
|
|
520
|
+
return requests.post(endpoint, headers=_headers(card), json=body, timeout=180)
|
|
506
521
|
if style == "heygen":
|
|
507
522
|
avatar_id = str(card.get("avatar_id") or "").strip()
|
|
508
523
|
if not avatar_id:
|
|
@@ -534,7 +549,11 @@ def _video_result(payload: Mapping[str, Any]) -> tuple[str, str]:
|
|
|
534
549
|
raw_output = payload.get("output")
|
|
535
550
|
if isinstance(raw_output, list) and raw_output:
|
|
536
551
|
raw_output = raw_output[0]
|
|
537
|
-
|
|
552
|
+
unsigned_urls = payload.get("unsigned_urls")
|
|
553
|
+
if isinstance(unsigned_urls, list) and unsigned_urls:
|
|
554
|
+
direct = str(unsigned_urls[0] or "").strip()
|
|
555
|
+
else:
|
|
556
|
+
direct = str(payload.get("content_url") or payload.get("video_url") or payload.get("url") or raw_output or "").strip()
|
|
538
557
|
if direct.startswith(("http://", "https://")):
|
|
539
558
|
return direct, ""
|
|
540
559
|
result = payload.get("result")
|
|
@@ -576,6 +595,8 @@ def _poll_video(card: Mapping[str, Any], request_id: str, *, attempts: int = 24,
|
|
|
576
595
|
endpoint = f"{base}/predictions/{request_id}"
|
|
577
596
|
elif style == "heygen":
|
|
578
597
|
endpoint = f"{base}/v3/videos/{request_id}"
|
|
598
|
+
elif style == "openrouter":
|
|
599
|
+
endpoint = f"{base}/videos/{request_id}"
|
|
579
600
|
else:
|
|
580
601
|
endpoint = f"{base}/videos/{request_id}"
|
|
581
602
|
headers = _headers(card, fal=style == "fal_queue")
|
|
@@ -22,6 +22,7 @@ FULL_IA_VIDEO_PROVIDER_CODES = (
|
|
|
22
22
|
"huggingface",
|
|
23
23
|
"inferenceport",
|
|
24
24
|
"heygen",
|
|
25
|
+
"openrouter",
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
# Mantidos internamente para preservar compatibilidade e controlar o prompt dos providers.
|
|
@@ -165,6 +166,16 @@ MEDIA_PROVIDER_CATALOG: tuple[MediaProviderDefinition, ...] = (
|
|
|
165
166
|
extra_fields=("avatar_id", "voice_id"),
|
|
166
167
|
description="Vídeos com avatar, script ou áudio pela API V3 da HeyGen.",
|
|
167
168
|
),
|
|
169
|
+
MediaProviderDefinition(
|
|
170
|
+
"openrouter",
|
|
171
|
+
"OpenRouter",
|
|
172
|
+
default_base_url="https://openrouter.ai/api/v1",
|
|
173
|
+
supports_image=True,
|
|
174
|
+
supports_video=True,
|
|
175
|
+
supports_text=True,
|
|
176
|
+
api_style="openrouter",
|
|
177
|
+
description="Gateway unificado para modelos de geração de imagem e vídeo via APIs dedicadas do OpenRouter.",
|
|
178
|
+
),
|
|
168
179
|
)
|
|
169
180
|
|
|
170
181
|
_MEDIA_BY_CODE = {item.code: item for item in MEDIA_PROVIDER_CATALOG}
|
package/package.json
CHANGED