@danhachuel/thunderbolt 0.4.13 → 0.4.15
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 +45 -44
- package/integrations/openai_model_discovery.py +26 -6
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1280,8 +1280,30 @@ def render_channel_edit_form(channel: dict, youtube_account_ids: list[str], yout
|
|
|
1280
1280
|
|
|
1281
1281
|
|
|
1282
1282
|
def render_home_update_controls() -> None:
|
|
1283
|
-
"""Render
|
|
1284
|
-
|
|
1283
|
+
"""Render update controls and only show actionable or execution-related notices."""
|
|
1284
|
+
cache_key = "home_update_version_check"
|
|
1285
|
+
checked_at_key = "home_update_version_checked_at"
|
|
1286
|
+
if not st.session_state.get(cache_key) or time.monotonic() - float(st.session_state.get(checked_at_key, 0)) > 300:
|
|
1287
|
+
st.session_state[cache_key] = check_version(APP_VERSION)
|
|
1288
|
+
st.session_state[checked_at_key] = time.monotonic()
|
|
1289
|
+
version_status = st.session_state[cache_key]
|
|
1290
|
+
notice_signature = f"{version_status.latest_version}|{version_status.update_available}|{version_status.error}"
|
|
1291
|
+
if st.session_state.get("home_update_notice_signature") != notice_signature:
|
|
1292
|
+
st.session_state["home_update_notice_signature"] = notice_signature
|
|
1293
|
+
st.session_state["home_update_notice_dismissed"] = False
|
|
1294
|
+
|
|
1295
|
+
update_result = st.session_state.get("home_update_result")
|
|
1296
|
+
notice_message = ""
|
|
1297
|
+
notice_kind = ""
|
|
1298
|
+
if update_result is not None and (not update_result.ok or update_result.restart_required):
|
|
1299
|
+
notice_message = str(update_result.message or "").strip()
|
|
1300
|
+
notice_kind = "success" if update_result.ok else "error"
|
|
1301
|
+
elif version_status.update_available:
|
|
1302
|
+
notice_message = f"Nova versão disponível: {display_version(version_status.latest_version)}. A versão actual é {APP_VERSION_LABEL or 'desconhecida'}."
|
|
1303
|
+
notice_kind = "info"
|
|
1304
|
+
notice_visible = bool(notice_message) and not st.session_state.get("home_update_notice_dismissed")
|
|
1305
|
+
|
|
1306
|
+
update_area, notice_area, close_area = st.columns([1.45, 3.55, 0.42], gap="small")
|
|
1285
1307
|
with update_area:
|
|
1286
1308
|
st.markdown(
|
|
1287
1309
|
"""
|
|
@@ -1297,6 +1319,18 @@ def render_home_update_controls() -> None:
|
|
|
1297
1319
|
border-color: #c4b5fd;
|
|
1298
1320
|
filter: brightness(1.08);
|
|
1299
1321
|
}
|
|
1322
|
+
div[data-testid="stAlert"] {
|
|
1323
|
+
width: fit-content;
|
|
1324
|
+
max-width: 100%;
|
|
1325
|
+
min-height: 0;
|
|
1326
|
+
padding: 0.45rem 0.75rem;
|
|
1327
|
+
margin: 0.2rem 0 0;
|
|
1328
|
+
display: inline-flex;
|
|
1329
|
+
align-items: center;
|
|
1330
|
+
}
|
|
1331
|
+
div[data-testid="stAlert"] p {
|
|
1332
|
+
margin: 0;
|
|
1333
|
+
}
|
|
1300
1334
|
</style>
|
|
1301
1335
|
""",
|
|
1302
1336
|
unsafe_allow_html=True,
|
|
@@ -1310,49 +1344,16 @@ def render_home_update_controls() -> None:
|
|
|
1310
1344
|
st.success("Actualização concluída. A reiniciar o Thunderbolt para aplicar a nova versão…")
|
|
1311
1345
|
time.sleep(0.8)
|
|
1312
1346
|
restart_current_process()
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
version_status = st.session_state[cache_key]
|
|
1320
|
-
notice_signature = f"{version_status.latest_version}|{version_status.update_available}|{version_status.error}"
|
|
1321
|
-
if st.session_state.get("home_update_notice_signature") != notice_signature:
|
|
1322
|
-
st.session_state["home_update_notice_signature"] = notice_signature
|
|
1323
|
-
st.session_state["home_update_notice_dismissed"] = False
|
|
1324
|
-
if version_status.update_available:
|
|
1325
|
-
notice_message = f"Nova versão disponível: {display_version(version_status.latest_version)}. A versão actual é {APP_VERSION_LABEL or 'desconhecida'}."
|
|
1326
|
-
notice_kind = "info"
|
|
1327
|
-
elif version_status.error:
|
|
1328
|
-
notice_message = f"Versão actual: {APP_VERSION_LABEL or 'desconhecida'} · verificação de actualização indisponível."
|
|
1329
|
-
notice_kind = "caption"
|
|
1330
|
-
else:
|
|
1331
|
-
notice_message = f"Versão actual: {APP_VERSION_LABEL or 'desconhecida'} · já está actualizada ({display_version(version_status.latest_version)})."
|
|
1332
|
-
notice_kind = "success"
|
|
1333
|
-
if not st.session_state.get("home_update_notice_dismissed"):
|
|
1334
|
-
notice_col, close_col = st.columns([8.5, 1])
|
|
1335
|
-
with notice_col:
|
|
1336
|
-
if notice_kind == "info":
|
|
1337
|
-
st.info(notice_message)
|
|
1338
|
-
elif notice_kind == "success":
|
|
1339
|
-
st.success(notice_message)
|
|
1340
|
-
else:
|
|
1341
|
-
st.caption(notice_message)
|
|
1342
|
-
with close_col:
|
|
1343
|
-
if st.button("×", key="home_update_notice_close", help="Fechar este aviso"):
|
|
1344
|
-
st.session_state["home_update_notice_dismissed"] = True
|
|
1345
|
-
st.rerun()
|
|
1346
|
-
update_result = st.session_state.get("home_update_result")
|
|
1347
|
-
if update_result is not None and not st.session_state.get("home_update_notice_dismissed"):
|
|
1348
|
-
result_col, result_close_col = st.columns([8.5, 1])
|
|
1349
|
-
with result_col:
|
|
1350
|
-
if update_result.ok:
|
|
1351
|
-
st.success(update_result.message)
|
|
1347
|
+
if notice_visible:
|
|
1348
|
+
with notice_area:
|
|
1349
|
+
if notice_kind == "info":
|
|
1350
|
+
st.info(notice_message)
|
|
1351
|
+
elif notice_kind == "success":
|
|
1352
|
+
st.success(notice_message)
|
|
1352
1353
|
else:
|
|
1353
|
-
st.error(
|
|
1354
|
-
with
|
|
1355
|
-
if st.button("×", key="
|
|
1354
|
+
st.error(notice_message)
|
|
1355
|
+
with close_area:
|
|
1356
|
+
if st.button("×", key="home_update_notice_close", help="Fechar este aviso"):
|
|
1356
1357
|
st.session_state["home_update_notice_dismissed"] = True
|
|
1357
1358
|
st.rerun()
|
|
1358
1359
|
|
|
@@ -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,11 +45,11 @@ 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
|
|
|
@@ -77,6 +90,13 @@ def validate_openai_compatible_api_key(
|
|
|
77
90
|
raise OpenAICompatibleAPIError(
|
|
78
91
|
f"O endpoint recusou a API key (HTTP {response.status_code}). Verifique a API key."
|
|
79
92
|
)
|
|
93
|
+
if response.status_code == 404 and "openrouter.ai" in str(urlsplit(base_url).netloc).casefold():
|
|
94
|
+
raise OpenAICompatibleAPIError(
|
|
95
|
+
"O OpenRouter devolveu HTTP 404. Confirme a Base URL "
|
|
96
|
+
"https://openrouter.ai/api/v1 e seleccione um modelo válido do catálogo "
|
|
97
|
+
"(por exemplo, openai/gpt-4o-mini). O teste envia POST para "
|
|
98
|
+
"https://openrouter.ai/api/v1/chat/completions."
|
|
99
|
+
)
|
|
80
100
|
raise OpenAICompatibleAPIError(f"O endpoint de teste devolveu HTTP {response.status_code}.")
|
|
81
101
|
|
|
82
102
|
|
package/package.json
CHANGED