feat: add LLM SSL verify control and auto CA cert download
- LLM_SSL_VERIFY env var to disable SSL verification for corporate proxies - install_llm_ca_certs() entrypoint function — downloads PEM/DER certs from LLM_CA_CERT_URLS, converts DER→PEM, installs to system CA store - _format_connection_error() — detailed exception chain logging - 7 new unit tests for _get_ssl_verify, _format_connection_error, verify= param - LLM_CA_CERT_URLS and LLM_SSL_VERIFY in .env.enterprise-clean - Removed duplicate @RELATION DEPENDS_ON -> [EXT:Library:tenacity]
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
# @BRIEF Services for LLM interaction and dashboard screenshots.
|
||||
# @LAYER Plugin
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]
|
||||
# @RELATION DEPENDS_ON -> [EXT:Library:tenacity]
|
||||
# @INVARIANT Screenshots must be 1920px width and capture full page height.
|
||||
# @DATA_CONTRACT DashboardSpec -> Screenshot + Analysis
|
||||
# @RATIONALE Extracted all hardcoded timeouts into named module-level constants (PLAYWRIGHT_NAVIGATION_TIMEOUT_MS, PLAYWRIGHT_WAIT_TIMEOUT_MS, PLAYWRIGHT_SHORT_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS, SCREENSHOT_SERVICE_TIMEOUT_MS, LLM_HTTP_TIMEOUT_S) and DEFAULT_USER_AGENT. Zero remaining numeric timeout literals.
|
||||
@@ -907,7 +905,14 @@ class LLMClient:
|
||||
# It routes to upstream providers transparently, and the default Authorization header
|
||||
# is sufficient. No additional headers like HTTP-Referer or X-API-Key are required.
|
||||
|
||||
http_client = httpx.AsyncClient(headers=default_headers, timeout=LLM_HTTP_TIMEOUT_S)
|
||||
ssl_verify = self._get_ssl_verify()
|
||||
logger.info(f"[LLMClient.__init__] SSL Verify: {ssl_verify} (set LLM_SSL_VERIFY=false to disable)")
|
||||
|
||||
http_client = httpx.AsyncClient(
|
||||
headers=default_headers,
|
||||
timeout=LLM_HTTP_TIMEOUT_S,
|
||||
verify=ssl_verify,
|
||||
)
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=self.api_key,
|
||||
base_url=base_url,
|
||||
@@ -916,6 +921,28 @@ class LLMClient:
|
||||
)
|
||||
# endregion LLMClient.__init__
|
||||
|
||||
# region LLMClient._get_ssl_verify [TYPE Function]
|
||||
# @PURPOSE Resolve SSL verification flag from environment.
|
||||
# @POST Returns False when LLM_SSL_VERIFY env var is "false"/"0"/"no" (case-insensitive).
|
||||
@staticmethod
|
||||
def _get_ssl_verify() -> bool:
|
||||
raw = os.getenv("LLM_SSL_VERIFY", "true").strip().lower()
|
||||
return raw not in ("false", "0", "no", "off")
|
||||
# endregion LLMClient._get_ssl_verify
|
||||
|
||||
# region LLMClient._format_connection_error [TYPE Function]
|
||||
# @PURPOSE Format exception chain for diagnostics, extracting httpx cause details.
|
||||
# @POST Returns a human-readable string with the full error chain.
|
||||
@staticmethod
|
||||
def _format_connection_error(exc: Exception) -> str:
|
||||
parts = [f"{type(exc).__name__}: {exc!s}"]
|
||||
cause = exc.__cause__ or exc.__context__
|
||||
while cause:
|
||||
parts.append(f" └─ {type(cause).__name__}: {cause!s}")
|
||||
cause = cause.__cause__ or cause.__context__
|
||||
return "\n".join(parts)
|
||||
# endregion LLMClient._format_connection_error
|
||||
|
||||
# region LLMClient._supports_json_response_format [TYPE Function]
|
||||
# @PURPOSE Detect whether provider/model is likely compatible with response_format=json_object.
|
||||
# @PRE Client initialized with base_url and default_model.
|
||||
@@ -1035,7 +1062,10 @@ class LLMClient:
|
||||
await asyncio.sleep(wait_time)
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[get_json_completion] LLM call failed: {e!s}")
|
||||
logger.error(
|
||||
f"[get_json_completion] LLM call failed.\n"
|
||||
f"{self._format_connection_error(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
if not response or not hasattr(response, 'choices') or not response.choices:
|
||||
|
||||
Reference in New Issue
Block a user