@danhachuel/thunderbolt 0.7.63 → 0.7.65

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.
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import hashlib
10
10
  import json
11
11
  import os
12
+ import re
12
13
  import time
13
14
  from contextlib import contextmanager
14
15
  from dataclasses import dataclass
@@ -274,6 +275,38 @@ def classify_response(response: Any) -> None:
274
275
  )
275
276
 
276
277
 
278
+ def _validate_llm_json_response(response: Any) -> None:
279
+ """Reject HTTP-200 LLM responses that cannot satisfy the JSON pool contract."""
280
+ try:
281
+ payload = response if isinstance(response, Mapping) else response.json()
282
+ except (ValueError, TypeError, AttributeError) as exc:
283
+ raise ProviderCallError(
284
+ "Provider LLM devolveu um corpo JSON de transporte inválido.",
285
+ category="payload",
286
+ retryable=False,
287
+ ) from exc
288
+ if not isinstance(payload, Mapping):
289
+ raise ProviderCallError("Provider LLM devolveu um payload inválido.", category="payload", retryable=False)
290
+ choices = payload.get("choices") or []
291
+ message = (choices[0] or {}).get("message") if choices and isinstance(choices[0], Mapping) else None
292
+ content = message.get("content") if isinstance(message, Mapping) else None
293
+ if isinstance(content, list):
294
+ content = "".join(str(item.get("text", "")) for item in content if isinstance(item, Mapping))
295
+ text = str(content or "").strip()
296
+ text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
297
+ text = re.sub(r"\s*```$", "", text).strip()
298
+ try:
299
+ parsed = json.loads(text)
300
+ except json.JSONDecodeError as exc:
301
+ raise ProviderCallError(
302
+ "Provider LLM devolveu conteúdo que não é JSON válido.",
303
+ category="payload",
304
+ retryable=False,
305
+ ) from exc
306
+ if not isinstance(parsed, dict):
307
+ raise ProviderCallError("Provider LLM devolveu JSON que não é um objecto.", category="payload", retryable=False)
308
+
309
+
277
310
  def enabled_cards(settings: Mapping[str, Any], pool: str) -> list[dict[str, Any]]:
278
311
  """Return cards in deterministic priority order; media pools keep active-card preference."""
279
312
  if pool == POOL_LLM:
@@ -426,11 +459,11 @@ def route_json_request(
426
459
  record_provider_attempt(record)
427
460
  if exc.retryable or exc.category in {"credential", "endpoint_or_model"}:
428
461
  set_provider_cooldown(card, exc.retry_after or cooldown or DEFAULT_COOLDOWN_SECONDS)
429
- if not exc.retryable:
462
+ if not exc.retryable and pool != POOL_LLM:
430
463
  raise ProviderRoutingError(str(exc), attempts=attempts) from exc
431
464
  if exc.retry_after:
432
465
  time.sleep(min(3600.0, exc.retry_after))
433
- elif cooldown > 0 and len(attempts) < maximum:
466
+ elif exc.retryable and cooldown > 0 and len(attempts) < maximum:
434
467
  time.sleep(min(cooldown, 5.0))
435
468
  except requests.RequestException as exc:
436
469
  classified = classify_request_exception(exc)
@@ -473,6 +506,9 @@ def route_llm_json(settings: Mapping[str, Any], system_prompt: str, user_prompt:
473
506
  }
474
507
  if not body["model"]:
475
508
  raise ProviderCallError("O cartão LLM não tem modelo configurado.", category="payload", retryable=False)
476
- return requests.post(endpoint, headers=headers, json=body, timeout=120)
509
+ response = requests.post(endpoint, headers=headers, json=body, timeout=120)
510
+ if 200 <= int(getattr(response, "status_code", 0) or 0) < 300:
511
+ _validate_llm_json_response(response.json())
512
+ return response
477
513
 
478
514
  return route_json_request(settings, pool=POOL_LLM, cards=None, request=request)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.63",
3
+ "version": "0.7.65",
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",