@danhachuel/thunderbolt 0.4.14 → 0.4.16
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.
|
@@ -14,6 +14,7 @@ from integrations.openai_model_discovery import (
|
|
|
14
14
|
OpenAICompatibleAPIError,
|
|
15
15
|
fetch_openai_compatible_models,
|
|
16
16
|
validate_openai_compatible_api_key,
|
|
17
|
+
validate_openrouter_api_key,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -420,7 +421,10 @@ def test_llm_provider_card(card: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
420
421
|
if not model:
|
|
421
422
|
return {"ok": False, "status": "error", "message": "Missing configuration — introduza o modelo antes do teste."}
|
|
422
423
|
try:
|
|
423
|
-
|
|
424
|
+
if normalized["provider"] == "openrouter":
|
|
425
|
+
validate_openrouter_api_key(api_key, base_url, model)
|
|
426
|
+
else:
|
|
427
|
+
validate_openai_compatible_api_key(api_key, base_url, model)
|
|
424
428
|
except OpenAICompatibleAPIError as exc:
|
|
425
429
|
return {
|
|
426
430
|
"ok": False,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
6
7
|
|
|
7
8
|
import requests
|
|
8
9
|
|
|
@@ -20,11 +21,23 @@ class OpenAICompatibleAPIError(ValueError):
|
|
|
20
21
|
"""Raised when an authenticated OpenAI-compatible API check fails."""
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _normalise_http_base_url(base_url: str, *, error_type: type[Exception]) -> str:
|
|
25
|
+
"""Keep only the HTTP origin/path used to build OpenAI-compatible endpoints."""
|
|
26
|
+
raw = str(base_url or "").strip()
|
|
27
|
+
if not raw:
|
|
28
|
+
raise error_type("Informe a Base URL antes da chamada.")
|
|
29
|
+
try:
|
|
30
|
+
parsed = urlsplit(raw)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
raise error_type("A Base URL não é válida.") from exc
|
|
33
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
34
|
+
raise error_type("A Base URL deve começar por http:// ou https://.")
|
|
35
|
+
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", ""))
|
|
36
|
+
|
|
37
|
+
|
|
23
38
|
def models_endpoint(base_url: str) -> str:
|
|
24
39
|
"""Return the ``/models`` endpoint for an OpenAI-compatible base URL."""
|
|
25
|
-
value =
|
|
26
|
-
if not value:
|
|
27
|
-
raise ModelDiscoveryError("Informe a Base URL antes de consultar os modelos.")
|
|
40
|
+
value = _normalise_http_base_url(base_url, error_type=ModelDiscoveryError)
|
|
28
41
|
if value.endswith("/models"):
|
|
29
42
|
return value
|
|
30
43
|
return f"{value}/models"
|
|
@@ -32,14 +45,62 @@ def models_endpoint(base_url: str) -> str:
|
|
|
32
45
|
|
|
33
46
|
def chat_completions_endpoint(base_url: str) -> str:
|
|
34
47
|
"""Return the Chat Completions endpoint for an OpenAI-compatible base URL."""
|
|
35
|
-
value =
|
|
36
|
-
if not value:
|
|
37
|
-
raise OpenAICompatibleAPIError("Informe a Base URL antes do teste.")
|
|
48
|
+
value = _normalise_http_base_url(base_url, error_type=OpenAICompatibleAPIError)
|
|
38
49
|
if value.endswith("/chat/completions"):
|
|
39
50
|
return value
|
|
51
|
+
if value.endswith("/models"):
|
|
52
|
+
value = value.removesuffix("/models")
|
|
40
53
|
return f"{value}/chat/completions"
|
|
41
54
|
|
|
42
55
|
|
|
56
|
+
def validate_openrouter_api_key(
|
|
57
|
+
api_key: str,
|
|
58
|
+
base_url: str,
|
|
59
|
+
model: str,
|
|
60
|
+
*,
|
|
61
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Validate an OpenRouter key and model without spending credits on a chat call."""
|
|
64
|
+
token = str(api_key or "").strip()
|
|
65
|
+
model_id = str(model or "").strip()
|
|
66
|
+
if not token:
|
|
67
|
+
raise OpenAICompatibleAPIError("Informe a API key antes do teste.")
|
|
68
|
+
if not model_id:
|
|
69
|
+
raise OpenAICompatibleAPIError("Informe o modelo antes do teste.")
|
|
70
|
+
|
|
71
|
+
base = _normalise_http_base_url(base_url, error_type=OpenAICompatibleAPIError)
|
|
72
|
+
headers = {
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"Authorization": f"Bearer {token}",
|
|
75
|
+
}
|
|
76
|
+
key_endpoint = f"{base}/key"
|
|
77
|
+
try:
|
|
78
|
+
key_response = requests.get(key_endpoint, headers=headers, timeout=timeout)
|
|
79
|
+
except requests.RequestException as exc:
|
|
80
|
+
raise OpenAICompatibleAPIError("Não foi possível contactar o OpenRouter para validar a API key.") from exc
|
|
81
|
+
|
|
82
|
+
if key_response.status_code in (401, 403):
|
|
83
|
+
raise OpenAICompatibleAPIError(
|
|
84
|
+
f"A API key do OpenRouter foi recusada (HTTP {key_response.status_code}). Verifique a credencial."
|
|
85
|
+
)
|
|
86
|
+
if key_response.status_code >= 400:
|
|
87
|
+
raise OpenAICompatibleAPIError(
|
|
88
|
+
f"O diagnóstico da API key do OpenRouter devolveu HTTP {key_response.status_code}."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
available_models = fetch_openai_compatible_models(token, base, timeout=timeout)
|
|
93
|
+
except ModelDiscoveryError as exc:
|
|
94
|
+
raise OpenAICompatibleAPIError(
|
|
95
|
+
"A API key foi aceite, mas não foi possível consultar o catálogo de modelos OpenRouter."
|
|
96
|
+
) from exc
|
|
97
|
+
if model_id not in available_models:
|
|
98
|
+
raise OpenAICompatibleAPIError(
|
|
99
|
+
f"A API key OpenRouter foi validada, mas o modelo '{model_id}' não está no catálogo actual. "
|
|
100
|
+
"Clique em Consultar modelos e seleccione um modelo disponível."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
43
104
|
def validate_openai_compatible_api_key(
|
|
44
105
|
api_key: str,
|
|
45
106
|
base_url: str,
|
|
@@ -77,6 +138,13 @@ def validate_openai_compatible_api_key(
|
|
|
77
138
|
raise OpenAICompatibleAPIError(
|
|
78
139
|
f"O endpoint recusou a API key (HTTP {response.status_code}). Verifique a API key."
|
|
79
140
|
)
|
|
141
|
+
if response.status_code == 404 and "openrouter.ai" in str(urlsplit(base_url).netloc).casefold():
|
|
142
|
+
raise OpenAICompatibleAPIError(
|
|
143
|
+
"O OpenRouter devolveu HTTP 404. Confirme a Base URL "
|
|
144
|
+
"https://openrouter.ai/api/v1 e seleccione um modelo válido do catálogo "
|
|
145
|
+
"(por exemplo, openai/gpt-4o-mini). O teste envia POST para "
|
|
146
|
+
"https://openrouter.ai/api/v1/chat/completions."
|
|
147
|
+
)
|
|
80
148
|
raise OpenAICompatibleAPIError(f"O endpoint de teste devolveu HTTP {response.status_code}.")
|
|
81
149
|
|
|
82
150
|
|
package/package.json
CHANGED