@danhachuel/thunderbolt 0.3.18 → 0.3.19

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 CHANGED
@@ -3853,10 +3853,6 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3853
3853
  test_result = test_llm_provider_card(edited)
3854
3854
  edited["test_result"] = stamp_test_result(test_result)
3855
3855
  _persist_llm_cards(settings, cards, str(settings.get(LLM_ACTIVE_CARD_KEY) or card_id))
3856
- if test_result.get("ok"):
3857
- st.success(f"Teste API: {test_result['message']}")
3858
- else:
3859
- st.error(f"Teste API: {test_result['message']}")
3860
3856
  elif refresh_clicked:
3861
3857
  try:
3862
3858
  from integrations.openai_model_discovery import fetch_openai_compatible_models
@@ -3880,7 +3876,7 @@ def _render_llm_card(settings: dict[str, Any], cards: list[dict[str, Any]], inde
3880
3876
  saved_test = edited.get("test_result") or card.get("test_result")
3881
3877
  if isinstance(saved_test, dict) and saved_test.get("message"):
3882
3878
  if saved_test.get("status") == "success":
3883
- st.success(f"Último teste: {saved_test['message']}")
3879
+ st.success("Último teste: API Key OK")
3884
3880
  else:
3885
3881
  st.error(f"Último teste: {saved_test['message']}")
3886
3882
 
@@ -9,12 +9,11 @@ from __future__ import annotations
9
9
  from dataclasses import dataclass
10
10
  from datetime import datetime, timezone
11
11
  from typing import Any, Mapping
12
- from urllib.parse import urlsplit, urlunsplit
13
-
14
12
  from integrations.openai_model_discovery import (
15
13
  DEFAULT_NVIDIA_NIM_BASE_URL,
16
- ModelDiscoveryError,
14
+ OpenAICompatibleAPIError,
17
15
  fetch_openai_compatible_models,
16
+ validate_openai_compatible_api_key,
18
17
  )
19
18
 
20
19
 
@@ -357,14 +356,6 @@ def apply_llm_cards_to_settings(
357
356
  return result
358
357
 
359
358
 
360
- def _safe_url(value: str) -> str:
361
- try:
362
- parsed = urlsplit(str(value or ""))
363
- return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
364
- except ValueError:
365
- return "endpoint configurado"
366
-
367
-
368
359
  def _redact(text: Any, secrets: tuple[str, ...]) -> str:
369
360
  message = str(text or "")
370
361
  for secret in secrets:
@@ -386,11 +377,12 @@ def test_llm_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
386
377
  return {"ok": False, "status": "error", "message": "Missing configuration — introduza a Base URL antes do teste."}
387
378
  if not base_url:
388
379
  return {"ok": False, "status": "error", "message": "Missing configuration — este provider não tem endpoint configurado."}
389
- if not definition.supports_model_discovery:
390
- return {"ok": False, "status": "error", "message": "Teste automático não suportado para este protocolo; verifique os campos do provider."}
380
+ model = str(normalized.get("model") or "").strip()
381
+ if not model:
382
+ return {"ok": False, "status": "error", "message": "Missing configuration — introduza o modelo antes do teste."}
391
383
  try:
392
- models = fetch_openai_compatible_models(api_key, base_url)
393
- except ModelDiscoveryError as exc:
384
+ validate_openai_compatible_api_key(api_key, base_url, model)
385
+ except OpenAICompatibleAPIError as exc:
394
386
  return {
395
387
  "ok": False,
396
388
  "status": "error",
@@ -401,7 +393,7 @@ def test_llm_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
401
393
  return {
402
394
  "ok": True,
403
395
  "status": "success",
404
- "message": f"API OK — {_safe_url(base_url)} respondeu com {len(models)} modelo(s).",
396
+ "message": "API Key OK",
405
397
  }
406
398
 
407
399
 
@@ -16,6 +16,10 @@ class ModelDiscoveryError(ValueError):
16
16
  """Raised when an OpenAI-compatible model list cannot be loaded safely."""
17
17
 
18
18
 
19
+ class OpenAICompatibleAPIError(ValueError):
20
+ """Raised when an authenticated OpenAI-compatible API check fails."""
21
+
22
+
19
23
  def models_endpoint(base_url: str) -> str:
20
24
  """Return the ``/models`` endpoint for an OpenAI-compatible base URL."""
21
25
  value = str(base_url or "").strip().rstrip("/")
@@ -26,6 +30,56 @@ def models_endpoint(base_url: str) -> str:
26
30
  return f"{value}/models"
27
31
 
28
32
 
33
+ def chat_completions_endpoint(base_url: str) -> str:
34
+ """Return the Chat Completions endpoint for an OpenAI-compatible base URL."""
35
+ value = str(base_url or "").strip().rstrip("/")
36
+ if not value:
37
+ raise OpenAICompatibleAPIError("Informe a Base URL antes do teste.")
38
+ if value.endswith("/chat/completions"):
39
+ return value
40
+ return f"{value}/chat/completions"
41
+
42
+
43
+ def validate_openai_compatible_api_key(
44
+ api_key: str,
45
+ base_url: str,
46
+ model: str,
47
+ *,
48
+ timeout: float = DEFAULT_TIMEOUT_SECONDS,
49
+ ) -> None:
50
+ """Validate credentials with one minimal authenticated Chat Completions call."""
51
+ token = str(api_key or "").strip()
52
+ model_id = str(model or "").strip()
53
+ if not token:
54
+ raise OpenAICompatibleAPIError("Informe a API key antes do teste.")
55
+ if not model_id:
56
+ raise OpenAICompatibleAPIError("Informe o modelo antes do teste.")
57
+
58
+ endpoint = chat_completions_endpoint(base_url)
59
+ headers = {
60
+ "Accept": "application/json",
61
+ "Authorization": f"Bearer {token}",
62
+ "Content-Type": "application/json",
63
+ }
64
+ payload = {
65
+ "model": model_id,
66
+ "messages": [{"role": "user", "content": "Responda apenas OK."}],
67
+ "max_tokens": 1,
68
+ "temperature": 0,
69
+ }
70
+ try:
71
+ response = requests.post(endpoint, headers=headers, json=payload, timeout=timeout)
72
+ except requests.RequestException as exc:
73
+ raise OpenAICompatibleAPIError(f"Não foi possível testar {endpoint}: {exc}") from exc
74
+
75
+ if response.status_code >= 400:
76
+ if response.status_code in (401, 403):
77
+ raise OpenAICompatibleAPIError(
78
+ f"O endpoint recusou a API key (HTTP {response.status_code}). Verifique a API key."
79
+ )
80
+ raise OpenAICompatibleAPIError(f"O endpoint de teste devolveu HTTP {response.status_code}.")
81
+
82
+
29
83
  def normalize_model_ids(payload: Any) -> list[str]:
30
84
  """Extract unique model IDs from the standard OpenAI response shape."""
31
85
  if isinstance(payload, dict):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
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",