11"""Llama Stack client retrieval class."""
22
33import logging
4-
4+ import ssl
55from typing import Optional
66
7+ import httpx
78from llama_stack import (
89 AsyncLlamaStackAsLibraryClient , # type: ignore
910)
1011from llama_stack_client import AsyncLlamaStackClient # type: ignore
11- from models .config import LlamaStackConfiguration
12+ from models .config import LlamaStackConfiguration , TLSSecurityProfile
1213from utils .types import Singleton
14+ from utils import tls
1315
1416
1517logger = logging .getLogger (__name__ )
@@ -20,6 +22,76 @@ class AsyncLlamaStackClientHolder(metaclass=Singleton):
2022
2123 _lsc : Optional [AsyncLlamaStackClient ] = None
2224
25+ def _construct_httpx_client (
26+ self , tls_security_profile : Optional [TLSSecurityProfile ]
27+ ) -> Optional [httpx .AsyncClient ]:
28+ """Construct HTTPX client with TLS security profile configuration.
29+
30+ Args:
31+ tls_security_profile: TLS security profile configuration.
32+
33+ Returns:
34+ Configured httpx.AsyncClient if TLS profile is set, None otherwise.
35+ """
36+ # if security profile is not set, return None to use default httpx client
37+ if tls_security_profile is None or tls_security_profile .profile_type is None :
38+ logger .info ("No TLS security profile configured, using default settings" )
39+ return None
40+
41+ logger .info ("TLS security profile: %s" , tls_security_profile .profile_type )
42+
43+ # get the TLS profile type
44+ profile_type = tls .TLSProfiles (tls_security_profile .profile_type )
45+
46+ # retrieve ciphers - custom list or profile-based
47+ ciphers = tls .ciphers_as_string (tls_security_profile .ciphers , profile_type )
48+ logger .info ("TLS ciphers: %s" , ciphers )
49+
50+ # retrieve minimum TLS version
51+ min_tls_ver = tls .min_tls_version (
52+ tls_security_profile .min_tls_version , profile_type
53+ )
54+ logger .info ("Minimum TLS version: %s" , min_tls_ver )
55+
56+ ssl_version = tls .ssl_tls_version (min_tls_ver )
57+ logger .info ("SSL version: %s" , ssl_version )
58+
59+ # check if TLS verification should be skipped (for testing only)
60+ if tls_security_profile .skip_tls_verification :
61+ logger .warning (
62+ "TLS verification is disabled. This is insecure and should "
63+ "only be used for testing purposes."
64+ )
65+ return httpx .AsyncClient (verify = False )
66+
67+ # create SSL context with the configured settings
68+ context = ssl .create_default_context ()
69+
70+ # load CA certificate if specified
71+ if tls_security_profile .ca_cert_path is not None :
72+ logger .info ("Loading CA certificate from: %s" , tls_security_profile .ca_cert_path )
73+ context .load_verify_locations (cafile = str (tls_security_profile .ca_cert_path ))
74+
75+ if ssl_version is not None :
76+ context .minimum_version = ssl_version
77+
78+ if ciphers is not None :
79+ # Note: TLS 1.3 ciphers cannot be set via set_ciphers() - they are
80+ # automatically negotiated when TLS 1.3 is used. The set_ciphers()
81+ # method only affects TLS 1.2 and below cipher selection.
82+ try :
83+ context .set_ciphers (ciphers )
84+ except ssl .SSLError as e :
85+ logger .warning (
86+ "Could not set ciphers '%s': %s. "
87+ "TLS 1.3 ciphers are automatically negotiated." ,
88+ ciphers ,
89+ e ,
90+ )
91+
92+ logger .info ("Creating httpx.AsyncClient with TLS security profile" )
93+ return httpx .AsyncClient (verify = context )
94+
2395 async def load (self , llama_stack_config : LlamaStackConfiguration ) -> None :
2496 """Retrieve Async Llama stack client according to configuration."""
2597 if llama_stack_config .use_as_library_client is True :
@@ -37,13 +109,20 @@ async def load(self, llama_stack_config: LlamaStackConfiguration) -> None:
37109 raise ValueError (msg )
38110 else :
39111 logger .info ("Using Llama stack running as a service" )
112+
113+ # construct httpx client with TLS security profile if configured
114+ http_client = self ._construct_httpx_client (
115+ llama_stack_config .tls_security_profile
116+ )
117+
40118 self ._lsc = AsyncLlamaStackClient (
41119 base_url = llama_stack_config .url ,
42120 api_key = (
43121 llama_stack_config .api_key .get_secret_value ()
44122 if llama_stack_config .api_key is not None
45123 else None
46124 ),
125+ http_client = http_client ,
47126 )
48127
49128 def get_client (self ) -> AsyncLlamaStackClient :
0 commit comments