@danhachuel/thunderbolt 0.5.48 → 0.5.50

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.
@@ -161,7 +161,9 @@ def _vision_prompt(video: Mapping[str, Any]) -> str:
161
161
  def analyse_thumbnail_with_paligemma(path: str, card: Mapping[str, Any]) -> dict[str, Any]:
162
162
  key = str(card.get("api_key") or "").strip()
163
163
  base = str(card.get("base_url") or "https://integrate.api.nvidia.com/v1").rstrip("/")
164
- endpoint = base if base.endswith("/chat/completions") else f"{base}/chat/completions"
164
+ if "integrate.api.nvidia.com" in base:
165
+ base = base.replace("integrate.api.nvidia.com", "ai.api.nvidia.com")
166
+ endpoint = base if "/vlm/google/paligemma" in base else f"{base}/vlm/google/paligemma"
165
167
  model = str(card.get("model") or "google/paligemma").strip()
166
168
  if not key:
167
169
  return {"score": 50, "diagnosis": "API key NVIDIA NIM Paligemma não configurada.", "status": "sem_dados"}
@@ -13,6 +13,7 @@ from integrations.openai_model_discovery import (
13
13
  DEFAULT_NVIDIA_NIM_BASE_URL,
14
14
  OpenAICompatibleAPIError,
15
15
  fetch_openai_compatible_models,
16
+ validate_paligemma_api_key,
16
17
  validate_openai_compatible_api_key,
17
18
  validate_openrouter_api_key,
18
19
  )
@@ -431,7 +432,9 @@ def test_llm_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
431
432
  if not model:
432
433
  return {"ok": False, "status": "error", "message": "Missing configuration — introduza o modelo antes do teste."}
433
434
  try:
434
- if normalized["provider"] == "openrouter":
435
+ if normalized["provider"] == "nvidia_nim_paligemma":
436
+ validate_paligemma_api_key(api_key, base_url)
437
+ elif normalized["provider"] == "openrouter":
435
438
  validate_openrouter_api_key(api_key, base_url, model)
436
439
  else:
437
440
  validate_openai_compatible_api_key(api_key, base_url, model)
@@ -258,3 +258,39 @@ def fetch_replicate_models(
258
258
  if not models:
259
259
  raise ModelDiscoveryError("O endpoint Replicate não devolveu modelos com versão disponível.")
260
260
  return sorted(models, key=str.casefold)[:MAX_MODEL_IDS]
261
+
262
+
263
+ def validate_paligemma_api_key(
264
+ api_key: str,
265
+ base_url: str,
266
+ *,
267
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
268
+ ) -> None:
269
+ """Validate the Paligemma VLM route instead of the text-only chat route."""
270
+ token = str(api_key or "").strip()
271
+ if not token:
272
+ raise OpenAICompatibleAPIError("Informe a API key antes do teste.")
273
+ configured = str(base_url or DEFAULT_NVIDIA_NIM_BASE_URL).rstrip("/")
274
+ if configured.endswith("/chat/completions"):
275
+ configured = configured.removesuffix("/chat/completions")
276
+ if "integrate.api.nvidia.com" in configured:
277
+ configured = configured.replace("integrate.api.nvidia.com", "ai.api.nvidia.com")
278
+ endpoint = configured if "/vlm/google/paligemma" in configured else f"{configured}/vlm/google/paligemma"
279
+ payload = {
280
+ "messages": [{"role": "user", "content": "Describe this image in one word."}],
281
+ "max_tokens": 8,
282
+ "temperature": 0,
283
+ }
284
+ try:
285
+ response = requests.post(
286
+ endpoint,
287
+ headers={"Accept": "application/json", "Authorization": f"Bearer {token}", "Content-Type": "application/json"},
288
+ json=payload,
289
+ timeout=timeout,
290
+ )
291
+ except requests.RequestException as exc:
292
+ raise OpenAICompatibleAPIError(f"Não foi possível testar o endpoint Paligemma: {exc}") from exc
293
+ if response.status_code >= 400:
294
+ if response.status_code in (401, 403):
295
+ raise OpenAICompatibleAPIError(f"O endpoint Paligemma recusou a API key (HTTP {response.status_code}).")
296
+ raise OpenAICompatibleAPIError(f"O endpoint Paligemma devolveu HTTP {response.status_code}.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.5.48",
3
+ "version": "0.5.50",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import os
5
+ import signal
5
6
  import sys
6
7
 
7
8
  # Este ficheiro é o primeiro módulo Python executado pelo launcher. As
@@ -28,6 +29,26 @@ def _utf8_stream(stream: object) -> object:
28
29
  sys.stdout = _utf8_stream(sys.stdout)
29
30
  sys.stderr = _utf8_stream(sys.stderr)
30
31
 
32
+
33
+ def _custom_sigint_handler(signum: int, frame: object) -> None:
34
+ """Encerrar sem executar ``server.stop``/Click do handler do Streamlit."""
35
+ print("\nEncerrando servidor...", file=sys.stderr, flush=True)
36
+ raise SystemExit(0)
37
+
38
+
39
+ _original_signal = signal.signal
40
+
41
+
42
+ def _protected_signal(signum: signal.Signals, handler: object) -> object:
43
+ """Não permitir que Streamlit substitua o handler seguro de SIGINT."""
44
+ if signum == signal.SIGINT:
45
+ return _original_signal(signal.SIGINT, _custom_sigint_handler)
46
+ return _original_signal(signum, handler)
47
+
48
+
49
+ _original_signal(signal.SIGINT, _custom_sigint_handler)
50
+ signal.signal = _protected_signal
51
+
31
52
  from streamlit.web.cli import main # noqa: E402
32
53
 
33
54
  if __name__ == "__main__":